前言
不知大家是否有觀察到一個最常見的錯誤:
先開啟事務,然后針對資源加鎖,操作資源,然后釋放鎖,最后提交事務
你是否發現了在這樣的場景下會出現并發安全的問題?
(提示:一個線程A在事務內部釋放鎖,另一個線程B拿到了鎖,線程B看不到線程A的操作 導致 線程B 重復執行線程A已經對資源進行的操作)
用一個業務場景去說明
用一個電商系統的訂單處理場景來具體說明這個“事務沒有完全被鎖包住”會導致的問題。
需求: 用戶點擊“下單”按鈕后,后臺要:
檢查該用戶是否已經提交過該訂單(防重復下單)
如果沒有,就創建訂單
并扣減庫存
錯誤的業務代碼:
@Transactional
public void createOrder(String userId, String orderNo) {if (!orderService.hasOrdered(orderNo)) {synchronized (("lock:" + orderNo).intern()) {// 鎖內邏輯// 此處加鎖}// 鎖釋放了,但事務還沒提交orderService.save(orderNo); // 保存訂單productService.decreaseStock(); // 扣減庫存}
}
1、線程A
開啟事務
查詢:是否已下單?→ 查詢不到(因為數據庫未提交)
執行下單邏輯:準備插入訂單
!! 鎖在事務內部,提前釋放
事務還沒提交!
2、線程B
緊接著執行相同操作
開啟事務
查詢:是否已下單?→ 同樣查詢不到(線程A沒提交)
執行下單邏輯:插入重復訂單、扣減庫存
提交事務
最終結果
因為 數據庫在**“讀已提交**”隔離級別下,線程B看不到線程A未提交的插入
又因為加鎖只包了業務邏輯而不是整個事務范圍
所以鎖一旦提前釋放,線程B就能并發進來了
線程A和線程B都成功下了單
結果就是 重復支付 / 重復下單
改進
public void createOrderSafe(String userId, String orderNo) {synchronized (("lock:" + orderNo).intern()) { // 線程B被阻塞doCreateOrder(userId, orderNo); // 鎖保護整個事務 內部是本地事務}
}
或者微服務中 使用redis分布式鎖
RLock lock = redissonClient.getLock("order:" + orderNo);
if (lock.tryLock()) {try {// 事務中執行訂單判斷與插入} finally {lock.unlock(); // 鎖直到事務結束才釋放}
}
總結
鎖放在事務外部