Spring?Retry?實現(xiàn)樂觀鎖重試實踐記錄
一、場景分析
假設(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 '采購價格',
sale_price decimal(10, 2) not null comment '銷售價格',
origin_stock int unsigned default '0' not null comment '初始庫存',
sold_stock int unsigned default '0' not null comment '已售庫存',
stock int unsigned default '0' not null comment '實時庫存',
occupy_stock int unsigned default '0' not null comment '訂單占用庫存',
version int default 0 not null comment '樂觀鎖版本號',
created_time datetime not null comment '創(chuàng)建時間',
updated_time datetime not null comment '更新時間'
)
comment '促銷管理服務(wù)-秒殺商品SKU';一張簡單的秒殺商品SKU表。使用 version 字段做樂觀鎖。使用 unsigned 關(guān)鍵字,限制 int 類型非負(fù),防止庫存超賣。
使用 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對象", description = "促銷管理-秒殺商品SKU")
public class PmsSecKillSku implements Serializable {
private static final long serialVersionUID = 1L;
// @Version 注解不能遺漏
@Version
@ApiModelProperty("樂觀鎖版本號")
private Integer version;
// other properties ......
}現(xiàn)在有這么一個支付回調(diào)接口:
/**
* 促銷管理-秒殺商品SKU 服務(wù)實現(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è)每次只能秒殺一個數(shù)量的SKU
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void paySucCallback(Integer skuId) {
// 持久化庫存
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());
// 占用庫存減1
wt.setOccupyStock( pmsSkuVo.getOccupyStock()-1 );
// 已售庫存加1
wt.setSoldStock( pmsSkuVo.getSoldStock()+1 );
// 實時庫存減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("請刷新后重新取消!");
}
}
}該方法的目的,是為了進行庫存更新,當(dāng)樂觀鎖版本號有沖突時,對方法進行休眠重試。
該方法在測試環(huán)境還能正常跑,到了生產(chǎn)環(huán)境,卻頻繁報 "請刷新后重新取消!"
仔細(xì)分析后發(fā)現(xiàn),測試環(huán)境的MYSQL數(shù)據(jù)庫全局隔離級別是,READ-COMMITTED(讀已提交)。而生產(chǎn)環(huán)境是 REPEATABLE_READ(可重復(fù)讀)。
SHOW GLOBAL VARIABLES LIKE 'transaction_isolation';
- 在讀已提交隔離級別下,該方法每次重試,都能讀取到別的事務(wù)提交的最新的 version,相當(dāng)于拿到樂觀鎖。
- 在可重復(fù)讀隔離級別下,因為有 MVCC 多版本并發(fā)控制,該方法每次重試,讀取到的都是同一個結(jié)果,相當(dāng)于一直拿不到樂觀鎖。所以多次循環(huán)之后,count 還是等于 0,程序拋出異常。
二、簡單驗證
假設(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ù)程序:

- 增加會話的隔離級別為 isolation = Isolation.REPEATABLE_READ 可以重復(fù)讀。
- 增加記錄日志。
- 在方法更新前阻塞當(dāng)前線程,模擬另一個事務(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次查詢的時候,執(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ù)庫的版本號只有3:

2.2、讀已提交
修改會話的隔離級別為 isolation = Isolation.READ_COMMITTED 讀已提交。

恢復(fù)數(shù)據(jù):

訪問接口:
### POST http://localhost:5910/pmsSecKillSku/pay?id=1 Content-Type: application/json token: 123
在第0次查詢的時候,執(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次查詢的時候,version=2;執(zhí)行完 SQL語句,第1次查詢的時候,version=3;拿到了樂觀鎖,更新成功。
三、最佳實踐
可以看到,使用 Thread.sleep 配合循環(huán)來進行獲取樂觀鎖的重試,存在一些問題:
- 依賴事務(wù)隔離級別的正確設(shè)置。
- 休眠的時間不好把控。
- 代碼復(fù)用性差。
Spring Retry 提供了一種更優(yōu)雅的方式,來進行樂觀鎖的重試。
恢復(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è)置為固定的500毫秒
// 可以根據(jù)系統(tǒng)的并發(fā)度,來設(shè)置
// 并發(fā)度高,設(shè)置長一點,并發(fā)度低,設(shè)置短一點
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());
// 占用庫存減1
wt.setOccupyStock( pmsSkuVo.getOccupyStock()-1 );
// 已售庫存加1
wt.setSoldStock( pmsSkuVo.getSoldStock()+1 );
// 實時庫存減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)樂觀鎖沖突的時候,拋出異常, OptimisticLockingFailureException。
這里特意設(shè)置事務(wù)隔離級別為 REPEATABLE_READ
3.3、測試
訪問接口:
@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次查詢的時候,執(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;

可以看到,在第二次查詢的時候,就獲取到鎖,并成功執(zhí)行更新。
到此這篇關(guān)于Spring Retry 實現(xiàn)樂觀鎖重試的文章就介紹到這了,更多相關(guān)Spring Retry 樂觀鎖重試內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring Boot整合Seata的過程詳解(AT 模式)
文章介紹了如何在SpringBoot項目中整合Seata框架來管理跨服務(wù)、跨數(shù)據(jù)庫的分布式事務(wù),本文結(jié)合實例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2025-10-10
Tomcat數(shù)據(jù)源配置方法_JBuilder中
今天幫一同事配置一個數(shù)據(jù)源,采用tomcat5.5.9,本來是個很簡單的事,以前也配過,但由于很長時間沒用過容器提供的數(shù)據(jù)源了(IOC用慣了),也只記的個大概了,所以剛開始一配就出錯了,google了一下,有很多資料,照著試試卻都不好使(到不是別人說的不對,只是大家用的版本不同)。2008-10-10
三種SpringBoot中實現(xiàn)異步調(diào)用的方法總結(jié)
Spring Boot 提供了多種方式來實現(xiàn)異步任務(wù),這篇文章主要為大家介紹了常用的三種實現(xiàn)方式,文中的示例代碼講解詳細(xì),需要的可以參考一下2023-05-05
java8學(xué)習(xí)教程之lambda表達(dá)式的使用方法
Java8最值得學(xué)習(xí)的特性就是Lambda表達(dá)式,下面這篇文章主要給大家介紹了關(guān)于java8學(xué)習(xí)教程之lambda表達(dá)式使用的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-09-09
Java弱鍵集合WeakHashMap及ConcurrentCache原理詳解
這篇文章主要介紹了Java弱鍵集合WeakHashMap及ConcurrentCache原理詳解,基于哈希表的Map接口實現(xiàn),支持null鍵和值,但是WeakHashMap具有弱鍵,可用來實現(xiàn)緩存存儲,在進行GC的時候會自動回收鍵值對,需要的朋友可以參考下2023-09-09

