背景
商品銷售扣減庫存是常見的場景,考慮性能的可以使用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
- 效果