背景

商品销售扣减库存是常见的场景,考虑性能的可以使用redis存储库存进行扣减,并发小的也可以采用数据量库存占用记录实时计算方式,最近开发的功能由于并发量不大,考虑到实现简洁的因素,决定采用库存占用记录实时计算方式。

实现流程

  • 使用redisson获取分布式
  • 查询库存占用表计算剩余库存数量
  • 插入库存占用表
  • 释放分布式锁

出现的问题

  • 问题描述
    由于使用了springboot注解式事务,导致分布式锁释放之后才提交事务,从锁释放到事务提交成功这段时间,其他事务能获取到分布式锁,但是由于事务还未提交,其他事务读取不到当前插入的库存占用记录,导致存在超卖的现象。
  • 问题截图
    配置的库存数量是20
    在这里插入图片描述
    模拟代码
    在这里插入图片描述
    实际库存占用是34,超卖14
    在这里插入图片描述

解决方案

方案汇总

  1. 使用编程式事务,手动提交事务后再释放分布式锁
  2. 将事务隔离级别改为读未提交
  3. 手动挂载spring事务完成钩子函数,在钩子函数释放分布式锁,需要添加事务
  4. 自动挂载spring事务完成钩子函数,自动释放分布式锁,需要添加事务
  5. 去除事务

方案分析

方案1:实现简单,但是无法统一封装好,使用麻烦,分布式锁需在调用扣库存方法时由调用方获取与释放。
方案2:简单,改动小,但是需要数据库支持,由于项目的oracle数据库不支持读未提交,故未采用。
方案3:实现简单,但是重复代码多,实现效果如下:
在这里插入图片描述
在这里插入图片描述
方案4:可用性强,使用简洁。
实现思路:将方案三的释放分布式锁逻辑自动挂载到spring事务完成钩子函数
实现步骤:

  • 重写spring事务钩子函数doCleanupAfterCompletion
    在这里插入图片描述
/**
 * @description 事务整合redis分布式锁
 * @date 2024/5/23
 */
@Slf4j
public class JdbcLockTransactionManager extends JdbcTransactionManager {

    private static final ThreadLocal<List<RLock>> LOCKS = new ThreadLocal<>();

    public JdbcLockTransactionManager(DataSource dataSource) {
        super(dataSource);
    }

    @Override
    protected void doCleanupAfterCompletion(Object transaction) {
        super.doCleanupAfterCompletion(transaction);
        //释放redis锁
        this.clearLock();
    }

    /**
     * @description:注册事务相关分布式锁
     * @date 15:24 2024/5/23
     * @param lock 分布式锁
     **/
    public static void registerLock(@NonNull RLock lock) {
        if (lock == null) {
            return;
        }
        List<RLock> lockList = LOCKS.get();
        if (lockList == null) {
            lockList = new ArrayList<>(1);
            LOCKS.set(lockList);
        }
        lockList.add(lock);
    }

    /** 清除redis锁 */
    private void clearLock() {
        List<RLock> locks = LOCKS.get();
        if (CollUtil.isEmpty(locks)) {
            return;
        }
        try {
            for (RLock lock : locks) {
                if (!(lock instanceof RedissonMultiLock) && !lock.isHeldByCurrentThread()) {
                    log.error("redis lock:[{}] auto released ", lock.getName());
                    return;
                }
                try {
                    lock.unlock();
                } catch (Exception ex) {
                    log.error(String.format("redis unlock:[%s] error", lock.getName()), ex);
                }
            }
        } finally {
            LOCKS.remove();
        }
    }
}
  • 参照DataSourceTransactionManagerAutoConfiguration自动挂载释放分布式锁的JdbcLockTransactionManager类
    在这里插入图片描述
/**
 * @description spring自动事务配置
 * @date 2024/5/23
 * @see org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({JdbcTemplate.class, TransactionManager.class})
@AutoConfigureOrder(Ordered.LOWEST_PRECEDENCE)
@AutoConfigureBefore(DataSourceTransactionManagerAutoConfiguration.class)
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceTransactionManagerAutoCfg {

    @Bean
    public DataSourceTransactionManager transactionManager(DataSource dataSource, ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
        DataSourceTransactionManager transactionManager = new JdbcLockTransactionManager(dataSource);
        transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(transactionManager));
        return transactionManager;
    }
}
  • 挂载分布式锁到threadLocal
    在这里插入图片描述
  • 效果

在这里插入图片描述

Logo

更多推荐