解決重復(fù)下單問題,常提到"一鎖二判三更新"到底是什么?
前言
大家好,我是田螺。
我們在聊到后端并發(fā)問題,或者說重復(fù)下單問題的時候,經(jīng)常提到"一鎖二判三更新".這個"一鎖而判三更新",到底是什么呢?本文田螺哥跟大家聊聊哈~~
- 什么是一鎖二判三更新
- 為什么需要一鎖二判三更新?
- 不同鎖策略下的實現(xiàn)差異
1.什么是一鎖二判三更新
其實,它是一套處理并發(fā)更新數(shù)據(jù)的標準流程:
- 一鎖:表示先獲取鎖,保證同一時間只有一個操作能執(zhí)行
- 二判:檢查數(shù)據(jù)狀態(tài)是否符合預(yù)期,防止臟更新
- 三更新:確認無誤后執(zhí)行數(shù)據(jù)更新操作
比如扣庫存的場景,我們來看一個一鎖二判三更新的代碼例子:
//一鎖二判三更新的代碼使用例子
@Transactional
public boolean deductStock(Long productId, int quantity) {
// 一鎖:獲取商品的行鎖
// 使用for update進行悲觀鎖鎖定,確保同一時間只有一個事務(wù)能操作該商品
Product product = productMapper.selectForUpdateById(productId);
if (product == null) {
throw new RuntimeException("商品不存在");
}
// 二判:判斷庫存是否充足
if (product.getStock() < quantity) {
throw new RuntimeException("庫存不足,當(dāng)前庫存:" + product.getStock());
}
// 三更新:執(zhí)行庫存扣減
int newStock = product.getStock() - quantity;
product.setStock(newStock);
int rows = productMapper.updateById(product);
return rows > 0;
}對應(yīng)的 MyBatis update更新方法:
<select id="selectForUpdateById" resultType="com.example.Product">
select * from product where id = #{id}
for update
</select>2. 為什么需要一鎖二判三更新
在并發(fā)場景,為什么需要一鎖二判三更新這套使用流程呢?
假設(shè)類似這種場景:
兩個用戶同時給同一個商品下單,而商品僅剩最后一件庫存。如果沒有加鎖,可能會出現(xiàn)兩個訂單都創(chuàng)建成功,但實際庫存不足的情況。
同理,也是這個場景,假設(shè)你加鎖了,如果沒有這個二判(判斷庫存是否充足),依然可能會出現(xiàn)兩個訂單創(chuàng)建都成功的情況。
錯誤使用例子:
// 錯誤示例:無鎖無判斷
public boolean deductStock(Long productId, int quantity) {
// 1. 查詢當(dāng)前庫存
Product product = productMapper.selectById(productId);
// 2. 直接扣減庫存(未判斷是否充足)
int newStock = product.getStock() - quantity;
product.setStock(newStock);
// 3. 更新庫存
return productMapper.updateById(product) > 0;
}因此,"一鎖二判三更新" 正是為這類并發(fā)場景設(shè)計的解決方案。單獨使用鎖或單獨做判斷都無法徹底解決問題,必須三者結(jié)合。
3. 不同鎖策略下的實現(xiàn)差異
一鎖二判三更新中的一鎖,其實有不同的實現(xiàn)方式的,既有悲觀鎖,也有樂觀鎖。
對于悲觀鎖,適合寫操作比較頻繁、沖突概率高的場景:
- 優(yōu)點:實現(xiàn)簡單,沖突處理直接
- 缺點:可能導(dǎo)致鎖等待,并發(fā)性能較低
比如,我們前面第一小節(jié)的就是悲觀鎖哈實現(xiàn)方式哈
<select id="selectForUpdateById" resultType="com.example.Product">
select * from product where id = #{id}
for update
</select>如果讀操作頻繁、寫操作沖突概率低的場景,則更適合用樂觀鎖。簡單demo代碼如下:
// 樂觀鎖實現(xiàn):一鎖(版本控制)二判三更新
public boolean deductStock(Long productId, int quantity) {
// 獲取當(dāng)前商品信息(包含版本號)
Product product = productMapper.selectById(productId);
if (product == null) {
throw new RuntimeException("商品不存在");
}
// 二判:判斷庫存是否充足
if (product.getStock() < quantity) {
throw new RuntimeException("庫存不足");
}
// 準備更新數(shù)據(jù)
int newStock = product.getStock() - quantity;
product.setStock(newStock);
// 版本號+1(樂觀鎖的關(guān)鍵)
product.setVersion(product.getVersion() + 1);
// 執(zhí)行更新(WHERE條件包含版本號,相當(dāng)于一鎖的實現(xiàn))
int rows = productMapper.updateWithVersion(product);
// 如果更新行數(shù)為0,說明版本號已變,并發(fā)沖突
if (rows == 0) {
throw new RuntimeException("并發(fā)更新沖突,請重試");
}
returntrue;
}很多時候,解決并發(fā)問題,我們使用的是Redis分布式鎖。
在分布式系統(tǒng)中,"一鎖" 通常會升級為分布式鎖,而 "二判" 在庫存場景下核心就是判斷庫存是否滿足需求。
以 Redis 分布式鎖為例,實現(xiàn)分布式環(huán)境下的庫存扣減簡單代碼如下:
// 分布式環(huán)境下的"一鎖二判三更新"
public boolean deductStock(Long productId, int quantity) {
// 分布式鎖的key,通常用業(yè)務(wù)標識+ID
String lockKey = "stock:lock:" + productId;
// 生成唯一標識,用于釋放鎖時的身份驗證
String requestId = UUID.randomUUID().toString();
try {
// 一鎖:獲取分布式鎖(演示用的)
// 第三個參數(shù)是超時時間,防止死鎖
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, 10, TimeUnit.SECONDS);
if (!locked) {
// 獲取鎖失敗,說明有其他進程正在操作,返回失敗或重試
returnfalse;
}
// 二判:查詢并判斷庫存
Product product = productMapper.selectById(productId);
if (product == null) {
throw new RuntimeException("商品不存在");
}
if (product.getStock() < quantity) {
throw new RuntimeException("庫存不足,當(dāng)前庫存:" + product.getStock());
}
// 三更新:扣減庫存
int newStock = product.getStock() - quantity;
product.setStock(newStock);
int rows = productMapper.updateById(product);
return rows > 0;
} finally {
// 釋放分布式鎖(需要驗證身份,防止誤刪其他進程的鎖)
if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
}


























