Spring?Retry?實(shí)現(xiàn)樂觀鎖重試實(shí)踐記錄
一、場(chǎng)景分析
假設(shè)有這么一張表:
create table pms_sec_kill_sku ( id int auto_increment comment '主鍵ID' primary key, spec_detail varchar(50) not null comment '規(guī)格描述', purchase_price decimal(10, 2) not null comment '采購(gòu)價(jià)格', sale_price decimal(10, 2) not null comment '銷售價(jià)格', origin_stock int unsigned default '0' not null comment '初始庫(kù)存', sold_stock int unsigned default '0' not null comment '已售庫(kù)存', stock int unsigned default '0' not null comment '實(shí)時(shí)庫(kù)存', occupy_stock int unsigned default '0' not null comment '訂單占用庫(kù)存', version int default 0 not null comment '樂觀鎖版本號(hào)', created_time datetime not null comment '創(chuàng)建時(shí)間', updated_time datetime not null comment '更新時(shí)間' ) comment '促銷管理服務(wù)-秒殺商品SKU';
一張簡(jiǎn)單的秒殺商品SKU表。使用 version 字段做樂觀鎖。使用 unsigned 關(guān)鍵字,限制 int 類型非負(fù),防止庫(kù)存超賣。
使用 MybatisPlus 來配置樂觀鎖:
import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.annotation.EnableTransactionManagement; @EnableTransactionManagement @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 分頁插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 樂觀鎖插件 interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } @Bean public DefaultDBFieldHandler defaultDBFieldHandler() { DefaultDBFieldHandler defaultDBFieldHandler = new DefaultDBFieldHandler(); return defaultDBFieldHandler; } }
@Data @TableName("pms_sec_kill_sku") @ApiModel(value = "PmsSecKillSku對(duì)象", description = "促銷管理-秒殺商品SKU") public class PmsSecKillSku implements Serializable { private static final long serialVersionUID = 1L; // @Version 注解不能遺漏 @Version @ApiModelProperty("樂觀鎖版本號(hào)") private Integer version; // other properties ...... }
現(xiàn)在有這么一個(gè)支付回調(diào)接口:
/** * 促銷管理-秒殺商品SKU 服務(wù)實(shí)現(xiàn)類 * @since 2025-02-26 14:21:42 */ @Service public class PmsSecKillSkuServiceImpl extends ServiceImpl<PmsSecKillSkuMapper, PmsSecKillSku> implements PmsSecKillSkuService { // 最大重試次數(shù) private static final int MAX_RETRIES = 3; /** * 訂單支付成功回調(diào) * 假設(shè)每次只能秒殺一個(gè)數(shù)量的SKU */ @Override @Transactional(rollbackFor = Exception.class) public void paySucCallback(Integer skuId) { // 持久化庫(kù)存 int count = 0, retries = 0; while (count == 0 && retries < MAX_RETRIES) { PmsSecKillSkuVo pmsSkuVo = this.baseMapper.findDetailById(skuId); PmsSecKillSku wt = new PmsSecKillSku(); wt.setId(pmsSkuVo.getId()); wt.setVersion(pmsSkuVo.getVersion()); // 占用庫(kù)存減1 wt.setOccupyStock( pmsSkuVo.getOccupyStock()-1 ); // 已售庫(kù)存加1 wt.setSoldStock( pmsSkuVo.getSoldStock()+1 ); // 實(shí)時(shí)庫(kù)存減1 wt.setStock( pmsSkuVo.getStock()-1 ); count = this.baseMapper.updateById(wt); retries++; if (count == 0) { try { Thread.sleep(100); } catch (InterruptedException e) { throw new BusinessException(e.getMessage()); } } } if (count == 0) { throw new BusinessException("請(qǐng)刷新后重新取消!"); } } }
該方法的目的,是為了進(jìn)行庫(kù)存更新,當(dāng)樂觀鎖版本號(hào)有沖突時(shí),對(duì)方法進(jìn)行休眠重試。
該方法在測(cè)試環(huán)境還能正常跑,到了生產(chǎn)環(huán)境,卻頻繁報(bào) "請(qǐng)刷新后重新取消!"
仔細(xì)分析后發(fā)現(xiàn),測(cè)試環(huán)境的MYSQL數(shù)據(jù)庫(kù)全局隔離級(jí)別是,READ-COMMITTED(讀已提交)。而生產(chǎn)環(huán)境是 REPEATABLE_READ(可重復(fù)讀)。
SHOW GLOBAL VARIABLES LIKE 'transaction_isolation';
- 在讀已提交隔離級(jí)別下,該方法每次重試,都能讀取到別的事務(wù)提交的最新的 version,相當(dāng)于拿到樂觀鎖。
- 在可重復(fù)讀隔離級(jí)別下,因?yàn)橛?MVCC 多版本并發(fā)控制,該方法每次重試,讀取到的都是同一個(gè)結(jié)果,相當(dāng)于一直拿不到樂觀鎖。所以多次循環(huán)之后,count 還是等于 0,程序拋出異常。
二、簡(jiǎn)單驗(yàn)證
假設(shè)現(xiàn)在表里面有這么一條記錄:
INSERT INTO `pms_sec_kill_sku` (`id`, `spec_detail`, `purchase_price`, `sale_price`, `origin_stock`, `sold_stock`, `stock`, `occupy_stock`, `version`, `created_time`, `updated_time`) VALUES (1, '尺碼:M1', 100.00, 1.00, 2, 0, 2, 2, 2, '2025-02-26 15:51:22', '2025-02-26 15:51:24');
2.1、可重復(fù)讀
修改服務(wù)程序:
- 增加會(huì)話的隔離級(jí)別為 isolation = Isolation.REPEATABLE_READ 可以重復(fù)讀。
- 增加記錄日志。
- 在方法更新前阻塞當(dāng)前線程,模擬另一個(gè)事務(wù)先提交。
@Api(value = "促銷管理-秒殺商品SKU", tags = {"促銷管理-秒殺商品SKU接口"}) @RestController @RequiredArgsConstructor @RequestMapping("pmsSecKillSku") public class PmsSecKillSkuController { private final PmsSecKillSkuService pmsSecKillSkuService; @ApiOperation(value = "支付成功", notes = "支付成功") @PostMapping("/pay") public R<Void> pay(Integer id) { pmsSecKillSkuService.paySucCallback(id); return R.success(); } }
訪問接口:
### POST http://localhost:5910/pmsSecKillSku/pay?id=1 Content-Type: application/json token: 123
在第0次查詢的時(shí)候,執(zhí)行更新:
UPDATE `pms_sec_kill_sku` SET sold_stock = sold_stock + 1, stock = stock - 1, occupy_stock = occupy_stock - 1, version = version + 1 WHERE id = 1;
可以看到,三次查詢,返回結(jié)果都是一樣的:
數(shù)據(jù)庫(kù)的版本號(hào)只有3:
2.2、讀已提交
修改會(huì)話的隔離級(jí)別為 isolation = Isolation.READ_COMMITTED 讀已提交。
恢復(fù)數(shù)據(jù):
訪問接口:
### POST http://localhost:5910/pmsSecKillSku/pay?id=1 Content-Type: application/json token: 123
在第0次查詢的時(shí)候,執(zhí)行更新:
UPDATE `pms_sec_kill_sku` SET sold_stock = sold_stock + 1, stock = stock - 1, occupy_stock = occupy_stock - 1, version = version + 1 WHERE id = 1;
可以看到,第0次查詢的時(shí)候,version=2;執(zhí)行完 SQL語句,第1次查詢的時(shí)候,version=3;拿到了樂觀鎖,更新成功。
三、最佳實(shí)踐
可以看到,使用 Thread.sleep 配合循環(huán)來進(jìn)行獲取樂觀鎖的重試,存在一些問題:
- 依賴事務(wù)隔離級(jí)別的正確設(shè)置。
- 休眠的時(shí)間不好把控。
- 代碼復(fù)用性差。
Spring Retry 提供了一種更優(yōu)雅的方式,來進(jìn)行樂觀鎖的重試。
恢復(fù)數(shù)據(jù):
3.1、配置重試模板
import org.springframework.retry.policy.SimpleRetryPolicy; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.retry.annotation.EnableRetry; import org.springframework.retry.backoff.FixedBackOffPolicy; import org.springframework.retry.support.RetryTemplate; @Configuration @EnableRetry public class RetryConfig { @Bean public RetryTemplate retryTemplate() { RetryTemplate retryTemplate = new RetryTemplate(); // 設(shè)置重試策略,這里設(shè)置最大重試次數(shù)為3次 SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(3); retryTemplate.setRetryPolicy(retryPolicy); // 設(shè)置重試間隔時(shí)間,這里設(shè)置為固定的500毫秒 // 可以根據(jù)系統(tǒng)的并發(fā)度,來設(shè)置 // 并發(fā)度高,設(shè)置長(zhǎng)一點(diǎn),并發(fā)度低,設(shè)置短一點(diǎn) FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy(); backOffPolicy.setBackOffPeriod(500); retryTemplate.setBackOffPolicy(backOffPolicy); return retryTemplate; } }
3.2、使用 Spring 的@Retryable注解
@Override @Retryable(value = OptimisticLockingFailureException.class) @Transactional(rollbackFor = Exception.class, isolation = Isolation.REPEATABLE_READ) public void paySucRetry(Integer skuId) { PmsSecKillSkuVo pmsSkuVo = this.baseMapper.findDetailById(skuId); log.info("===============查詢結(jié)果為{}", pmsSkuVo); PmsSecKillSku wt = new PmsSecKillSku(); wt.setId(pmsSkuVo.getId()); wt.setVersion(pmsSkuVo.getVersion()); // 占用庫(kù)存減1 wt.setOccupyStock( pmsSkuVo.getOccupyStock()-1 ); // 已售庫(kù)存加1 wt.setSoldStock( pmsSkuVo.getSoldStock()+1 ); // 實(shí)時(shí)庫(kù)存減1 wt.setStock( pmsSkuVo.getStock()-1 ); try { Thread.sleep(10000); } catch (InterruptedException e) { throw new BusinessException(e.getMessage()); } int count = this.baseMapper.updateById(wt); if (count == 0) { throw new OptimisticLockingFailureException("樂觀鎖沖突"); } }
當(dāng)樂觀鎖沖突的時(shí)候,拋出異常, OptimisticLockingFailureException。
這里特意設(shè)置事務(wù)隔離級(jí)別為 REPEATABLE_READ
3.3、測(cè)試
訪問接口:
@Api(value = "促銷管理-秒殺商品SKU", tags = {"促銷管理-秒殺商品SKU接口"}) @RestController @RequiredArgsConstructor @RequestMapping("pmsSecKillSku") public class PmsSecKillSkuController { private final PmsSecKillSkuService pmsSecKillSkuService; @ApiOperation(value = "可重試", notes = "可重試") @PostMapping("/retry") public R<Void> retry(Integer id) { pmsSecKillSkuService.paySucRetry(id); return R.success(); } }
### POST http://localhost:5910/pmsSecKillSku/retry?id=1 Content-Type: application/json token: 123
在第0次查詢的時(shí)候,執(zhí)行更新:
UPDATE `pms_sec_kill_sku` SET sold_stock = sold_stock + 1, stock = stock - 1, occupy_stock = occupy_stock - 1, version = version + 1 WHERE id = 1;
可以看到,在第二次查詢的時(shí)候,就獲取到鎖,并成功執(zhí)行更新。
到此這篇關(guān)于Spring Retry 實(shí)現(xiàn)樂觀鎖重試的文章就介紹到這了,更多相關(guān)Spring Retry 樂觀鎖重試內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot使用spring retry重試機(jī)制的操作詳解
- Java中使用Spring Retry實(shí)現(xiàn)重試機(jī)制的流程步驟
- spring @retryable不生效的一種場(chǎng)景分析
- 重試框架Guava-Retry和spring-Retry的使用示例
- Spring-Retry(重試機(jī)制)解讀
- SpringBoot中使用spring-retry 解決失敗重試調(diào)用
- Spring-retry實(shí)現(xiàn)循環(huán)重試功能
- spring-retry組件的使用教程
- Spring @Retryable注解輕松搞定循環(huán)重試功能
相關(guān)文章
SpringCloud全局過慮器GlobalFilter的用法小結(jié)
這篇文章主要介紹了SpringCloud全局過慮器GlobalFilter的使用,全局過慮器使用非常廣泛,比如驗(yàn)證是否登錄,全局性的處理,黑名單或白名單的校驗(yàn)等,本文結(jié)合示例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-07-07Spring Bean的包掃描的實(shí)現(xiàn)方法
這篇文章主要介紹了Spring Bean的包掃描的實(shí)現(xiàn)方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-01-01手動(dòng)模擬JDK動(dòng)態(tài)代理的方法
這篇文章主要介紹了手動(dòng)模擬JDK動(dòng)態(tài)代理的方法,幫助大家更好的了解和學(xué)習(xí)Java 代理的相關(guān)知識(shí),感興趣的朋友可以了解下2020-11-11教你如何把Eclipse創(chuàng)建的Web項(xiàng)目(非Maven)導(dǎo)入Idea
這篇文章主要介紹了教你如何把Eclipse創(chuàng)建的Web項(xiàng)目(非Maven)導(dǎo)入Idea,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04SpringDataElasticsearch與SpEL表達(dá)式實(shí)現(xiàn)ES動(dòng)態(tài)索引
這篇文章主要介紹了SpringDataElasticsearch與SpEL表達(dá)式實(shí)現(xiàn)ES動(dòng)態(tài)索引,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的朋友可以參考一下2022-09-09SpringBoot?Webflux創(chuàng)建TCP/UDP?server并使用handler解析數(shù)據(jù)
這篇文章主要介紹了SpringBoot?Webflux創(chuàng)建TCP/UDP?server并使用handler解析數(shù)據(jù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02hibernate 中 fetch=FetchType.LAZY 懶加載失敗處理方法
這篇文章主要介紹了hibernate 中 fetch=FetchType.LAZY 懶加載失敗處理方法,需要的朋友可以參考下2017-09-09詳解Java如何在業(yè)務(wù)代碼中優(yōu)雅的使用策略模式
這篇文章主要為大家介紹了Java如何在業(yè)務(wù)代碼中優(yōu)雅的使用策略模式,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的可以了解下2023-08-08