⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7484023895268278310 「后端程序员Aska」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

用了”一锁二判三更新”,但是幂等被击穿

代码案例

在我们的项目中,我们很多地方都会为了避免并发,增加分布式锁,并且会采用一锁、二判、三更新的方式实现一个幂等的逻辑。

同时,为了方便大家使用分布式锁,我们自己定义了一个@DistributeLock的直接。也是就有以下代码:

同一个方法中,增加了多个注解,同时有@DistributeLock和@Transactional两个注解。

这种情况在,我们自定义的注解@DistributeLock的切面默认会在最后执行,于是这段代码就是会先执行事务,然后再执行加锁。

最终的逻辑就像下面这段代码一样:

@Transactional(rollbackFor = Exception.class)
public boolean register(Request request) {
RLock lock = redisson.getLock(request.getIdentifier());
try {
//一锁
lock.lock();
//二查
User user = userMapper.find(request.getIdentifier());
if (user != null) {
returnfalse;
}
//三更新,保存订单数据
userMapper.insertOrder(request);
} finally {
lock.unlock();
}
returntrue;
}

按照这个顺序执行的:

  • 进入事务
  • 加锁
  • 解锁
  • 事务提交

这时候就会出现一种情况,在第三步和第四步中间,如果有一个其他的线程也调用这个 register 方法了。

那么就会出现一个问题,锁已经释放了,但是事务还没提交。这时候其他的线程在并发请求过来的时候:

  • 一锁: 拿锁可以拿到,因为锁被释放了
  • 二查: 查询数据也查不到,因为这时候之前的那个事务可能还没提交,未提交的数据,新的事务是看不到的。
  • 三更新: 执行更新操作,导致数据重复或者报错。

这就是我们需要解决的问题,那么看上去就是事务的切面执行顺序的问题,我们应该让锁的粒度大于事务的粒度就能解决了这个问题了。

那么,就想办法让分布式锁的注解的切面先执行。解决办法就是借助@Order注解,他可以直接用在切面类上,用于指定切面的执行顺序。值越小,优先级越高,切面会越早执行。

所以,修改后的分布式锁的切面类如下:

新增 Order 注解,把他的优先级设置为最小值,即优先级最高,最先开始执行即可。

总结

在使用分布式锁的时候,习惯性的尽量缩小同步代码块的范围

但是如果数据库隔离级别是可重复读,这种情况下不要把分布式锁加在@Transactional注解的事务方法内部。

因为可能会出现这种情况:

线程1开启事务A后获取分布式锁,执行业务代码后在事务内释放了分布式锁。这时候线程1开启了事务B获取到了线程1释放的分布式锁,执行查询操作时查到的数据就可能出现问题。因为此时事务A是在事务内释放了锁,事务A本身还没有完成提交。

文章目录
  1. 1. 代码案例
  2. 2. 总结