springboot事务结合分布式锁超卖问题
商品销售扣减库存是常见的场景,考虑性能的可以使用redis存储库存进行扣减,并发小的也可以采用数据量库存占用记录实时计算方式,最近开发的功能由于并发量不大,考虑到实现简洁的因素,决定采用库存占用记录实时计算方式。
·
背景
商品销售扣减库存是常见的场景,考虑性能的可以使用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);
}
@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
- 效果
更多推荐
所有评论(0)