概述
前幾天排查了一個死鎖問題,最開始百思不得其解,因為發生死鎖的兩個事務是單語句事務,語句類型相同(where屬性列相同,僅值不同),而且語句都走了相同的索引,但最終確實發生了死鎖。通過定位排查發現,問題的源頭就是index_merge,死鎖的原因也很普通,兩個事務加鎖順序不同,並存在相互等待的情況。因為這個案例比較特殊,所以在此分享給大家。
死鎖信息
拿到死鎖問題,首先需要查看幾個基本信息,包括死鎖等待關系,表結構定義等。
1.表結構定義
Create Table: CREATE TABLE `t_xxx_customer` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`partner_id` bigint(20) unsigned DEFAULT NULL,
`customer_id` bigint(20) unsigned DEFAULT NULL,
`deleted` tinyint(4) DEFAULT NULL,
`partner_user_id` bigint(20) unsigned DEFAULT NULL,
`xxx_id` varchar(128) DEFAULT NULL,
`xxx_name` varchar(256) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `partner_id` (`partner_id`),
KEY `customer_id` (`customer_id`),
KEY `partner_user_id` (`partner_user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=140249 DEFAULT CHARSET=utf8;
2.死鎖信息提取與分析
通過show engine innodb status;命令可以獲取innodb引擎中最近一次發生死鎖的信息,信息如下
*** (1) TRANSACTION: UPDATE t_xxx_customer SET xxx_id='101', xxx_name='bbb' where customer_id=235646 and partner_id=1688 and deleted=0;
*** (1) HOLDS THE LOCK(S): RECORD LOCKS space id 1640 page no 3947 n bits 432 index partner_id of table xxx.t_xxx_customer trx id 2625291980 lock_mode X locks rec but not gap Record lock, heap no 334 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 8; hex 0000000000000698; asc ;; 1: len 8; hex 0000000000021747; asc G;;
*** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 1640 page no 3395 n bits 160 index PRIMARY of table t_xxx_customer trx id 2625291980 lock_mode X locks rec but not gap waiting Record lock, heap no 89 PHYSICAL RECORD: n_fields 25; compact format; info bits 0
*** (2) TRANSACTION: UPDATE t_xxx_customer SET xxx_id='102', xxx_name='aaa' where customer_id=151069 and partner_id=1688 and deleted=0;
*** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 1640 page no 3395 n bits 160 index PRIMARY of table xxx.t_xxx_customer trx id 2625291981 lock_mode X locks rec but not gap
*** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 1640 page no 3947 n bits 432 index partner_id of table xxx.t_xxx_customer trx id 2625291981 lock_mode X locks rec but not gap waiting Record lock, heap no 334 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 8; hex 0000000000000698; asc ;; 1: len 8; hex 0000000000021747; asc G;;
*** WE ROLL BACK TRANSACTION (2)
從死鎖結果來看,我們很容易看到事務1持有 partner_id二級索引上的鎖,等待PK索引上的鎖;而事務2持有PK索引鎖,等待partner_id二級索引上的鎖,兩個事務相互持有對方需要的鎖資源,而無法往前推進,造成死鎖。單從死鎖信息來看,我們可能會比較疑惑,每個事務只有一個語句,為什么同樣的語句,對二級索引和主鍵的加鎖順序會不同?
產生死鎖的原因
首先我們來看看語句的執行計划,
語句的type是index_merge,Extra的信息是Using intersect(customerid,partnerid),從而我們得知語句執行計划走了index_merge優化,單個語句通過兩個索引(customerid,partnerid)來提取記錄集合並取交集獲得最終結果集。index_merge具體算法不在此展開,基本使用場景是語句包含多個查詢條件,每個條件都單獨存在索引,而單個條件的索引過濾度不高,組合起來過濾度比較高,這個時候就可能會走index_merge優化,使得單個SQL語句可以同時利用兩個索引過濾。會不會與index_merge有關呢?
在index_merge的情況下,會導致二級索引與主鍵索引順序不一致的情況嗎?結合上面的死鎖信息,我們得知死鎖兩個的二級索引key是0x698,而主鍵索引key是0x21747。我們看看到底是哪條記錄的主鍵和二級索引發生了死鎖,
可以看到0x21747對應的customer_id為151069,partner_id為1688,是不是感覺似曾相識,對的,第二個事務的語句查詢條件就是這兩個條件的組合。這說明,對於這條記錄,第一個事務語句只有partnerid索引(1688)滿足條件;對於第二個事務,customer_id和partner_id索引都滿足條件。由於每個語句執行時都需要利用兩個二級索引,假設先使用customer_id索引掃描,然后使用partner_id索引掃描,那么對於id為0x21747的記錄,事務1的partner_id=1688滿足條件,加partner_id鎖,然后對對應的PK索引加鎖;對於事務2,對customer_id= 151069加鎖,對對應的PK索引加鎖,然后對partner_id=1688索引加鎖。那么對partner_id二級索引和PK主鍵索引在兩個事務的上鎖順序是相反的,所以導致了死鎖。對於id為0x21747記錄:
序號 | 事務1 | 事務2 |
1 | customer_id 不滿足條件不加鎖 | customer_id= 151069 加鎖 |
2 | partner_id=1688加鎖 | PK=0x21747加鎖 |
3 | PK=0x21747加鎖 | partner_id=1688加鎖 |
4 | PK=0x21747加鎖 |
表格第2步和第3步,兩個事務的加鎖順序是相反的,導致了死鎖發生。
如何避免死鎖
前面啰啰嗦嗦講了一個死鎖案例的來龍去脈,但僅僅是產生死鎖的一種情況。生產環境中,我們當然不需要死鎖頻繁發生,畢竟是需要部分事務回滾才能解鎖的,下面介紹幾個簡單的原則,有助於減少死鎖的發生。
1) 盡量按順序加鎖,從源頭避免死鎖
2)選擇合適的隔離級別,隔離級別越高,並發沖突越激烈,實際場景Read-Committed就夠用了
3)避免使用大事務,根據二段鎖原則,只有事務結束(提交或回滾)才會釋放鎖,持有的鎖越多,可能導致的沖突越大
4)為表添加合適的索引,避免走不到索引導致對每條記錄都加鎖