MySQL資料更新被另一個交易卡住:查鎖等待與短交易

後台按下保存,一直轉圈;資料庫的 UPDATE 沒有立刻回報錯誤,也沒有完成。先確認它是否等待另一個交易持有的鎖。若同一列被尚未結束的交易更新,後來的更新可能需要等到對方提交或撤回。

增加等待時間不一定能解決原因。真正要核對的是誰在等、誰持有鎖,以及持鎖交易為什麼遲遲沒有結束。這篇用獨立 MySQL 8.4.9 的 InnoDB 合成表,建立兩個真實連線觀察同一列的等待。

先區分鎖等待與一般慢查詢

執行很久可能來自資料掃描、磁碟、網路,也可能是等待鎖。只看「花了幾秒」不能辨認原因。先由有權限的維護者讀取等待資訊,保留正在執行的語句與相關交易,再決定如何處理。

MySQL 8.4 的 performance_schema.data_lock_waits 記錄資料鎖的等待關係。搭配 data_locks,可以查看對應表、索引和鎖狀態;再連結 threads,找到等待與阻擋連線的 ID。讀取權限依帳號設定,不要為了查詢就給應用程式過大的管理權限。

兩個連線更新同一個主鍵

合成表 lock_items 只有一列,id=1、amount=10,使用 InnoDB 與主鍵索引。連線 A 開始交易,將數值加 1,但暫不提交:

-- 連線 A;在自己的合成測試表操作
START TRANSACTION;
UPDATE lock_items SET amount = amount + 1 WHERE id = 1;
SELECT CONNECTION_ID();
-- 暫不提交,接著在另一個連線啟動 B

連線 B 使用另一個真正的資料庫連線,嘗試將同一列再加 10。本例只為 B 設定十秒等待上限,沒有修改全域設定:

-- 連線 B
SET SESSION innodb_lock_wait_timeout = 10;
START TRANSACTION;
UPDATE lock_items SET amount = amount + 10 WHERE id = 1;
-- 這裡先等待 A 結束,後續語句尚未執行
SELECT ROW_COUNT() AS updated_rows;
COMMIT;

此時 B 的 UPDATE 尚未完成。A 已執行更新,但它的交易還開著;「語句執行完」與「交易提交了」是兩個不同時點。應用程式若先更新資料,再等外部 API、寄信或人工操作,就可能把持鎖時間一起拉長。

讀出等待關係,別猜阻擋者

在第三個觀察連線讀取以下資料,將示範的資料庫及表名換成已核准查看的目標:

SELECT r.OBJECT_SCHEMA, r.OBJECT_NAME, r.INDEX_NAME,
       r.LOCK_TYPE, r.LOCK_MODE, r.LOCK_STATUS, r.LOCK_DATA,
       rt.PROCESSLIST_ID AS waiting_connection,
       bt.PROCESSLIST_ID AS blocking_connection
FROM performance_schema.data_lock_waits AS w
JOIN performance_schema.data_locks AS r
  ON r.ENGINE = w.ENGINE
 AND r.ENGINE_LOCK_ID = w.REQUESTING_ENGINE_LOCK_ID
LEFT JOIN performance_schema.threads AS rt
  ON rt.THREAD_ID = w.REQUESTING_THREAD_ID
LEFT JOIN performance_schema.threads AS bt
  ON bt.THREAD_ID = w.BLOCKING_THREAD_ID
WHERE r.OBJECT_SCHEMA = 'wn_support3'
  AND r.OBJECT_NAME = 'lock_items';

本例實際得到 PRIMARY 索引、RECORD 鎖、X,REC_NOT_GAP 模式,狀態是 WAITING,LOCK_DATA 為 1。等待連線是 23,阻擋連線是 22,與剛才 A、B 的 ID 對上。這些 ID 只屬於這次演示;重跑時應讀當下結果,不能照抄數字處理正式連線。

觀察時要保留時間點。等待關係隨交易結束而消失,稍後查不到不代表從來沒有發生。反過來,查到一筆等待,也不能只憑表名就斷定該終止哪個程序,仍需核對業務與交易內容。

