标签:
今天公司的系统发现一个bug:主表记录的已还款总额和还款记录表里面的偿还金额之和不一致。看到这个问题,我的第一反应是怀疑还款的时候离线锁没生效,导致并发修改主表记录。可是经过查看日志和代码,排除了这个可能性。然后又怀疑可能是由于还款之后,修改已还款总额和还款状态时只调用了jpa的save,没有flush,导致没及时写入数据库,别的线程更新的时候不是最新数据。但再一想,发现不对,因为还款的操作是在事务之中进行的,事务结束,jpa会自动把修改写入数据库,应该不会出现这个问题。后来请来大牛帮忙分析,终于发现了这个巨坑。。。。
先说一下代码结构
1 @Transactional(value = Transactional.TxType.REQUIRES_NEW) 2 public void repayInTranaction() { 3 repay(); 4 } 5 // 调用返回的时候,主表的修改才flush到数据库 6 7 private void repay() { 8 try { 9 lock.lock(); 10 // 查询主表记录 11 // 插入还款记录,状态为待还款 12 // 还款 13 // 修改还款记录,状态为已成功 14 // 将该次还款金额加到主表的已还款总额,这个地方修改过后调用了save, 没有flush 15 } catch (Exception e) { 16 // error handling 17 } finally { 18 lock.unlock(); 19 } 20 }
其中,repayInTransaction方法会被并发调用。
也许经验比较丰富的大牛看到这段示例代码立刻就能看出问题所在。是的,正如本文的标题所说,问题的关键就在于事务隔离。spring transaction的默认隔离级别(关于隔离级别,可参考http://blog.sina.com.cn/s/blog_539d361e0100ncf6.html)是“已提交读”,也就是说,如果一个事务修改了某一行数据,但尚未提交,此时另外一个事务来读取同一行是不能读取到第一个事务的修改的。
在上面的代码中,如果第一个线程执行完repay方法,但repayInTranaction方法尚未返回,这个时候lock已经释放,但事务尚未提交,新的线程创建了新的事务,要来修改同一条主表记录。由于锁已经释放,新线程可以执行到第10行的部分,读出第一个线程已经修改但尚未提交的数据。然后第一个线程提交第一个事务,接着第二个线程提交第二个事务,悲剧就发生了。。。
补救方法比较简单,修改一下代码结构,让锁的作用范围比事务的范围更大就ok了。修改后代码如下:
1 public void repay() { 2 try { 3 lock.lock(); 4 repayInTranaction(); 5 } catch (Exception e) { 6 // error handling 7 } finally { 8 lock.unlock(); 9 } 10 } 11 12 @Transactional(value = Transactional.TxType.REQUIRES_NEW) 13 public void repayInTranaction() { 14 // 查询主表记录 15 // 插入还款记录,状态为待还款 16 // 还款 17 // 修改还款记录,状态为已成功 18 // 将该次还款金额加到主表的已还款总额,这个地方修改过后调用了save, 没有flush 19 }
标签:
原文地址:http://www.cnblogs.com/segeon/p/4399354.html