背景
商品销售扣减库存是常见的场景,考虑性能的可以使用redis存储库存进行扣减,并发小的也可以采用数据量库存占用记录实时计算方式,最近开发的功能由于并发量不大,考虑到实现简洁的因素,决定采用库存占用记录实时计算方式。
实现流程
- 使用redisson获取分布式
- 查询库存占用表计算剩余库存数量
- 插入库存占用表
- 释放分布式锁
出现的问题
- 问题描述
由于使用了springboot注解式事务,导致分布式锁释放之后才提交事务,从锁释放到事务提交成功这段时间,其他事务能获取到分布式锁,但是由于事务还未提交,其他事务读取不到当前插入的库存占用记录,导致存在超卖的现象。 - 问题截图
配置的库存数量是20
模拟代码
实际库存占用是34,超卖14
解决方案
方案汇总
- 使用编程式事务,手动提交事务后再释放分布式锁
- 将事务隔离级别改为读未提交
- 手动挂载spring事务完成钩子函数,在钩子函数释放分布式锁,需要添加事务
- 自动挂载spring事务完成钩子函数,自动释放分布式锁,需要添加事务
- 去除事务
方案分析
方案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);}@Overrideprotected 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 {@Beanpublic DataSourceTransactionManager transactionManager(DataSource dataSource, ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {DataSourceTransactionManager transactionManager = new JdbcLockTransactionManager(dataSource);transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(transactionManager));return transactionManager;}
}
- 挂载分布式锁到threadLocal
- 效果