1.问题
//使用Redisson组件实现分布式锁
@Autowired
private Redisson redisson;
//创建资产的伪代码
@Transactional(rollbackFor = Exception.class)
public void save() {
//获取锁
RLock lock = redisson.getLock("lock_name");
//根据资产唯一key查询数据库
//判断资产是否存在,如果存在则直接返回并且提示资产已存在
//如果资产不存在,则进行资产的创建
//资产创建的逻辑.....
//释放锁
lock.unlock();
}
首先解释上面的代码逻辑:
- 因为创建资产逻辑中涉及到很多表的操作,所以需要保证原子性,因此使用了声明式事物@Transactional注解来保证其原子性.
- 又因为资产不能重复,所以需要保证唯一性,因此使用了锁(Redisson)的Lock使请求串行化,并且根据唯一key进行判断,以此来保证唯一性.
产生的问题:
- 首先资产表中的唯一标识没有加唯一索引(也是一个问题,但是不在本次讨论范围之内)
- 在uat环境的压测中发现偶尔会出现重复的资产数据
2.原因
首先说明我们Mysql数据库的隔离级别为:可重复读
- 使用了@Transactional注解,所以Spring会对这个接口进行代理,并且进行事物的控制,只有在当前方法执行完成之后,才会去提交事物或者回滚事物.
- 而lock.unlock()释放锁的逻辑是定义在当前方法中的,也就是说当前方法执行完成之后,锁就已经被释放掉了,这个时候后面的线程就能够获取锁执行业务逻辑了.
- 但是此时事物还没提交完成,因为隔离级别的原因,后面线程根据唯一key查询出来的结果为空,所以会继续执行后面的代码逻辑,从而就导致的数据的重复.
3.解决方案
1.将事物的代码放到lock的逻辑当中进行处理
//使用Redisson组件实现分布式锁
@Autowired
private Redisson redisson;
@Autowired
private TestService testService;
public void save() {
//获取锁
RLock lock = redisson.getLock("lock_name");
//注意,这里不能直接调用当前类的方法,否则会使事物失效,因为调用当前类的方法时,没办法走代理逻辑
//具体可以百度Spring事物失效的几种情况
testService.save();
//释放锁
lock.unlock();
}
@Service
public class TestService{
@Transactional(rollbackFor = Exception.class)
public void save() {
//根据资产唯一key查询数据库
//判断资产是否存在,如果存在则直接返回并且提示资产已存在
//如果资产不存在,则进行资产的创建
//资产创建的逻辑.....
}
}
2.使用编程式事物的方式进行处理
//使用Redisson组件实现分布式锁
@Autowired
private Redisson redisson;
@Autowired
private PlatformTransactionManager transactionManager;
public void save() {
//获取锁
RLock lock = redisson.getLock("lock_name");
//手动获取一个事物
//这里只说明解决问题的思路,一般项目不需要这么写编程式事物,都会进行封装,然后直接使用
TransactionStatus transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
//根据资产唯一key查询数据库
//判断资产是否存在,如果存在则直接返回并且提示资产已存在
//如果资产不存在,则进行资产的创建
//资产创建的逻辑.....
//手动提交事物
transactionManager.commit(transaction);
} catch (Exception e) {
//手动回滚事物
transactionManager.rollback(transaction);
}
//释放锁
lock.unlock();
}
}