自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

服了!DELETE 同一行記錄也會造成死鎖!!

數(shù)據(jù)庫 MySQL
本文將基于 MySQL 5.7.21 版本進(jìn)行討論,該版本使用 InnoDB 存儲引擎,并采用 Repeated Read 作為事務(wù)隔離級別。

1.問題背景

“哥們,又雙叒叕寫了個死鎖,秀??!??”

圖片圖片

就算是經(jīng)常寫死鎖的同學(xué)看到估計(jì)都會有點(diǎn)懵,兩條一模一樣的 DELETE 語句怎么會產(chǎn)生死鎖呢?

2.MySQL 鎖回顧

看到這里的靚仔肯定對 MySQL 的鎖非常了解,哥們還是帶大家對鎖的分類進(jìn)行快速回顧;

本文將基于 MySQL 5.7.21 版本進(jìn)行討論,該版本使用 InnoDB 存儲引擎,并采用 Repeated Read 作為事務(wù)隔離級別。

圖片圖片

要查看 MySQL 的加鎖信息,必須啟用 InnoDB 狀態(tài)監(jiān)控功能;

SET GLOBAL innodb_status_output=ON;
SET GLOBAL innodb_status_output_locks=ON;

要獲取 InnoDB 存儲引擎的詳細(xì)狀態(tài)信息,可以使用以下 SQL 命令;

SHOW ENGINE INNODB STATUS;

3.DELETE 流程

在深入分析問題原因之前先對 DELETE 操作的基本流程進(jìn)行復(fù)習(xí)。眾所周知,MySQL 以頁作為數(shù)據(jù)的基本存儲單位,每個頁內(nèi)包含兩個主要的鏈表:正常記錄鏈表和垃圾鏈表。每條記錄都有一個記錄頭,記錄頭中包括一個關(guān)鍵屬性——deleted_flag。

圖片圖片

執(zhí)行 DELETE 操作期間,系統(tǒng)首先將正常記錄的記錄頭中的 delete_flag 標(biāo)記設(shè)置為 1。這一步驟也被稱為 delete mark,是數(shù)據(jù)刪除流程的一部分。

圖片圖片

在事務(wù)成功提交之后,由 purge 線程 負(fù)責(zé)對已標(biāo)記為刪除的數(shù)據(jù)執(zhí)行邏輯刪除操作。這一過程包括將記錄從正常記錄鏈表中移除,并將它們添加到垃圾鏈表中,以便后續(xù)的清理工作。

圖片圖片

針對不同狀態(tài)下的記錄,MySQL 在加鎖時(shí)采取不同的策略,特別是在處理唯一索引上記錄的加鎖情況。以下是具體的加鎖規(guī)則:

  • 正常記錄: 對于未被標(biāo)記為刪除的記錄,MySQL 會施加記錄鎖,以確保事務(wù)的隔離性和數(shù)據(jù)的一致性。
  • delete mark: 當(dāng)記錄已被標(biāo)記為刪除(即 delete_flag 被設(shè)置為1),但尚未由 purge 線程清理時(shí),MySQL 會對這些記錄施加臨鍵鎖,以避免在清理前發(fā)生數(shù)據(jù)沖突。
  • 已刪除記錄: 對于已經(jīng)被 purge 線程邏輯刪除的記錄,MySQL 會施加間隙鎖,這允許在已刪除記錄的索引位置插入新記錄,同時(shí)保持索引的完整性和順序性。

4.原因剖析

在分析死鎖的案例中,我們關(guān)注的表 t_order_extra_item_15 具有一個由 (order_id, extra_key) 組成的聯(lián)合唯一索引。為了更好地理解死鎖的產(chǎn)生機(jī)制,我們將對上述死鎖日志進(jìn)行簡化處理。


事務(wù)137060372(A)

事務(wù)137060371(B)

執(zhí)行語句

delete from t_order_extra_item_15 WHERE (order_id = xxx and extra_key = xxx)

delete from t_order_extra_item_15 WHERE (order_id = xxx and extra_key = xxx)

持有鎖


lock_mode X locks rec but not gap(記錄鎖)

等待鎖

lock_mode X locks rec but not gap waiting(記錄鎖)

lock_mode X waiting(臨鍵鎖)

