情景回顾
某天运营同事发现管理后台生成了两条重复的订单,反馈到技术部门进行紧急排查。
伪代码分析
通过排查代码发现,发现核心代码有并发问题,根据事务传递性锁中的事务无法独立生效,所以锁释放时事务并没有提交,此时新的请求获得锁时查询到的数据依然是更新前的数据,故锁没有起到应有的效果。
代码如下:
@Override
@Transactional(rollbackFor = Exception.class)
public void addOrder(User user) {
RLockTemplate.lockGet(user.getUserId(),()->{
//进行生成订单操作 insert into t_order
saveOrder(user.getUserId());//这个方法底层也有事务
return true;
});
//保存日志 insert into t_log
saveLog(user.getUserId());
}
解决方案
这里去掉最外层的事务,将事务放在分布式锁里面。
解决幂等性问题总结建议
事务应该在分布式锁的里面进行控制。首先获取分布式锁保证在分布式环境中相关操作的原子性和一致性,之后在锁的保护下进行事务操作,这样可以确保在并发环境下,对共享资源的访问和修改是安全的,操作完成后再释放分布式锁。如果把事务放在分布式锁的外面,则无法保证事务内的操作在并发场景下的安全性和一致性。
其他:
-
唯一事务号或ID:
API调用时传递一个唯一标识符(比如UUID或数据库自增ID),服务端对这个ID做校验和记录,确保相同ID的请求只被执行一次。 -
数据库唯一约束:
在数据库表中设置唯一约束或索引,可以防止相同的记录被插入多次。 -
乐观锁:
在数据表中使用版本号或时间戳字段,当进行更新操作时检查这个版本号是否变化,若没变化则允许更新,否则拒绝,以此避免数据重复操作。 -
分布式锁:
使用类似Redis、ZooKeeper这样的分布式锁工具,在执行关键部分前获得锁,确保分布式环境中同时只有一个线程能操作共享资源。 -
Token机制:
服务端生成一个Token返回给客户端,客户端执行操作时携带这个Token,服务端通过Token来识别请求,执行成功后Token失效,确保请求只被执行一次。 -
幂等框架:
使用一些成熟的框架比如Idempotent、Spring Retry等,可以直接在方法上添加注解来实现幂等性,无需自己实现复杂的逻辑。 -
消息队列:
使用消息队列确保消息消费的幂等性,比如Kafka等持久化消息,配合消息的offset实现消息处理的幂等性处理。
具体使用哪种方案取决于系统的需求和架构设计。一些方案可能要与业务紧密结合,有些则可以通过中间件来解耦实现。