MySQL 死鎖了,怎么辦?
之前分享過(guò)?? MySQL 死鎖??的文章,然后很多讀者對(duì)「插入意向鎖」認(rèn)識(shí)很迷糊。
大家誤以為「插入意向鎖」是意向鎖,也就是表鎖,確實(shí)這個(gè)名字很讓人誤解。
但是,實(shí)際上「插入意向鎖」不是意向鎖,而是特殊的間隙鎖,屬于行級(jí)鎖,注意是「特殊」的間隙鎖,并不是我們常說(shuō)的間隙鎖。
所以,我在原來(lái)文章的基礎(chǔ)上補(bǔ)充了兩個(gè)知識(shí)點(diǎn):
- 什么是插入意向鎖?
- insert 語(yǔ)句是怎么加鎖的?
提綱如下:
有個(gè)業(yè)務(wù)主要邏輯就是新增訂單、修改訂單、查詢訂單等操作。然后因?yàn)橛唵问遣荒苤貜?fù)的,所以當(dāng)時(shí)在新增訂單的時(shí)候做了冪等性校驗(yàn),做法就是在新增訂單記錄之前,先通過(guò) select ... for update 語(yǔ)句查詢訂單是否存在,如果不存在才插入訂單記錄。
而正是因?yàn)檫@樣的操作,當(dāng)業(yè)務(wù)量很大的時(shí)候,就可能會(huì)出現(xiàn)死鎖。
接下來(lái)跟大家聊下為什么會(huì)發(fā)生死鎖,以及怎么避免死鎖。
死鎖的發(fā)生
本次案例使用存儲(chǔ)引擎 Innodb,隔離級(jí)別為可重復(fù)讀(RR)。
接下來(lái),我用實(shí)戰(zhàn)的方式來(lái)帶大家看看死鎖是怎么發(fā)生的。
我建了一張訂單表,其中 id 字段為主鍵索引,order_no 字段普通索引,也就是非唯一索引:
CREATE TABLE `t_order` (
`id` int NOT NULL AUTO_INCREMENT,
`order_no` int DEFAULT NULL,
`create_date` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_order` (`order_no`) USING BTREE
) ENGINE=InnoDB ;
然后,先 t_order 表里現(xiàn)在已經(jīng)有了 6 條記錄:
假設(shè)這時(shí)有兩事務(wù),一個(gè)事務(wù)要插入訂單 1007 ,另外一個(gè)事務(wù)要插入訂單 1008,因?yàn)樾枰獙?duì)訂單做冪等性校驗(yàn),所以兩個(gè)事務(wù)先要查詢?cè)撚唵问欠翊嬖冢淮嬖诓挪迦胗涗?,過(guò)程如下:
可以看到,兩個(gè)事務(wù)都陷入了等待狀態(tài)(前提沒(méi)有打開(kāi)死鎖檢測(cè)),也就是發(fā)生了死鎖,因?yàn)槎荚谙嗷サ却龑?duì)方釋放鎖。
這里在查詢記錄是否存在的時(shí)候,使用了 select ... for update 語(yǔ)句,目的為了防止事務(wù)執(zhí)行的過(guò)程中,有其他事務(wù)插入了記錄,而出現(xiàn)幻讀的問(wèn)題。
如果沒(méi)有使用 select ... for update 語(yǔ)句,而使用了單純的 select 語(yǔ)句,如果是兩個(gè)訂單號(hào)一樣的請(qǐng)求同時(shí)進(jìn)來(lái),就會(huì)出現(xiàn)兩個(gè)重復(fù)的訂單,有可能出現(xiàn)幻讀,如下圖:
為什么會(huì)產(chǎn)生死鎖?
可重復(fù)讀隔離級(jí)別下,是存在幻讀的問(wèn)題。
Innodb 引擎為了解決「可重復(fù)讀」隔離級(jí)別下的幻讀問(wèn)題,就引出了 next-key 鎖,它是記錄鎖和間隙鎖的組合。
- Record Loc,記錄鎖,鎖的是記錄本身;
- Gap Lock,間隙鎖,鎖的就是兩個(gè)值之間的空隙,以防止其他事務(wù)在這個(gè)空隙間插入新的數(shù)據(jù),從而避免幻讀現(xiàn)象。
普通的 select 語(yǔ)句是不會(huì)對(duì)記錄加鎖的,因?yàn)樗峭ㄟ^(guò) MVCC 的機(jī)制實(shí)現(xiàn)的快照讀,如果要在查詢時(shí)對(duì)記錄加行鎖,可以使用下面這兩個(gè)方式:
begin;
//對(duì)讀取的記錄加共享鎖
select ... lock in share mode;
commit; //鎖釋放
begin;
//對(duì)讀取的記錄加排他鎖
select ... for update;
commit; //鎖釋放
行鎖的釋放時(shí)機(jī)是在事務(wù)提交(commit)后,鎖就會(huì)被釋放,并不是一條語(yǔ)句執(zhí)行完就釋放行鎖。
比如,下面事務(wù) A 查詢語(yǔ)句會(huì)鎖住(2, +∞]范圍的記錄,然后期間如果有其他事務(wù)在這個(gè)鎖住的范圍插入數(shù)據(jù)就會(huì)被阻塞。
next-key 鎖的加鎖規(guī)則其實(shí)挺復(fù)雜的,在一些場(chǎng)景下會(huì)退化成記錄鎖或間隙鎖。
需要注意的是,如果 update 語(yǔ)句的 where 條件沒(méi)有用到索引列,那么就會(huì)全表掃描,在一行行掃描的過(guò)程中,不僅給行加上了行鎖,還給行兩邊的空隙也加上了間隙鎖,相當(dāng)于鎖住整個(gè)表,然后直到事務(wù)結(jié)束才會(huì)釋放鎖。
所以在線上千萬(wàn)不要執(zhí)行沒(méi)有帶索引條件的 update 語(yǔ)句,不然會(huì)造成業(yè)務(wù)停滯,我有個(gè)讀者就因?yàn)楦闪诉@個(gè)事情,然后被老板教育了一波。
回到前面死鎖的例子,在執(zhí)行下面這條語(yǔ)句的時(shí)候:
select id from t_order where order_no = 1008 for update;
因?yàn)?order_no 不是唯一索引,所以行鎖的類(lèi)型是間隙鎖,于是間隙鎖的范圍是(1006, +∞)。那么,當(dāng)事務(wù) B 往間隙鎖里插入 id = 1008 的記錄就會(huì)被鎖住。
因?yàn)楫?dāng)我們執(zhí)行以下插入語(yǔ)句時(shí),會(huì)在插入間隙上再次獲取插入意向鎖。
Insert into t_order (order_no, create_date) values (1008, now());
插入意向鎖與間隙鎖是沖突的,所以當(dāng)其它事務(wù)持有該間隙的間隙鎖時(shí),需要等待其它事務(wù)釋放間隙鎖之后,才能獲取到插入意向鎖。而間隙鎖與間隙鎖之間是兼容的,所以所以兩個(gè)事務(wù)中 select ... for update 語(yǔ)句并不會(huì)相互影響。
案例中的事務(wù) A 和事務(wù) B 在執(zhí)行完后 select ... for update 語(yǔ)句后都持有范圍為(1006,+∞)的間隙鎖,而接下來(lái)的插入操作為了獲取到插入意向鎖,都在等待對(duì)方事務(wù)的間隙鎖釋放,于是就造成了循環(huán)等待,導(dǎo)致死鎖。
為什么間隙鎖與間隙鎖之間是兼容的?
在MySQL官網(wǎng)上還有一段非常關(guān)鍵的描述:
Gap locks in InnoDB are “purely inhibitive”, which means that their only purpose is to prevent other transactions from Inserting to the gap. Gap locks can co-exist. A gap lock taken by one transaction does not prevent another transaction from taking a gap lock on the same gap. There is no difference between shared and exclusive gap locks. They do not conflict with each other, and they perform the same function.
這段話表明間隙鎖在本質(zhì)上是不區(qū)分共享間隙鎖或互斥間隙鎖的,而且間隙鎖是不互斥的,即兩個(gè)事務(wù)可以同時(shí)持有包含共同間隙的間隙鎖。
這里的共同間隙包括兩種場(chǎng)景:
- 其一是兩個(gè)間隙鎖的間隙區(qū)間完全一樣;
- 其二是一個(gè)間隙鎖包含的間隙區(qū)間是另一個(gè)間隙鎖包含間隙區(qū)間的子集。
間隙鎖本質(zhì)上是用于阻止其他事務(wù)在該間隙內(nèi)插入新記錄,而自身事務(wù)是允許在該間隙內(nèi)插入數(shù)據(jù)的。也就是說(shuō)間隙鎖的應(yīng)用場(chǎng)景包括并發(fā)讀取、并發(fā)更新、并發(fā)刪除和并發(fā)插入。
插入意向鎖是什么?
注意!插入意向鎖名字雖然有意向鎖,但是它并不是意向鎖,它是一種特殊的間隙鎖。
在MySQL的官方文檔中有以下重要描述:
An Insert intention lock is a type of gap lock set by Insert operations prior to row Insertion. This lock signals the intent to Insert in such a way that multiple transactions Inserting into the same index gap need not wait for each other if they are not Inserting at the same position within the gap. Suppose that there are index records with values of 4 and 7. Separate transactions that attempt to Insert values of 5 and 6, respectively, each lock the gap between 4 and 7 with Insert intention locks prior to obtaining the exclusive lock on the Inserted row, but do not block each other because the rows are nonconflicting.
這段話表明盡管插入意向鎖是一種特殊的間隙鎖,但不同于間隙鎖的是,該鎖只用于并發(fā)插入操作。
如果說(shuō)間隙鎖鎖住的是一個(gè)區(qū)間,那么「插入意向鎖」鎖住的就是一個(gè)點(diǎn)。因而從這個(gè)角度來(lái)說(shuō),插入意向鎖確實(shí)是一種特殊的間隙鎖。
插入意向鎖與間隙鎖的另一個(gè)非常重要的差別是:盡管「插入意向鎖」也屬于間隙鎖,但兩個(gè)事務(wù)卻不能在同一時(shí)間內(nèi),一個(gè)擁有間隙鎖,另一個(gè)擁有該間隙區(qū)間內(nèi)的插入意向鎖(當(dāng)然,插入意向鎖如果不在間隙鎖區(qū)間內(nèi)則是可以的)。
另外,我補(bǔ)充一點(diǎn),插入意向鎖的生成時(shí)機(jī):
每插入一條新記錄,都需要看一下待插入記錄的下一條記錄上是否已經(jīng)被加了間隙鎖,如果已加間隙鎖,那 Insert 語(yǔ)句應(yīng)該被阻塞,并生成一個(gè)插入意向鎖 。
Insert 語(yǔ)句是怎么加行級(jí)鎖的?
Insert 語(yǔ)句在正常執(zhí)行時(shí)是不會(huì)生成鎖結(jié)構(gòu)的,它是靠聚簇索引記錄自帶的 trx_id 隱藏列來(lái)作為隱式鎖來(lái)保護(hù)記錄的。
什么是隱式鎖?
當(dāng)事務(wù)需要加鎖的時(shí),如果這個(gè)鎖不可能發(fā)生沖突,InnoDB會(huì)跳過(guò)加鎖環(huán)節(jié),這種機(jī)制稱為隱式鎖。隱式鎖是 InnoDB 實(shí)現(xiàn)的一種延遲加鎖機(jī)制,其特點(diǎn)是只有在可能發(fā)生沖突時(shí)才加鎖,從而減少了鎖的數(shù)量,提高了系統(tǒng)整體性能。
隱式鎖就是在 Insert 過(guò)程中不加鎖,只有在特殊情況下,才會(huì)將隱式鎖轉(zhuǎn)換為顯示鎖,這里我們列舉兩個(gè)場(chǎng)景。
如果記錄之間加有間隙鎖,為了避免幻讀,此時(shí)是不能插入記錄的;
如果 Insert 的記錄和已有記錄存在唯一鍵沖突,此時(shí)也不能插入記錄;
1.記錄之間加有間隙鎖
每插入一條新記錄,都需要看一下待插入記錄的下一條記錄上是否已經(jīng)被加了間隙鎖,如果已加間隙鎖,那 Insert 語(yǔ)句應(yīng)該被阻塞,并生成一個(gè)插入意向鎖。
舉個(gè)例子,現(xiàn)在 t_order 表中,只有這些數(shù)據(jù),order_no 是二級(jí)索引。
現(xiàn)在,事務(wù) A 執(zhí)行了下面這條語(yǔ)句。
# 事務(wù) A
mysql> begin;
Query OK, 0 rows affected (0.01 sec)
mysql> select * from t_order where order_no = 1006 for update;
Empty set (0.01 sec)
接著,我們執(zhí)行 select * from performance_schema.data_locks\G; 語(yǔ)句 ,確定事務(wù) A 加了什么類(lèi)型的鎖,這里只關(guān)注在記錄上加鎖的類(lèi)型。
可以看到,加的是 X 型得鎖,但是具體是記錄鎖、間隙鎖、next-key 鎖呢?注意,這里 LOCK_TYPE 中的 RECORD 表示行級(jí)鎖,而不是記錄鎖的意思。
首先通過(guò) LOCK_MODE 可以確認(rèn)是「next-key 鎖或者間隙鎖」,還是「記錄鎖」:
- 如果 LOCK_MODE 為 X,說(shuō)明是 next-key 鎖或者間隙鎖;
- 如果 LOCK_MODE 為X, REC_NOT_GAP,說(shuō)明是記錄鎖。
對(duì)于是 next-key 鎖,還是間隙鎖,就要看 LOCK_DATA 信息。
- 如果 LOCK_DATA 信息為 supremum,說(shuō)明是間隙鎖;
- 如果 LOCK_DATA 信息為具體的記錄值,說(shuō)明是 next-key;
因此,本次的例子加的是間隙鎖,間隙鎖的范圍是(1005, +∞)。
然后,有個(gè)事務(wù) B 在這個(gè)間隙鎖中,插入了一個(gè)記錄,那么此時(shí)該事務(wù) B 就會(huì)被阻塞:
# 事務(wù) B 插入一條記錄
mysql> begin;
Query OK, 0 rows affected (0.01 sec)
mysql> insert into t_order(order_no, create_date) values(1010,now());
### 阻塞狀態(tài)。。。。
接著,我們執(zhí)行 select * from performance_schema.data_locks\G; 語(yǔ)句 ,確定事務(wù) B 加了什么類(lèi)型的鎖,這里只關(guān)注在記錄上加鎖的類(lèi)型。
可以看到,事務(wù) B 的狀態(tài)為等待狀態(tài),因?yàn)橄蚴聞?wù) A 生成的間隙鎖 (1005, +∞) 中插入了一條記錄,所以事務(wù) B 的插入操作生成了一個(gè)插入意向鎖(LOCK_MODE: X,INSERT_INTENTION )。
2.遇到唯一鍵沖突
如果在插入新記錄時(shí),插入了一個(gè)與「已有的記錄的主鍵或者唯一二級(jí)索引列值相同」的記錄」(不過(guò)可以有多條記錄的唯一二級(jí)索引列的值同時(shí)為NULL,這里不考慮這種情況),此時(shí)插入就會(huì)失敗,然后對(duì)于這條記錄加上了 S 型的鎖。
至于是行級(jí)鎖的類(lèi)型是記錄鎖,還是 next-key 鎖,跟是主鍵沖突還是唯一二級(jí)索引沖突有關(guān)系。
如果主鍵值重復(fù):
- 當(dāng)隔離級(jí)別為讀已提交時(shí),插入新記錄的事務(wù)會(huì)給已存在的主鍵值重復(fù)的聚簇索引記錄添加 S 型記錄鎖。
- 當(dāng)隔離級(jí)別是可重復(fù)讀(默認(rèn)隔離級(jí)別),插入新記錄的事務(wù)會(huì)給已存在的主鍵值重復(fù)的聚簇索引記錄添加 S 型 next-key 鎖。
如果唯一二級(jí)索引列重復(fù):
- 不論是哪個(gè)隔離級(jí)別,插入新記錄的事務(wù)都會(huì)給已存在的二級(jí)索引列值重復(fù)的二級(jí)索引記錄添加 S 型 next-key 鎖。對(duì)的,沒(méi)錯(cuò),即使是讀已提交隔離級(jí)別也是加 next-key 鎖,這是讀已提交隔離級(jí)別中為數(shù)不多的給記錄添加間隙鎖的場(chǎng)景。因?yàn)槿绻惶砑娱g隙鎖的話,會(huì)讓唯一二級(jí)索引中出現(xiàn)多條唯一二級(jí)索引列值相同的記錄,這就違背了 UNIQUE 的約束。
下面舉個(gè)唯一二級(jí)索引沖突的例子,MySQL 8.0 版本,事務(wù)隔離級(jí)別為可重復(fù)讀(默認(rèn)隔離級(jí)別)。
t_order 表中的 order_no 字段為唯一二級(jí)索引,并且已經(jīng)存在 order_no 值為 1001 的記錄,此時(shí)事務(wù) A,插入了 order_no 為 1001 的記錄,就出現(xiàn)了報(bào)錯(cuò)。
但是除了報(bào)錯(cuò)之外,還做一個(gè)很重要的事情,就是對(duì) order_no 值為 1001 這條記錄加上了 S 型的 next-key 鎖。
我們可以執(zhí)行 select * from performance_schema.data_locks\G; 語(yǔ)句 ,確定事務(wù)加了什么類(lèi)型的鎖,這里只關(guān)注在記錄上加鎖的類(lèi)型。
可以看到,index_order 二級(jí)索引中的 1001(LOCK_DATA) 記錄的鎖類(lèi)型為 S 型的 next-key 鎖。注意,這里 LOCK_TYPE 中的 RECORD 表示行級(jí)鎖,而不是記錄鎖的意思。如果是記錄鎖的話,LOCK_MODE 會(huì)顯示 S, REC_NOT_GAP。
此時(shí),事務(wù) B 執(zhí)行了 select * from t_order where order_no = 1001 for update; 就會(huì)阻塞,因?yàn)檫@條語(yǔ)句想加 X 型的鎖,是與 S 型的鎖是沖突的,所以就會(huì)被阻塞。
我們也可以從 performance_schema.data_locks 這個(gè)表中看到,事務(wù) B 的狀態(tài)(LOCK_STATUS)是等待狀態(tài),加鎖的類(lèi)型 X 型的記錄鎖(LOCK_MODE: X,REC_NOT_GAP )。
上面的案例是針對(duì)唯一二級(jí)索引重復(fù)而插入失敗的場(chǎng)景。
接下來(lái),分析兩個(gè)事務(wù)執(zhí)行過(guò)程中,執(zhí)行了相同的 insert 語(yǔ)句的場(chǎng)景。
現(xiàn)在 t_order 表中,只有這些數(shù)據(jù),order_no 為唯一二級(jí)索引。
在隔離級(jí)別可重復(fù)讀的情況下,開(kāi)啟兩個(gè)事務(wù),前后執(zhí)行相同的 Insert 語(yǔ)句,此時(shí)事務(wù) B 的 Insert 語(yǔ)句會(huì)發(fā)生阻塞。
兩個(gè)事務(wù)的加鎖過(guò)程:
- 事務(wù) A 先插入 order_no 為 1006 的記錄,可以插入成功,此時(shí)對(duì)應(yīng)的唯一二級(jí)索引記錄被「隱式鎖」保護(hù),此時(shí)還沒(méi)有實(shí)際的鎖結(jié)構(gòu);
- 接著,事務(wù) B 也插入 order_no 為 1006 的記錄,由于事務(wù) A 已經(jīng)插入 order_no 值為 1006 的記錄,所以事務(wù) B 在插入二級(jí)索引記錄時(shí)會(huì)遇到重復(fù)的唯一二級(jí)索引列值,此時(shí)事務(wù) B 想獲取一個(gè) S 型 next-key 鎖,但是事務(wù) A 并未提交,事務(wù) A 插入的 order_no 值為 1006 的記錄上的「隱式鎖」會(huì)變「顯示鎖」且鎖類(lèi)型為 X 型的記錄鎖,所以事務(wù) B 向獲取 S 型 next-key 鎖時(shí)會(huì)遇到鎖沖突,事務(wù) B 進(jìn)入阻塞狀態(tài)。
我們可以執(zhí)行 select * from performance_schema.data_locks\G; 語(yǔ)句 ,確定事務(wù)加了什么類(lèi)型的鎖,這里只關(guān)注在記錄上加鎖的類(lèi)型。
先看事務(wù) A 對(duì) order_no 為 1006 的記錄加了什么鎖?從下圖可以看到,事務(wù) A 對(duì) order_no 為 1006 記錄加上了類(lèi)型為 X 型的記錄鎖(注意,這個(gè)是在執(zhí)行事務(wù) B 之后才產(chǎn)生的鎖,沒(méi)執(zhí)行事務(wù) B 之前,該記錄還是隱式鎖)。
然后看事務(wù) B 想對(duì) order_no 為 1006 的記錄加什么鎖?從下圖可以看到,事務(wù) B 想對(duì) order_no 為 1006 的記錄加 S 型的 next-key 鎖,但是由于事務(wù) A 在該記錄上持有了 X 型的記錄鎖,這兩個(gè)鎖是沖突的,所以導(dǎo)致事務(wù) B 處于等待狀態(tài)。
從這個(gè)實(shí)驗(yàn)可以得知,并發(fā)多個(gè)事務(wù)的時(shí)候,第一個(gè)事務(wù)插入的記錄,并不會(huì)加鎖,而是會(huì)用隱式鎖保護(hù)唯一二級(jí)索引的記錄。
但是當(dāng)?shù)谝粋€(gè)事務(wù)還未提交的時(shí)候,有其他事務(wù)插入了與第一個(gè)事務(wù)相同的記錄,第二個(gè)事務(wù)就會(huì)被阻塞,因?yàn)榇藭r(shí)第一事務(wù)插入的記錄中的隱式鎖會(huì)變?yōu)轱@示鎖且類(lèi)型是 X 型的記錄鎖,而第二個(gè)事務(wù)是想對(duì)該記錄加上 S 型的 next-key 鎖,X 型與 S 型的鎖是沖突的,所以導(dǎo)致第二個(gè)事務(wù)會(huì)等待,直到第一個(gè)事務(wù)提交后,釋放了鎖。
如果 order_no 不是唯一二級(jí)索引,那么兩個(gè)事務(wù),前后執(zhí)行相同的 Insert 語(yǔ)句,是不會(huì)發(fā)生阻塞的,就如前面的這個(gè)例子。
如何避免死鎖?
死鎖的四個(gè)必要條件:互斥、占有且等待、不可強(qiáng)占用、循環(huán)等待。只要系統(tǒng)發(fā)生死鎖,這些條件必然成立,但是只要破壞任意一個(gè)條件就死鎖就不會(huì)成立。
在數(shù)據(jù)庫(kù)層面,有兩種策略通過(guò)「打破循環(huán)等待條件」來(lái)解除死鎖狀態(tài):
設(shè)置事務(wù)等待鎖的超時(shí)時(shí)間。當(dāng)一個(gè)事務(wù)的等待時(shí)間超過(guò)該值后,就對(duì)這個(gè)事務(wù)進(jìn)行回滾,于是鎖就釋放了,另一個(gè)事務(wù)就可以繼續(xù)執(zhí)行了。在 InnoDB 中,參數(shù) innodb_lock_wait_timeout 是用來(lái)設(shè)置超時(shí)時(shí)間的,默認(rèn)值時(shí) 50 秒。當(dāng)發(fā)生超時(shí)后,就出現(xiàn)下面這個(gè)提示:
開(kāi)啟主動(dòng)死鎖檢測(cè)。主動(dòng)死鎖檢測(cè)在發(fā)現(xiàn)死鎖后,主動(dòng)回滾死鎖鏈條中的某一個(gè)事務(wù),讓其他事務(wù)得以繼續(xù)執(zhí)行。將參數(shù) innodb_deadlock_detect 設(shè)置為 on,表示開(kāi)啟這個(gè)邏輯,默認(rèn)就開(kāi)啟。當(dāng)檢測(cè)到死鎖后,就會(huì)出現(xiàn)下面這個(gè)提示:
上面這個(gè)兩種策略是「當(dāng)有死鎖發(fā)生時(shí)」的避免方式。
我們可以回歸業(yè)務(wù)的角度來(lái)預(yù)防死鎖,對(duì)訂單做冪等性校驗(yàn)的目的是為了保證不會(huì)出現(xiàn)重復(fù)的訂單,那我們可以直接將 order_no 字段設(shè)置為唯一索引列,利用它的唯一下來(lái)保證訂單表不會(huì)出現(xiàn)重復(fù)的訂單,不過(guò)有一點(diǎn)不好的地方就是在我們插入一個(gè)已經(jīng)存在的訂單記錄時(shí)就會(huì)拋出異常。
參考資料:
- 《MySQL 是怎樣運(yùn)行的?》
- http://mysql.taobao.org/monthly/2020/09/06/