事務(wù) A 試圖獲取記錄鎖,但被事務(wù) B 持有的相同的記錄鎖所阻塞。而且,事務(wù) B 在嘗試獲取臨鍵鎖時(shí)也遇到了阻塞,這是因?yàn)槭聞?wù) A 先前已經(jīng)請求了記錄鎖,從而形成了一種相互等待的狀態(tài),這種情況最終導(dǎo)致了死鎖的發(fā)生。

然而事務(wù) B 為何在已經(jīng)持有記錄鎖的情況下還需要等待臨鍵鎖?唯一合理的解釋是,在事務(wù) B 最初執(zhí)行 DELETE 操作時(shí),它所嘗試操作的記錄已經(jīng)被其他事務(wù)鎖定。當(dāng)這個其他事務(wù)完成了 delete mark 并提交后,事務(wù) B 不得不重新發(fā)起對臨鍵鎖的請求。

經(jīng)過深入分析得出結(jié)論,在并發(fā)環(huán)境中,必然存在另一個執(zhí)行相同 DELETE 操作的事務(wù),我們稱之為事務(wù) C。

通過仔細(xì)分析業(yè)務(wù)代碼和服務(wù)日志,我們迅速驗(yàn)證了這一假設(shè)。現(xiàn)在,導(dǎo)致死鎖的具體原因已經(jīng)非常明顯。為了幫助大家更好地理解三個事務(wù)的執(zhí)行順序,我們制定了一個事務(wù)執(zhí)行時(shí)序的設(shè)想表格。

事務(wù) A

事務(wù) B

事務(wù) C



1. delete from t_order_extra_item_15 WHERE (order_id = xxx and extra_key = xxx ) )

獲取記錄鎖成功(lock_mode X locks rec but not gap)


2. delete from t_order_extra_item_15 WHERE (order_id = xxx and extra_key = xxx ) )

等待獲取記錄鎖( lock_mode X locks rec but not gap waiting)


3. delete from t_order_extra_item_15 WHERE (order_id = xxx and extra_key = xxx ) )

等待獲取記錄鎖( lock_mode X locks rec but not gap waiting)





4. delete mark 設(shè)置記錄頭刪除標(biāo)識位

delete_flag=1



5. 事務(wù)提交


6. 獲取記錄鎖成功

記錄狀態(tài)變更重新獲取臨鍵鎖(lock_mode X)


7. 發(fā)現(xiàn)死鎖,回滾該事務(wù)

WE ROLL BACK TRANSACTION




8. 事務(wù)提交


在執(zhí)行流程的第 6 步中,事務(wù) B 嘗試重新獲取臨鍵鎖,這時(shí)與事務(wù) A 發(fā)生了相互等待的狀況,導(dǎo)致死鎖的發(fā)生。為解決這一問題,數(shù)據(jù)庫管理系統(tǒng)自動回滾了事務(wù) A,以打破死鎖狀態(tài)。

5.現(xiàn)場還原

哥們深知道理論分析至關(guān)重要,實(shí)踐才是檢驗(yàn)真理的唯一標(biāo)準(zhǔn)。Talk is cheap, Show me the code. 在相同的系統(tǒng)環(huán)境下,我們創(chuàng)建了一個測試表來模擬實(shí)際情況;

