一次有趣的MYSQL死鎖排查過程

數據庫問題中,由於SQL問題導致的數據庫故障是最爲常見的,本文針對曾經負責的一個核心繫統在上線新業務功能拋出了許多 MySQL 死鎖導致事務回滾的異常,給出了詳細的排查流程:

  • 1、復現死鎖出現的場景
  • 2、分析死鎖出現的原因
  • 3、給出解決方案

1、 復現場景

某天晚上,某核心應用在生產環境正在發佈,突然線上大量報警,很多異常信息都是關於數據庫死鎖的

 Deadlock found when trying to get lock; try restarting transaction

Mysql數據庫基礎信息:

  • 1、版本:Ali-RDS 5.6.16-log
  • 2、數據庫引擎:InnoDB
  • 3、事務隔離級別:READ-COMMITED

去除業務中非重要字段,出現死鎖異常的數據庫表結構爲:

CREATE TABLE `bs_customer_sass_cache` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `gmt_create` datetime NOT NULL COMMENT '創建時間',
  `gmt_modified` datetime NOT NULL COMMENT '修改時間',
  `cpb_mc` varchar(256) NOT NULL COMMENT '產品說明',
  `cpb_id` varchar(64) NOT NULL COMMENT '產品Id',
  `state` varchar(64) DEFAULT NULL COMMENT '狀態',
  `cpb_no` varchar(256) DEFAULT NULL COMMENT '產品訂單號',
  PRIMARY KEY (`id`),
  KEY `idx_cpbId_cpbNo` (`cpb_id`,`cpb_no`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='sass緩存'

bs_customer_sass_cache表中包含兩個索引:主鍵索引和idx_cpbId_cpbNo由 (cpb_id,cpb_no) 字段構成的非主鍵聯合索引。

兩個或更多個列上的索引被稱作聯合索引,也稱爲複合索引。對於複合索引:Mysql從左到右的使用索引中的字段,一個查詢可以只使用索引中的一部份,但只能是最左側部分(最左前綴原則)。例如索引是key index (a,b,c)。可以支持a | a,b| a,b,c 3種組合進行索引。

默認,MySQL session開啓自動提交模式(變量autocommit爲ON)。只要你執行DML操作的語句, MySQL會立即隱式提交事務(Implicit Commit)。變量autocommit分會話系統變量與全局系統變量:

$Mysql> show session variables like 'autocommit';
$Mysql> show global variables like 'autocommit';

在這裏插入圖片描述

上述SQL修改會話系統變量或全局系統變量,只對當前實例有效,如果MySQL服務重啓的話,這些設置就會丟失:

$Mysql> set session autocommit=0;
$Mysql> set global autocommit=0;

2、分析死鎖出現原因

2.1 獲取死鎖信息

  • 1、獲取死鎖信息命令:
$Mysql> show engine innodb status

上訴命令顯示的不是當前狀態,而是過去某個時間範圍內InnoDB存儲引擎的狀態。

Per second averages calculated from the last 8 seconds

死鎖日誌主要包含以下幾個部分:

Content Description
BACKGROUND THREAD 後臺Master線程
SEMAPHORES 信號量信息
LATEST DETECTED DEADLOCK 最近一次死鎖信息,只有產生過死鎖纔會有
TRANSACTIONS 事物信息
FILE I/O IO Thread信息
INSERT BUFFER AND ADAPTIVE HASH INDEX INSERT BUFFER和自適應HASH索引
LOG 日誌
BUFFER POOL AND MEMORY BUFFER POOL和內存
INDIVIDUAL BUFFER POOL INFO 如果設置了多個BUFFER POOL實例,這裏顯示每個BUFFER POOL信息。可通過innodb_buffer_pool_instances參數設置
ROW OPERATIONS 行操作統計信息
END OF INNODB MONITOR OUTPU 日誌

TRANSACTIONS

包含了InnoDB事務(transaction)的統計信息。

  • 2、查詢 正在執行的事務:
$Mysql> select * from information_schema.innodb_trx;
innodb_trx ## 當前運行的所有事務 
innodb_locks ## 當前出現的鎖 
innodb_lock_waits ## 鎖等待的對應關係
查出innodb_trx中死鎖事務的trx_mysql_thread_id,然後kill掉。 
  • 3、查詢mysql數據庫中存在的線程:
$Mysql> show processlist;
$Mysql> kill thread_id
  • 4、mysql 事務鎖超時時間 innodb_lock_wait_timeout:

查詢全局等待事務鎖超時時間
$Mysql> SHOW GLOBAL VARIABLES LIKE ‘innodb_lock_wait_timeout’;
設置全局等待事務鎖超時時間
$Mysql> SET GLOBAL innodb_lock_wait_timeout=100;
查詢當前會話等待事務鎖超時時間
$Mysql> SHOW VARIABLES LIKE ‘innodb_lock_wait_timeout’;

2.2 分析死鎖日誌

記錄最近一次死鎖信息,只有產生過死鎖纔會有記錄。查看死鎖日誌:

------------------------
LATEST DETECTED DEADLOCK
------------------------
2020-04-26 17:36:23 0x7fb82c9b6700
*** (1) TRANSACTION:
TRANSACTION 124829516, ACTIVE 11 sec fetching rows
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 1
MySQL thread id 513161, OS thread handle 140429259630336, query id 11331899 10.200.7.9 hswy_basic Searching rows for update
/* ApplicationName=IntelliJ IDEA 2019.3.4 */ update `bs_customer_sass_cache`
set `gmt_modified` = sysdate(), `state` = ''1''
where (`state` = ''0'' AND `cpb_id` = ''20200426'' AND `cpb_no` = ''2020042615158064425'')
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 5810 page no 3 n bits 72 index PRIMARY of table `eagle_dev`.`bs_customer_sass_cache` trx id 124829516 lock_mode X locks rec but not gap waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 9; compact format; info bits 0
 0: len 8; hex 0000000000000002; asc         ;;
 1: len 6; hex 00000770bf4d; asc    p M;;
 2: len 7; hex 3a0000013728e0; asc :   7( ;;
 3: len 5; hex 99a63513f5; asc   5  ;;
 4: len 5; hex 99a635190d; asc   5  ;;
 5: len 10; hex e4baa7e59381e58c8532; asc          2;;
 6: len 8; hex 3230323030343236; asc 20200426;;
 7: len 1; hex 30; asc 0;;
 8: len 19; hex 32303230303432363135313538303634343235; asc 2020042615158064425;;

*** (2) TRANSACTION:
TRANSACTION 124829517, ACTIVE 10 sec starting index read
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 513162, OS thread handle 140428999091968, query id 11331918 10.200.7.9 hswy_basic Searching rows for update
/* ApplicationName=IntelliJ IDEA 2019.3.4 */ update `bs_customer_sass_cache`
set `gmt_modified` = sysdate(), `state` = ''1''
where (`state` = ''0'' AND `cpb_id` = ''20200426'' AND `cpb_no` = ''2020042615158064425'')
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 5810 page no 3 n bits 72 index PRIMARY of table `eagle_dev`.`bs_customer_sass_cache` trx id 124829517 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 9; compact format; info bits 0
 0: len 8; hex 0000000000000002; asc         ;;
 1: len 6; hex 00000770bf4d; asc    p M;;
 2: len 7; hex 3a0000013728e0; asc :   7( ;;
 3: len 5; hex 99a63513f5; asc   5  ;;
 4: len 5; hex 99a635190d; asc   5  ;;
 5: len 10; hex e4baa7e59381e58c8532; asc          2;;
 6: len 8; hex 3230323030343236; asc 20200426;;
 7: len 1; hex 30; asc 0;;
 8: len 19; hex 32303230303432363135313538303634343235; asc 2020042615158064425;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 5810 page no 4 n bits 72 index idx_cpbId_cpbNo of table `eagle_dev`.`bs_customer_sass_cache` trx id 124829517 lock_mode X waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 8; hex 3230323030343236; asc 20200426;;
 1: len 19; hex 32303230303432363135313538303634343235; asc 2020042615158064425;;
 2: len 8; hex 0000000000000001; asc         ;;

*** WE ROLL BACK TRANSACTION (2)
2.2.1、死鎖日誌分析

TRANSACTION 124829516, ACTIVE 11 sec fetching rows

事務編號爲 124829516 ,活躍了11秒,fetching rows 表示事務狀態爲正在查詢記錄。常見的其他狀態:
updating or deleting:表示事物已經真正進入了update/delete的函數邏輯(row_update_for_mysql)
starting index read :表示事務狀態爲根據索引讀取數據。

mysql tables in use 1, locked 1

mysql tables in use 1有一個表被使用(trx->n_mysql_tables_in_use) ,在函數ha_innobase::external_lock中trx->n_mysql_tables_in_use被遞增。

locked 1表示表上有一個表鎖(加鎖函數爲lock_table,trx->mysql_n_tables_locked),對於DML語句爲LOCK_IX

LOCK WAIT 4 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 1

此事務處於LOCK WAIT狀態,擁有4個鎖結構(4個行鎖結構,鎖結構???),heap size是爲了存儲鎖結構而申請的內存大小(可以忽略),其中有4個行鎖的結構。undo log entries 1表示當前事務有 1個 undo log 記錄。

MySQL thread id 513161, OS thread handle 140429259630336, query id 11331899 10.200.7.9 customer_basic_user Searching rows for update

本事務所在MySQL線程的id是513161,該線程在操作系統級別的id就是140429259630336,當前查詢的id爲11331899(MySQL內部使用,可以忽略),還有用戶名主機信息。

update bs_customer_sass_cache
set gmt_modified = sysdate(), state = ‘‘1’’
where (state = ‘‘0’’ AND cpb_id = ‘‘20200426’’ AND cpb_no = ‘‘2020042615158064425’’)

本事物發生阻塞的SQL語句

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:

本事務當前在等待獲取的鎖:

RECORD LOCKS space id 5810 page no 3 n bits 72 index PRIMARY of table eagle_dev.bs_customer_sass_cache trx id 124829516 lock_mode X locks rec but not gap waiting

等待獲取的表空間ID爲5810 ,頁號爲3,n bits 72表示這個聚集索引記錄鎖結構上留有72個Bit位。該鎖的類型是X型記錄鎖(rec but not gap)。

lock_mode X表示該記錄鎖爲排他鎖:lock->type_mode & LOCK_MODE_MASK
其他還有:
” 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

2.2.2、死鎖業務分析

繼續來分析應用中發生死鎖的事物代碼位置:

@Transactional
public void updateStatus(Long id, String cpbId,  String cpbNo) {
    customerSassCacheDAO.updateCpbNo(id, cpbId, cpbNo);
    customerSassCacheDAO.updateStatus(cpbId, cpbNo, '0');
}

上訴代碼的目的是爲了修改同一條記錄的兩個字段

updateCpbNo 對應的mybatis SQL:

update bs_customer_sass_cache
set gmt_modified=sysdate(), cpb_no = #{cpbNo}
where id =#{id}  and cpb_id = #{cpbId};

updateStatus對應的mybatis SQL:

update `bs_customer_sass_cache`
set `gmt_modified` = sysdate(), `state` = '1'
where `state` = #{state} AND `cpb_id` = #{cpbId} AND `cpb_no` = #{cpbNo};

updateCpbNo用到的是主鍵索引,updateStatus用的的是idx_cpbId_cpbNo索引。

2.2.3、死鎖背後的原理

通過MySQL begin/commit語句模擬兩個併發的數據庫事物:

事務T1:

begin

update bs_customer_sass_cache
set gmt_modified=sysdate(), cpb_no = '2020042615158064425'
where id = 1 and cpb_id = '20200426';

select  sleep(10);

update `bs_customer_sass_cache`
set `gmt_modified` = sysdate(), `state` = '1'
where (`state` = '0' AND `cpb_id` = '20200426' AND `cpb_no` = '2020042615158064425');

select sleep(50);
commit;

事務T2:

begin;

update bs_customer_sass_cache
set gmt_modified=sysdate(), cpb_no = '2020042615158064425'
where id = 2 and cpb_id = '20200426';

select  sleep(10);

update `bs_customer_sass_cache`
set `gmt_modified` = sysdate(), `state` = '1'
where (`state` = '0' AND `cpb_id` = '20200426' AND `cpb_no` = '2020042615158064425');

select sleep(50);

commit;
事物T1和T2的執行順序如下:

在這裏插入圖片描述

InnoDB行鎖是通過給索引上的索引項加鎖來實現的,這一點MySQL與Oracle不同,後者是通過在數據塊中對相應數據行加鎖來實現的。InnoDB這種行鎖實現特點意味着:只有通過索引條件檢索數據,InnoDB才使用行級鎖,否則,InnoDB將使用表鎖!

Mysql行級鎖並不是直接鎖記錄,而是鎖索引,如果一條SQL語句用到了主鍵索引,mysql會鎖住主鍵索引;如果一條語句操作了非主鍵索引,mysql會先鎖住非主鍵索引,再鎖定主鍵索引。

事物T1執行第一條SQL語句獲得了primary id=1主鍵索引對應的鎖,事物T2執行第一條SQL語句時獲取了primary id=2主鍵索引對應的鎖;事物T1執行第二條SQL語句時,佔有了idx_cpbId_cpbNo (20200426, 2020042615158064425)非主鍵索引,嘗試佔有primary id=2主鍵索引對應的鎖失敗;事物T2執行第二條SQL語句時,佔有idx_cpbId_cpbNo (20200426, 2020042615158064425)非主鍵索引對應的鎖失敗。

主鍵索引: Mysql數據庫InnoDB引擎使用B+Tree作爲索引結構,InnoDB的數據文件本身就是索引文件,樹的葉節點data域保存了完整的數據記錄,這個索引的key是數據表的主鍵,因此InnoDB表數據文件本身就是主索引。B+Tree葉節點包含了完整的數據記錄,這種索引叫做聚集索引
在這裏插入圖片描述

輔助索引: InnoDB的輔助索引data域存儲相應記錄主鍵的值而不是地址。換句話說,InnoDB的所有輔助索引都引用主鍵作爲data域。
在這裏插入圖片描述
上訴索引簡化的結構如下:
在這裏插入圖片描述

死鎖 是併發系統中常見的問題,同樣也會出現在Innodb系統中。當兩個及以上的事務,雙方都在等待對方釋放已經持有的鎖或者因爲加鎖順序不一致造成循環等待鎖資源,就會出現"死鎖"。舉例來說A 事務持有x1鎖 ,申請x2鎖,B 事務持有x2鎖,申請x1 鎖。A和B 事務持有鎖並且申請對方持有的鎖進入循環等待,就造成死鎖。

  • 2、數據庫4種隔離級別

READ UNCOMMITTED(讀未提交數據)
允許事務讀取未被其他事務提交的變更,髒讀、不可重複讀和幻讀的問題都會出現
READ COMMITED(讀已提交數據)
只允許事務讀取已經被其他事務提交的變更,可以避免髒讀,但不可重複讀和幻讀問題仍然會出現
REPEATABLE READ(可重複讀)
確保事務可以多次從一個字段中讀取相同的值,在這個事務持續期間,禁止其他事務對這個字段進行更新,可以避免髒讀和不可重複讀,但幻讀的問題依然存在
SERIALIZABLE(串行化)
確保事務可以從一個表中讀取相同的行,在這個事務持續期間,禁止其他事務對該表執行插入、更新和刪除操作,所有併發問題都可以避免,但性能十分低

  • 3、共享鎖/排他鎖

共享鎖又稱爲讀鎖,簡稱S鎖,顧名思義,共享鎖就是多個事務對於同一數據可以共享一把鎖,都能訪問到數據,但是隻能讀不能修改。

排他鎖又稱爲寫鎖,簡稱X鎖,顧名思義,排他鎖就是不能與其他所並存,如一個事務獲取了一個數據行的排他鎖,其他事務就不能再獲取該行的其他鎖,包括共享鎖和排他鎖,但是獲取排他鎖的事務是可以對數據就行讀取和修改。

間隙鎖(Gap Lock)是Innodb在提交下爲了解決幻讀問題時引入的鎖機制,(下面的所有案例沒有特意強調都使用可重複讀隔離級別)幻讀的問題存在是因爲新增或者更新操作,這時如果進行範圍查詢的時候(加鎖查詢),會出現不一致的問題,這時使用不同的行鎖已經沒有辦法滿足要求,需要對一定範圍內的數據進行加鎖,間隙鎖就是解決這類問題的

死鎖在操作系統中指的是兩個或兩個以上的進程在執行的過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或者系統產生了死鎖,這些永遠在互相等待的進程稱爲死鎖進程。

3、解決方案

死鎖出現的原因排查過程已詳細解釋了死鎖背後發生的原理,我們來解決這個問題。

更新事物時儘可能帶上主鍵
在一個transaction中,更新update操作儘可能不要操作同一條記錄

4、數據庫常見問題歸類

死鎖是指兩個或兩個以上的進程在執行過程中,因爭奪資源而造成的一種互相等待的現象。正常死鎖會自動釋放,innodb有一個內在的死鎖檢測工具,當死鎖超過一定時間後,會回滾其中一個事務,innodb_lock_wait_timeout可配置死鎖等待超時時間。

死鎖在兩情況下最容易產生:

高併發同時操作同一條數據
存在主鍵和輔助索引,加鎖順序相反

避免死鎖方法即降低併發,操作數據時使加鎖順序相同。

願你也能走出你的信息繭房
代碼之外的騷技能

個人博客:http://www.geek-make.com

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章