情景回顾

某天运营同事发现管理后台生成了两条重复的订单,反馈到技术部门进行紧急排查。

伪代码分析

通过排查代码发现,发现核心代码有并发问题,根据事务传递性锁中的事务无法独立生效,所以锁释放时事务并没有提交,此时新的请求获得锁时查询到的数据依然是更新前的数据,故锁没有起到应有的效果。
代码如下:

@Override@Transactional(rollbackFor = Exception.class)public void addOrder(User user) {RLockTemplate.lockGet(user.getUserId(),()->{//进行生成订单操作 insert into t_ordersaveOrder(user.getUserId());//这个方法底层也有事务return true;});//保存日志 insert into t_logsaveLog(user.getUserId());}

解决方案

这里去掉最外层的事务,将事务放在分布式锁里面。

解决幂等性问题总结建议

事务应该在分布式锁的里面进行控制。首先获取分布式锁保证在分布式环境中相关操作的原子性和一致性,之后在锁的保护下进行事务操作,这样可以确保在并发环境下,对共享资源的访问和修改是安全的,操作完成后再释放分布式锁。如果把事务放在分布式锁的外面,则无法保证事务内的操作在并发场景下的安全性和一致性。

其他:

  1. 唯一事务号或ID:
    API调用时传递一个唯一标识符(比如UUID或数据库自增ID),服务端对这个ID做校验和记录,确保相同ID的请求只被执行一次。

  2. 数据库唯一约束:
    在数据库表中设置唯一约束或索引,可以防止相同的记录被插入多次。

  3. 乐观锁:
    在数据表中使用版本号或时间戳字段,当进行更新操作时检查这个版本号是否变化,若没变化则允许更新,否则拒绝,以此避免数据重复操作。

  4. 分布式锁:
    使用类似Redis、ZooKeeper这样的分布式锁工具,在执行关键部分前获得锁,确保分布式环境中同时只有一个线程能操作共享资源。

  5. Token机制:
    服务端生成一个Token返回给客户端,客户端执行操作时携带这个Token,服务端通过Token来识别请求,执行成功后Token失效,确保请求只被执行一次。

  6. 幂等框架:
    使用一些成熟的框架比如Idempotent、Spring Retry等,可以直接在方法上添加注解来实现幂等性,无需自己实现复杂的逻辑。

  7. 消息队列:
    使用消息队列确保消息消费的幂等性,比如Kafka等持久化消息,配合消息的offset实现消息处理的幂等性处理。

具体使用哪种方案取决于系统的需求和架构设计。一些方案可能要与业务紧密结合,有些则可以通过中间件来解耦实现。