Mysql
SELECT … FOR UPDATE 等待鎖定後返回舊值
似乎如果
SELECT ... FOR UPDATE
必須等待鎖定,同時另一個執行緒更改結果並送出,則初始查詢返回舊結果,即更改之前的結果。這是預期的行為還是錯誤?它當然似乎沒有用。
這很容易展示。我們來看下表:
CREATE TABLE `orders`( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `description` VARCHAR(50), PRIMARY KEY (`id`) ) ENGINE=INNODB;
而這一系列的陳述:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN; SELECT MAX(id) FROM orders FOR UPDATE; # PAUSE HERE INSERT INTO orders (description) VALUES ('bla'); COMMIT;
現在我們執行以下操作:
- 打開兩個連接。
- 在連接 1 上,執行到暫停。這成功了。
- 在連接 2 上,執行到暫停。這會卡住等待鎖定。
- 在連接 1 上,執行其餘部分。這成功了。
- 連接 2 現在返回了以前的id!
- 在連接 2 上,
SELECT
再次執行。現在它返回更新的值。因此,為了獲得看似明智和可取的行為,我們似乎被迫執行
SELECT
兩次。這是預期的行為還是錯誤?
(在 MySQL 5.7.25 上測試)
通過做一些實驗,我已經弄清楚了這種行為的解釋。我使用連接 1 來執行一些其他更改,而不是插入新行。綜合結果揭示了該過程的工作原理。
- 如果連接 1 更新了
description
,id
連接 2 將看到更新的description
。(索引掃描在鎖定行之前停止。)- 如果連接 1 將 更新
id
為較小的 值,例如從 10 到 9,則連接 2 將返回新值 9。(索引掃描在鎖定行之前停止,看到它消失,並以降序方式繼續,從而到達9點。)- 如果連接 1 將 更新
id
為更大的值,例如從 10 到 11,則連接 2 將跳過該值,而是返回id
小於其原始值的最大值。id
在此範例中,如果存在,則返回9。(索引掃描在鎖定行之前停止,看到它消失,並以降序方式繼續,因此永遠不會超過 11,而是到達 9。)- 如果連接 1 將 更新
id
為更大的值,例如從 10 到 11,然後插入新行代替舊行,例如使用id
10,連接 2 將選擇新插入的行。(索引掃描在鎖定行之前停止,並且會看到在其位置插入了一些東西。)- TODO:如果連接 1 將 更新
id
為更大的值,例如從 10 到 20,然後插入新行代替舊行,但值大於原始值,例如 11,連接 2 會看到嗎?很可能不是,但這應該進行測試。- 如果連接 1 鎖定了
MAX(id)
10 的 a,而連接 2 選擇了硬編碼id
的 9,則鎖定不會重疊,兩者都可以獨立進行。我們可以得出結論,這
MAX(id)
是使用反向掃描確定的,並且鎖位於實際(索引)行上。這是有道理的。我們的隔離級別READ COMMITTED
不獲取間隙鎖。REPEATABLE READ
可能,但它會導致連接 2 的事務完全失敗,因此沒有更深入地探索該場景。結束確切的工作
連接 2 開始反向掃描,直到它鎖定某一行。
它將等待該鎖被釋放,並繼續從(並包括)
id
它在. 不是什麼行,而是什麼*id
*。
- 結果,如果 那裡不再有一行
id
,則掃描繼續超出它,直到更小id
的 s。- 因此,如果行已更新,則更新的值是可見的(如果選擇的不僅僅是id)。
- 結果,如果該行已被移動並為其id插入了一個新行,則選擇該新行。