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


























