背景

最近有一个减库存的场景,存在并发安全问题,因为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); // 记录出库日志
}

总结

在使用分布式锁时,要注意数据库事务提交与解锁时机,避免分布式锁失效。

Logo

鸿蒙生态一站式服务平台。

更多推荐