背景
最近有一个减库存的场景,存在并发安全问题,因为redisson客户端对分布式锁的支持比较成熟,所以使用了redisson的分布式锁来保证并发安全问题,避免超卖。代码如下:
@Transactional
public void allot(Param param) {
RLock lock = redissonClient.getLock(key);
try {
lock.lock();
query(param); // 查询库存是否满足条件,不满足则抛异常,结束
invoke(param); // 减库存
saveLog(param); // 记录出库日志
} catch (Exception e) {
log.error("分配失败", e);
throw e;
} finally {
lock.unlock();
}
}
问题点暴露
测试阶段,测试反馈压测存在超卖问题。我自己也模拟了一下并发请求,发现果真如此。
下面我们从执行流程的角度看分析一下,这段代码存在什么问题
执行流程
注意,spring的@Transactional事务注解,开启事务的时机是在方法中第一条SQL真正执行前。
执行流程如下:
可以发现,此流程中在事务还未提交的时候,redisson分布式锁 unlock先一步解锁了,数据库此时并未真正的扣减库存,也就是数据并未更新。此时如果有其他线程进入该方法,读取到的当然还是未扣减的库存,再执行扣减操作,当然就有问题了。
解决方案
将加锁解锁的代码 放在事务注解修饰的方法的外层就可以了,代码如下
public void take(Param param){
RLock lock = redissonClient.getLock(key);
try {
lock.lock();
allot(param);
} catch (Exception e) {
log.error("分配失败", e);
throw e;
} finally {
lock.unlock();
}
}
@Transactional
public void allot(Param param) {
query(param); // 查询库存是否满足条件,不满足则抛异常,结束
invoke(param); // 减库存
saveLog(param); // 记录出库日志
}
总结
在使用分布式锁时,要注意数据库事务提交与解锁时机,避免分布式锁失效。