MySQL死鎖分析

什么是死鎖

MySQL的死鎖指的是兩個(gè)事務(wù)互相等待的場(chǎng)景,這種循環(huán)等待理論上不會(huì)有盡頭。
比如事務(wù)A持有行1的鎖,事務(wù)B持有行2的鎖,
然后事務(wù)A試圖獲取行2的鎖,事務(wù)B試圖獲取行1的鎖,
這樣事務(wù)A要等待事務(wù)B釋放行2的鎖,事務(wù)B要等待事務(wù)A釋放行1的鎖,
兩個(gè)事務(wù)互相等待,誰也提交不了。這種情況下MySQL會(huì)選擇中斷并回滾其中一個(gè)事務(wù),使得另一個(gè)事務(wù)可以提交。MySQL會(huì)記錄死鎖的日志。

死鎖案列

MySQL版本:Server version: 8.0.27 MySQL Community Server - GPL

測(cè)試表

    CREATE TABLE `record` (
      `id` bigint(20)  NOT NULL AUTO_INCREMENT,
      `biz_id` bigint(20) NOT NULL DEFAULT 1,
      `vid` bigint(20) NOT NULL DEFAULT '0',
      PRIMARY KEY (`id`),
      index idx_biz_id(`biz_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

初始化數(shù)據(jù)

insert into record(biz_id,vid) values(10,1231);
insert into record(biz_id,vid) values(12,8716);
insert into record(biz_id,vid) values(14,2948);
insert into record(biz_id,vid) values(16,7463);

+----+--------+------+
| id | biz_id | vid  |
+----+--------+------+
|  1 |     10 | 1231 |
|  2 |     12 | 8716 |
|  3 |     14 | 2948 |
|  4 |     16 | 7463 |
+----+--------+------+

先刪再插以此實(shí)現(xiàn)insertOrUpdate

步驟 事務(wù)A 事務(wù)B
1 set autocommit=0;
2 delete from record where id=18;
3 set autocommit=0;
4 delete from record where id=18;
5 insert into record(biz_id,vid) values(18,1231) blocked
6 insert into record(biz_id,vid) values(18,1231) ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
7 Query OK, 1 row affected (9.30 sec)

查看日志

show engine innodb status

發(fā)現(xiàn)死鎖日志

LATEST DETECTED DEADLOCK
------------------------
2022-03-03 16:41:21 0x70000da29000
*** (1) TRANSACTION:
// 事務(wù)ID,活躍時(shí)間,事務(wù)正在進(jìn)行的操作
TRANSACTION 1897, ACTIVE 79 sec inserting
//修改1個(gè)表,鎖1行數(shù)據(jù)
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1128, 2 row lock(s)
//線程ID和queryId
MySQL thread id 24, OS thread handle 123145545482240, query id 166 localhost root update
// 執(zhí)行的SQL
insert into record(biz_id,vid) values(18,1231)

*** (1) HOLDS THE LOCK(S):
//持有的鎖信息
RECORD LOCKS space id 6 page no 4 n bits 72 index PRIMARY of table `test`.`record` trx id 1897 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;


*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
//等待的鎖
RECORD LOCKS space id 6 page no 4 n bits 72 index PRIMARY of table `test`.`record` trx id 1897 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;


*** (2) TRANSACTION:
TRANSACTION 1898, ACTIVE 49 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1128, 2 row lock(s)
MySQL thread id 20, OS thread handle 123145546547200, query id 167 localhost root update
insert into record(biz_id,vid) values(18,1231)

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 6 page no 4 n bits 72 index PRIMARY of table `test`.`record` trx id 1898 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;


*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 6 page no 4 n bits 72 index PRIMARY of table `test`.`record` trx id 1898 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;
//死鎖處理結(jié)果
*** WE ROLL BACK TRANSACTION (2)

MySQL的8種鎖

1 行鎖(Record Locks)

行鎖是作用在索引上的。

2 間隙鎖(Gap Locks)

間隙鎖是鎖住一個(gè)區(qū)間的鎖。
這個(gè)區(qū)間是一個(gè)開區(qū)間,范圍是從某個(gè)存在的值向左直到比他小的第一個(gè)存在的值,所以間隙鎖包含的內(nèi)容就是在查詢范圍內(nèi),而又不存在的數(shù)據(jù)區(qū)間。
比如有id分別是1,10,20,要修改id<15的數(shù)據(jù),那么生成的間隙鎖有以下這些:(-∞,1),(1,10),(10,20),此時(shí)若有其他事務(wù)想要插入id=11的數(shù)據(jù),則需要等待。
間隙鎖是不互斥的。
作用是防止其他事務(wù)在區(qū)間內(nèi)添加記錄,而本事務(wù)可以在區(qū)間內(nèi)添加記錄,從而防止幻讀。
在可重復(fù)讀這種隔離級(jí)別下會(huì)啟用間隙鎖,而在讀未提交和讀已提交兩種隔離級(jí)別下,即使使用select ... in share mode或select ... for update,也不會(huì)有間隙鎖,無法防止幻讀。

3 臨鍵鎖(Next-key Locks)

臨鍵鎖=間隙鎖+行鎖,于是臨鍵鎖的區(qū)域是一個(gè)左開右閉的區(qū)間。
隔離級(jí)別是可重復(fù)讀時(shí),select ... in share mode或select ... for update會(huì)使用臨鍵鎖,防止幻讀。普通select語句是快照讀,不能防止幻讀。

4 共享鎖/排他鎖(Shared and Exclusive Locks)

共享鎖和排它鎖都是行鎖。共享鎖用于事務(wù)并發(fā)讀取,比如select ... in share mode。排它鎖用于事務(wù)并發(fā)更新或刪除。比如select ... for update

5 意向共享鎖/意向排他鎖(Intention Shared and Exclusive Locks)

意向共享鎖和意向排他鎖都是表級(jí)鎖。
官方文檔中說,事務(wù)獲得共享鎖前要先獲得意向共享鎖,獲得排它鎖前要先獲得意向排它鎖。
意向排它鎖互相之間是兼容的。

6 插入意向鎖(Insert Intention Locks)

插入意向鎖鎖的是一個(gè)點(diǎn),是一種特殊的間隙鎖,用于并發(fā)插入。
插入意向鎖和間隙鎖互斥。插入意向鎖互相不互斥。

7 自增鎖(Auto-inc Locks)

自增鎖用于事務(wù)中插入自增字段。5.1版本前是表鎖,5.1及以后版本是互斥輕量鎖。
自增所相關(guān)的變量有:

auto_increment_offset,初始值

auto_increment_increment,每次增加的數(shù)量

innodb_autoinc_lock_mode,自增鎖模式

其中:

innodb_autoinc_lock_mode=0,傳統(tǒng)方式,每次都產(chǎn)生表鎖。此為5.1版本前的默認(rèn)配置。

innodb_autoinc_lock_mode=1,連續(xù)方式。產(chǎn)生輕量鎖,申請(qǐng)到自增鎖就將鎖釋放,simple insert會(huì)獲得批量的鎖,保證連續(xù)插入。此為5.2版本后的默認(rèn)配置。

innodb_autoinc_lock_mode=2,交錯(cuò)鎖定方式。不鎖表,并發(fā)速度最快。但最終產(chǎn)生的序列號(hào)和執(zhí)行的先后順序可能不一致,也可能斷裂。

鎖組合

首先我們要知道對(duì)于MySQL有兩種常規(guī)鎖模式

LOCK_S(讀鎖,共享鎖)

LOCK_X(寫鎖,排它鎖)

最容易理解的鎖模式,讀加共享鎖,寫加排它鎖.

有如下幾種鎖的屬性

LOCK_REC_NOT_GAP (鎖記錄)

LOCK_GAP (鎖記錄前的GAP)

LOCK_ORDINARY (同時(shí)鎖記錄+記錄前的GAP 。傳說中的Next Key鎖)

LOCK_INSERT_INTENTION(插入意向鎖,其實(shí)是特殊的GAP鎖)

鎖的屬性可以與鎖模式任意組合。例如.

lock->type_mode 可以是Lock_X 或者Lock_S

locks gap before rec 表示為gap鎖:lock->type_mode & LOCK_GAP

locks rec but not gap 表示為記錄鎖,非gap鎖:lock->type_mode &
LOCK_REC_NOT_GAP

insert intention 表示為插入意向鎖:lock->type_mode & LOCK_INSERT_INTENTION

waiting 表示鎖等待:lock->type_mode & LOCK_WAIT

鎖之間兼容性

X 排他鎖
S 共享鎖
IX 意向排他鎖
IS 意向共享鎖

X IX S IS
X
IX 兼容 兼容
S 兼容 兼容
IS 兼容 兼容 兼容

死鎖解鎖

InnoDB存儲(chǔ)引擎會(huì)選擇回滾undo量最小的事務(wù)

參考

并發(fā)insert死鎖驗(yàn)證
MySQL DELETE 刪除語句加鎖分析
死鎖看這里

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容