A 提交後,B 才繼續

在連線 A 執行 COMMIT 後,B 的 UPDATE 完成,受影響列數是 1,接著 B 提交。重新讀取 amount 得到 21:先由 A 加 1,再由 B 加 10。這份案例隨後再查,已沒有該表的等待關係。

-- 回到連線 A
COMMIT;

-- B 完成並提交後,重新查詢
SELECT amount FROM lock_items WHERE id = 1;
-- 本例:21

這個結果說明持鎖交易的結束可以讓等待者繼續,不表示所有卡住的查詢都只需提交某個連線。正式環境應由交易擁有者確認工作已完成後結束交易;不要為了清空等待清單,替不明交易任意提交。

逾時不是整個交易自動撤回的承諾

另一分支讓 A 再次持有同一列,B 的 session 等待上限改為 1 秒。B 的更新回報 ERROR 1205,表示這次鎖等待逾時。A 隨後明確撤回,最後 amount 仍是先前已提交的 21。

本例的 innodb_rollback_on_timeout 為 0。一般鎖等待逾時只撤回等待失敗的語句,不能據此認為交易前面的所有改動都已撤回。應用程式收到錯誤後,要按既有交易流程明確處理撤回與連線狀態,再決定是否重新執行整個適當的工作單位。

演示使用的 B 命令列程序遇錯退出,連線關閉;正式連線池可能繼續保留連線,所以不能把命令列退出的結果直接當成應用程式的處理方式。不要吞掉逾時錯誤,然後仍向訪客顯示保存成功。

鎖等待逾時與死鎖也不同。這裡只有 B 等 A,沒有互相等待形成循環;若遇死鎖,要依其錯誤與交易重試策略處理,不能把 1205 的演示稱為死鎖測試。

網頁逾時後,先查原請求的結果

訪客看到轉圈或網頁逾時,不表示資料庫更新已撤回。HTTP 請求、應用程式和資料庫各自有等待與錯誤處理;有時前端先停止等待,後面的工作卻仍在執行。維護者應核對原請求的記錄與交易結果,再判斷能否重試。

若只是把「保存」按鈕重新啟用,讓訪客連按幾次,可能同時產生更多等待。前端應清楚表示正在處理,失敗時保留輸入並給出符合實際結果的提示。是否重試、如何避免重複寫入,仍由應用程式的提交規則處理,不能交給鎖等待上限代替。

交給維護者的資料至少包括發生時間、請求識別、相關表與鍵、等待和阻擋連線,以及應用程式得到的錯誤。公開畫面或工單不要附完整憑證與客戶內容;合成案例只需要那一列的主鍵與數值,就能重現等待關係。

處理後重試正常保存與受控等待兩條路徑:正常保存要完成,等待失敗也要可靠地結束交易。只確認等待清單變空,還不足以知道訪客是否保存成功;應再讀取預期資料,並核對回應是否一致。

把交易縮短,但保留必要的一致性

先查持鎖交易是否漏了提交、錯誤分支沒有撤回,或把不需要持鎖的長工作放在交易裡。需要的資料變更仍應保持正確的原子性;不能為了減少等待,就把應一起完成的更新拆成互相矛盾的結果。

查詢條件與索引也會影響鎖定範圍。這份示範以主鍵精確定位一列,不代表正式範圍更新只會鎖一列。把實際 WHERE、執行計畫和等待資訊一起交給維護者,才能判斷是否需要調整查詢或流程。

先在合成資料重現 A 持鎖、B 等待,以及 A 結束後 B 的結果,再核對錯誤分支是否撤回。不要直接加大全域等待值或終止未知連線;讓交易在完成後可靠地結束,才是這類等待首先要處理的原因。

參考資料

原文鏈接:https://wntheme.com/mysql-lock-wait-short-transaction/,轉載請註明出處。
0

評論0

顯示驗證碼
沒有帳號?註冊  忘記密碼?