场景
假设这样一个场景,一个业务中有主单 masterOrder ,子单 subOrder,主单子单的关系为一对多,主单和子单的状态有未完成、已完成。
业务逻辑是,当所有的子单更新成已完成时,主单也更新为已完成。
例如实现的伪代码如下:
@Transactional
public void completed(String subOrderId) {
// 获取主单号
String masterOrderId = orderRepository.getMasterOrderId(subOrderId);
// 加锁主单号
Lock lock = redisLock.getLock(masterOrderId);
lock.lock();
// 更新子单为已完成
orderRepository.updateSubOrderStatusCompleted(subOrderId);
// 查询未完成的子单条数
int unCompletedCount = orderRepository.countUnCompleted(masterOrderId);
if unCompletedCount == 0 {
// 更新主单
orderRepository.updateMasterOrderStatusCompleted(masterOrderId)
}
lock.unlock();
}
以上代码看上去业务逻辑挺正常的,然而在并发场景下有可能导致错误,主单永远不能更新成已完成。
场景分析
场景1:B线程会因事务隔离原因查询仍然存在一条子订单,导致主订单状态无法得到更新;
假设还剩下两个子单未完成
A 线程 B 线程
开启事务 开启事务
锁主单
更新子单为已完成 锁主单等待
查询未完成条数,结果为1,不更新 ...
解锁 ...
获取锁成功
更新子单为已完成
查询未完成条数,结果为1,不更新主订单状态 (事务隔离导致)
提交事务 解锁
提交事务
场景2:A 线程就算解锁后,马上提交了事务,如果数据库事务隔离级别为可重复读的情况下也会导致主订单状态无法得到更新;
假设还剩下两个子单未完成
A 线程 B 线程
开启事务 开启事务
锁主单
更新子单为已完成 锁主单等待
查询未完成条数,结果为1,不更新 ...
解锁 ...
提交事务
获取锁成功
更新子单为已完成
查询未完成条数,结果为1,不更新主订单状态 (可重复读的事务隔离级别)
解锁
提交事务
只有在 B 线程开启事务,发生在 A 线程提交事务之后,才会没有问题
假设还剩下两个子单未完成
A 线程 B 线程
开启事务
锁主单
更新子单为已完成
查询未完成条数,结果为1,不更新
解锁
提交事务
开启事务
获取锁成功
更新子单为已完成
查询未完成条数,结果为0,更新主订单状态
解锁
提交事务
所以需要在开启事务前,加上分布式锁。
样例
举例1:切面事务
@PostMapping("/hello")
public String hello() {
Lock lock = redisLock.getLock(key);
try {
//加锁
if(lock.lock()){
xxxService.completed();
}else{
throw new BusyException(("系统繁忙,请稍后再试!");
}
}catch (InterruptedException e){
throw new BusyException(("系统繁忙,请稍后再试!");
}finally {
lock.unlock();
}
}
// xxxService.completed 代码
@Transcational
public void completed() {
// 业务代码
}
举例2:编程事务
@Autowired
private TransactionTemplate transactionTemplate;
@Transcational
public void completed() {
Lock lock = redisLock.getLock(key);
try {
//加锁
if(lock.lock()){
transactionTemplate.executeWithoutResult(status -> {
//业务代码
});
}else{
throw new BusyException(("系统繁忙,请稍后再试!");
}
}catch (InterruptedException e){
throw new BusyException(("系统繁忙,请稍后再试!");
}finally {
lock.unlock();
}
}
评论区