最近生产环境零星出现了几笔脏数据,即同一业务编号出现了两条数据(我们系统中唯一性并未依靠于数据库的索引)。明明代码中已经加锁了, 还出现这样的问题,经定位,发现是事务的隔离性,导致第二个事务看不到第一个事务的数据,从而导致数据重复。
业务伪代码
// 省略了一些必要的异常处理。
@Transactional
public void saveAndComplete(T entity) {
lock.lock();
code = repository.findByCode(entity.getCode());
if (code.isEmpty()) {
repository.save(entity);
}
this.Complete(entity.getCode());
lock.unlock();
}
代码很简单,有一个saveAndComplete
方法,用来保存实体,并完成任务,并用Spring的事务管理器管理事务,当Complete
方法抛出异常时,进行回滚。但是如果数据库事务隔离级别为读已提交及以上,在高并发量下,还是会出现重复数据。
原因分析
- 事务
t1
启动。 - 事务
t2
启动。 - 线程
t1
获取锁,成功。 - 事务
t2
获取锁,失败,并等待。 - 线程
t1
检查是否存在entity1
,发现不存在,保存数据。 - 线程
t1
释放锁。 - 事务
t1
提交。 - 事务
t2
获取锁。 - 事务
t2
检查是否存在entity1
,发现不存在,保存数据。 - 事务
t2
释放锁。 - 事务
t2
提交。
主要原因就在步骤9,由于InnoDB
默认的隔离级别是可重复读
,所以即使事务t1
已经将entity1
插入数据库,事务t2
也是看不到的,所以会出现重复数据。
问题解决
问题解决起来也不难,只要保证事务t2
在事务t1
结束之后再开始即可,即插入数据这个动作的事务线性化。代码如下
// 省略了一些必要的异常处理。
public void saveAndComplete(T entity) {
lock.lock();
transactionManager.getTransaction(); // 先获取锁,再开启事务
code = repository.findByCode(entity.getCode());
if (code.isEmpty()) {
repository.save(entity);
}
this.Complete(entity.getCode());
transactionManager.commit(); // 先提交事务,最后释放锁。
lock.unlock();
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。