事務跟行鎖(Lock)的關係
首先解釋下事務和鎖各自的作用
- 事務的作用
事務主要保證一組數據庫操作(增刪改)的原子性, 即要麼全部執行成功,要麼全部失敗,避免出現數據不一致的中間狀態. -
加鎖的作用
加鎖主要解決併發場景下的數據競爭問題, 比如多個請求同時修改同一條數據時,可能導致"髒讀""不可重複讀"等問題.如果僅僅使用事務不用鎖的侷限性
事務操作雖然能保證原子性,但是但是無法阻止併發場景下的數據競爭問題. 以'庫存扣減'為例,假設商品初始庫存為10,同時有兩個訂單(A和B)都要買5件,流程都是"查詢庫存->判斷足夠->扣減庫存".
場景1:僅用事務不加鎖會出現超賣
- 訂單 A 的事務開始,查詢庫存:10(此時還沒扣減)。
- 訂單 B 的事務開始,也查詢庫存:10(因為 A 的扣減還沒提交,B 讀到的是舊數據)。
- 訂單 A 判斷庫存足夠,扣減 5,庫存變為 5,提交事務。
- 訂單 B 也判斷庫存足夠(基於之前讀到的 10),扣減 5,庫存變為 0,提交事務。
結果: 最終庫存0,兩個訂單各賣5件,看似沒有問題,但如果兩個訂單都買6件哪? - A查庫存10->扣6(剩4)->提交
-
B查庫存10->扣6(剩-2)->提交
這裏事務雖然保證了A和B各自的'扣減操作'是原子性的(不會只扣一半), 但無法阻止B在A未完成時讀取並修改同一份庫存數據,這就是併發數據競爭導致的問題.場景2: 事務+鎖,避免超賣
加鎖的作用是讓併發操作"排隊",確保同一時間只有一個事務能夠修改目標數據,其它事務必須等待,從而避免基於舊數據做判斷.
- 悲觀鎖(直接鎖定數據,阻止併發修改)
-
在查詢庫存的時候就鎖定對應數據, 其它事務必須等當前事務完成才能操作
事務跟行鎖(Lock)結合使用
在 ThinkPHP 中,使用行鎖(Lock) 鎖定記錄時,必須將鎖操作寫在事務內部。這是因為數據庫的行鎖依賴事務的上下文,只有在事務中才能保證鎖的有效性和原子性。
原因分析
- 行鎖的特性:
數據庫的行鎖(如for update)需要在事務中生效, 事務提交或者回滾後,鎖會自動釋放. 如果在事務外執行行鎖操作,鎖會立即釋放,無法達到預期的鎖定效果. -
原子性保證
事務的核心作用是保證一組操作的原子性(要麼全部成功,要麼全部失敗).將鎖操作放在事務內,可以確保鎖定的記錄在事務完成前不會被其它事務修改,避免併發衝突.正確示例(鎖在事務內部)
use think\Db; use app\model\User; // 開啓事務 Db::startTrans(); try { // 1. 在事務內鎖定記錄(FOR UPDATE) $user = User::where('id', 1) ->lock(true) // 等同於 FOR UPDATE ->find(); if (!$user) { throw new \Exception('用户不存在'); } // 2. 執行更新操作(依賴鎖定的記錄) $user->balance -= 100; $user->save(); // 3. 提交事務(鎖自動釋放) Db::commit(); echo '操作成功'; } catch (\Exception $e) { // 回滾事務(鎖自動釋放) Db::rollback(); echo '操作失敗:' . $e->getMessage(); }錯誤的示例(鎖在事務外)
// 錯誤:鎖在事務外,會立即釋放 $user = User::where('id', 1)->lock(true)->find(); Db::startTrans(); try { // 此時鎖已釋放,可能被其他事務修改 $user->balance -= 100; $user->save(); Db::commit(); } catch (\Exception $e) { Db::rollback(); } -
上面這種寫法lock(true)在事務外執行,查詢結束後鎖會立即釋放,無法阻止其它事務修改記錄,可能導致數據不一致
總結
必須將鎖操作(lock(true))寫在事務內部,才能保證鎖的有效性和併發安全。正確流程是:
開啓事務 → 鎖定記錄 → 執行操作 → 提交/回滾事務。
這種方式可以有效避免併發場景下的資源競爭,確保數據一致性。