CREATE TABLE `t_lock` (
  `id` int NOT NULL,
  `uniq` int NOT NULL,
  `idx` int NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq` (`uniq`) USING BTREE,
  KEY `idx` (`idx`)
);

INSERT INTO t_lock VALUES (1, 1, 1);
INSERT INTO t_lock VALUES (5, 5, 5);
INSERT INTO t_lock VALUES (10, 10, 10);

大聰明一上來便直接手動開啟 3 個 MySQL 命令列界面,每個界面中獨(dú)立開啟事務(wù)執(zhí)行 DELETE FROM t_lock where uniq = 5; 語句,然而實(shí)驗(yàn)結(jié)果并未能成功復(fù)現(xiàn)先前討論的死鎖狀況。

經(jīng)過反復(fù) SHOW ENGINE INNODB STATUS; 檢查鎖的狀態(tài)得出結(jié)論:在 DELETE 操作中,加鎖和 delete mark 是連續(xù)的不可分割的步驟,不受人為干預(yù)。一旦一個事務(wù)開始執(zhí)行 DELETE,其他事務(wù)對該記錄的訪問請求將自動轉(zhuǎn)為臨鍵鎖,避免了死鎖的發(fā)生。

為了更準(zhǔn)確地模擬并發(fā)環(huán)境下 DELETE 操作可能導(dǎo)致的死鎖,這里采用 Java 語言編寫了一個示例程序;

public class Main {

    private static final String URL = "jdbc:mysql://localhost:3306/db_test";
    private static final String USER = "root";
    private static final String PASSWORD = "123456";
    private static final String SQL = "DELETE FROM t_lock WHERE uniq = 5;";

    public static void main(String[] args) {
        // 開啟 3 個線程,模擬并發(fā)刪除
        for (int i = 0; i < 3; i++) {
            new Thread(Main::executeSQL).start();
        }
    }

    public static void executeSQL() {
        try (
                Connection connection = DriverManager.getConnection(URL, USER, PASSWORD);
                Statement statement = connection.createStatement()
        ) {
            System.out.println(LocalTime.now() + ":" + Thread.currentThread().getName());
            // 關(guān)閉自動提交
            connection.setAutoCommit(false);
            int rows = statement.executeUpdate(SQL);
            // 延時(shí) 5 秒便于觀察加鎖信息
            Thread.sleep(5000);
            connection.commit();
            System.out.println(LocalTime.now() + ":" + Thread.currentThread().getName() + ":" + rows);
        } catch (Exception e) {
            // 死鎖堆棧輸出
            e.printStackTrace();
        }
    }
}

果不其然,程序執(zhí)行異常,異常堆棧中清晰地記錄了死鎖信息。進(jìn)一步檢查 MySQL 服務(wù)端的死鎖日志,與線上業(yè)務(wù)的死鎖日志如出一轍。程序執(zhí)行過程中三個并發(fā)事務(wù)的加鎖信息,和文章第四段的原因分析完全一致。這證實(shí)了我們的現(xiàn)場模擬成功復(fù)現(xiàn)了死鎖情況。

6 問題思考

6.1 可以通過 SELECT FOR UPDATE 避免嗎

不行。SELECT FOR UPDATE 的加鎖邏輯與 DELETE 語句的加鎖邏輯是一致的。加鎖的類型完全取決于被加鎖記錄的狀態(tài)。由于這一機(jī)制,使用 SELECT FOR UPDATE 并不能解決由 DELETE 操作引起的死鎖問題。

6.2 只有唯一索引會有這個問題嗎

的確,只有唯一索引會引發(fā)此類死鎖問題,主鍵索引和普通索引均不會。在上述的系統(tǒng)環(huán)境下的實(shí)驗(yàn)結(jié)果表明,不同索引類型在索引等值加 X 鎖情況下的行為如下:


主鍵索引

唯一索引

普通索引

正常記錄

記錄鎖

記錄鎖

臨鍵鎖

delete mark

記錄鎖

臨鍵鎖

臨鍵鎖

已刪除記錄

間隙鎖

間隙鎖

間隙鎖

唯一索引在處理"正常記錄"時(shí)施加的是記錄鎖,但在處理處于"delete mark"狀態(tài)的記錄時(shí),它施加的是臨鍵鎖。這種加鎖類型的不一致性,在執(zhí)行并發(fā)的 DELETE 操作時(shí),增加了導(dǎo)致死鎖的風(fēng)險(xiǎn)。

6.3 持有記錄鎖后再請求臨鍵鎖為什么需要等待

因?yàn)樵谕恍杏涗浬线^去已經(jīng)有事務(wù)在等待獲取鎖了,為了避免鎖饑餓現(xiàn)象的發(fā)生,先前請求加鎖的事務(wù)在鎖釋放后將獲得優(yōu)先權(quán)??谡f無憑,大聰明直接開啟 2 個 MySQL 命令列界面,分別執(zhí)行 DELETE FROM t_lock where uniq = 5; 語句,實(shí)際操作結(jié)果如下;

事務(wù) A

事務(wù) B

1. delete from t_lock WHERE uniq = 5;

獲取記錄鎖成功(lock_mode X locks rec but not gap)


2. delete mark 設(shè)置記錄頭刪除標(biāo)識位

delete_flag=1



3. delete from t_lock WHERE uniq = 5;

等待獲取臨鍵鎖( lock_mode X waiting)

4. delete from t_lock WHERE uniq = 5;

獲取臨鍵鎖成功(lock_mode X)



5. 發(fā)現(xiàn)死鎖,回滾該事務(wù)

WE ROLL BACK TRANSACTION

6. 事務(wù)提交


在操作流程的第四步中,事務(wù) A 嘗試請求對 uniq = 5 的臨鍵鎖,發(fā)現(xiàn)事務(wù) B 已經(jīng)先行一步請求了同一行記錄上的臨鍵鎖。然而,事務(wù) B 的這一請求由于事務(wù) A 持有的記錄鎖而被阻塞,從而相互等待造成了死鎖現(xiàn)象。

6.4 高版本的 MySQL 會存在 DELETE 死鎖嗎

在 MySQL 環(huán)境 8.x 版本環(huán)境中,DELETE 操作引發(fā)的死鎖情況得到了改進(jìn)。通過觀察加鎖日志發(fā)現(xiàn),事務(wù)在對于 delete mark 的記錄加鎖時(shí),如果已經(jīng)持有了該記錄的記錄鎖,他將獲取間隙鎖而不是臨鍵鎖,這一變化有效避免了死鎖的發(fā)生。

具體的加鎖信息在此略去,大伙們?nèi)舾信d趣可以親自進(jìn)行驗(yàn)證。??

7 事后總結(jié)

問題的來龍去脈都已梳理清晰,解決方案可歸納為以下幾種:

  1. 升級 MySQL 版本: ?? 升級到最新版本可能會帶來人力成本和系統(tǒng)風(fēng)險(xiǎn);
  2. 更改隔離級別 RC: ?? 可以解決死鎖問題,但會引入臟讀和幻讀現(xiàn)象;
  3. 放任不管: ?? 不影響數(shù)據(jù)一致性,會導(dǎo)致服務(wù)和數(shù)據(jù)庫出現(xiàn)異常;
  4. 引入分布式鎖: ?? 開發(fā)成本相對較小,且影響范圍可控,已被采納;

平日朗誦八股文時(shí)如濤濤江水連綿不絕,可實(shí)際業(yè)務(wù)場景總會遇到各種奇葩的問題。因此,我們應(yīng)該始終對技術(shù)保持一顆敬畏之心,追求不斷學(xué)習(xí)和成長。

作者:曹建濤,轉(zhuǎn)轉(zhuǎn)C2C&寄賣業(yè)務(wù)研發(fā)工程師

參考資料

[1]InnoDB Locking: https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html

[2]An InnoDB Deadlock Example: https://dev.mysql.com/doc/refman/5.7/en/innodb-deadlock-example.html

責(zé)任編輯:武曉燕 來源: 轉(zhuǎn)轉(zhuǎn)技術(shù)
相關(guān)推薦

2024-08-01 09:57:17

DELETE死鎖工具

2020-04-01 08:51:47

注釋字符代碼

2020-08-24 08:25:48

Python開發(fā)工具

2020-07-15 09:40:37

代碼Python瀏覽記錄

2021-07-26 10:32:54

MySQL數(shù)據(jù)庫存儲

2022-11-30 17:13:05

MySQLDynamic存儲

2016-12-02 08:53:18

Python一行代碼

2020-08-05 11:53:41

數(shù)據(jù)代碼自動化

2014-02-12 13:43:50

代碼并行任務(wù)

2022-04-09 09:11:33

Python

2017-04-05 11:10:23

Javascript代碼前端

2025-04-01 01:25:00

MySQLInnoDBMyISAM

2024-06-12 08:21:07

Deadlock死鎖版本

2020-02-28 15:27:31

代碼開發(fā)工具

2018-01-02 09:26:28

Ubuntuzzupdate版本升級

2021-11-02 16:25:41

Python代碼技巧

2020-08-19 10:30:25

代碼Python多線程

2020-09-09 16:00:22

Linux進(jìn)程

2021-10-19 17:52:16

Git命令算數(shù)

2021-08-31 09:49:37

CPU執(zhí)行語言
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號