您的位置:首页 > 其它

由事物隔离级别引发的血案

2015-04-07 20:43 169 查看
今天公司的系统发现一个bug:主表记录的已还款总额和还款记录表里面的偿还金额之和不一致。看到这个问题,我的第一反应是怀疑还款的时候离线锁没生效,导致并发修改主表记录。可是经过查看日志和代码,排除了这个可能性。然后又怀疑可能是由于还款之后,修改已还款总额和还款状态时只调用了jpa的save,没有flush,导致没及时写入数据库,别的线程更新的时候不是最新数据。但再一想,发现不对,因为还款的操作是在事务之中进行的,事务结束,jpa会自动把修改写入数据库,应该不会出现这个问题。后来请来大牛帮忙分析,终于发现了这个巨坑。。。。

先说一下代码结构

@Transactional(value = Transactional.TxType.REQUIRES_NEW)
public void repayInTranaction() {
repay();
}
// 调用返回的时候,主表的修改才flush到数据库

private void repay() {
try {
lock.lock();
// 查询主表记录
// 插入还款记录,状态为待还款
// 还款
// 修改还款记录,状态为已成功
// 将该次还款金额加到主表的已还款总额,这个地方修改过后调用了save, 没有flush
} catch (Exception e) {
// error handling
} finally {
lock.unlock();
}
}


其中,repayInTransaction方法会被并发调用。

也许经验比较丰富的大牛看到这段示例代码立刻就能看出问题所在。是的,正如本文的标题所说,问题的关键就在于事务隔离。spring transaction的默认隔离级别(关于隔离级别,可参考http://blog.sina.com.cn/s/blog_539d361e0100ncf6.html)是“已提交读”,也就是说,如果一个事务修改了某一行数据,但尚未提交,此时另外一个事务来读取同一行是不能读取到第一个事务的修改的。

在上面的代码中,如果第一个线程执行完repay方法,但repayInTranaction方法尚未返回,这个时候lock已经释放,但事务尚未提交,新的线程创建了新的事务,要来修改同一条主表记录。由于锁已经释放,新线程可以执行到第10行的部分,读出第一个线程已经修改但尚未提交的数据。然后第一个线程提交第一个事务,接着第二个线程提交第二个事务,悲剧就发生了。。。

补救方法比较简单,修改一下代码结构,让锁的作用范围比事务的范围更大就ok了。修改后代码如下:

public void repay() {
try {
lock.lock();
repayInTranaction();
} catch (Exception e) {
// error handling
} finally {
lock.unlock();
}
}

@Transactional(value = Transactional.TxType.REQUIRES_NEW)
public void repayInTranaction() {
// 查询主表记录
// 插入还款记录,状态为待还款
// 还款
// 修改还款记录,状态为已成功
// 将该次还款金额加到主表的已还款总额,这个地方修改过后调用了save, 没有flush
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: