mysql中刪除同一行會常常出現死鎖?太可怕了

以前有一個同事問到我,爲何多個線程同時去作刪除同一行數據的操做,總是報死鎖,在線上已經出現好屢次了,我問了他幾個問題:
 
1. 是否是在一個事務中作了好幾件事情?
     答:不是,只作一個刪除操做,自動提交
2. 有多少個線程在作刪除?
     答:差很少10個
3. 是什麼隔離級別?
     答:可重複讀
 
當時以爲難以想象,按說自動提交的話行鎖,若是已經有事務加鎖了,則會等待,等提交以後再去作,發現已經刪除了,就會返回,刪除0條,爲何會死鎖?
但事情已經出了,必須研究一下,否則終究是心頭之苦啊。
而後想到既然線上有這麼簡單的操做就能夠死鎖,那麼應該寫個簡單的程序就能夠重現,而後同事李潤傑兄弟咔嚓咔嚓沒多時就給我了,在這裏謝謝他。
 
首先環境是這樣的:

CREATE TABLE `abcdefg` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `abc` varchar(30),
  `def` varchar(30) ,
  `ghi` date,
  `jkl` date,
  `mnp` tinyint(4),
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniqdefghijkl` (`def`,`ghi`,`jkl`)
);
 
這個表包括2個索引,一個是聚簇索引,另外一個是uniqdefghijkl的二級惟一索引。
事先插入不少數據,而後3個線程同時作對同一條記錄的刪除,這裏只作刪除操做,而且都是自動提交,爲了獲得一批要刪除的數據,事先查詢不少條出來備用。
 
刪除語句是這樣的:
delete from abcdefg WHERE abc= '我是變量' and def= '我是變量' and ghi= '2013-12-19 00:00:00' and jkl= '2013-12-20 00:00:00';

那麼如今就開始重現。。。
果真很快,死鎖真的出現了,下面是執行show engine innodb status的結果:
===================================================
LATEST DETECTED DEADLOCK
------------------------
140123 12:20:50
*** (1) TRANSACTION:
TRANSACTION 2E10, ACTIVE 4917 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 376, 1 row lock(s)
MySQL thread id 3, OS thread handle 0x1008, query id 43 192.168.xx.x username upd
ating
delete from abcdefg WHERE abc= '我是變量' and def= '我是變量' and ghi= '2013-12-19 00:00:00' and jkl= '2013-12-20 00:00:00';
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 12295 n bits 528 index `uniqdefghijkl` of table
`deadlock`.`abcdefg` trx id 2E10 lock_mode X locks rec but not gap waiti
ng
Record lock, heap no 167 PHYSICAL RECORD: n_fields 4; compact format; 

*** (2) TRANSACTION:
TRANSACTION 2E0E, ACTIVE 4917 sec starting index read
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1248, 2 row lock(s)
MySQL thread id 1, OS thread handle 0x1190, query id 41 192.168.xx.xx username upd
ating
delete from abcdefg WHERE abc= '我是變量' and def= '我是變量' and ghi= '2013-12-19 00:00:00' and jkl= '2013-12-20 00:00:00';
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 0 page no 12295 n bits 528 index `uniqdefghijkl` of table
`deadlock`.`abcdefg` trx id 2E0E lock_mode X locks rec but not gap
Record lock, heap no 167 PHYSICAL RECORD: n_fields 4; compact format; 

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 12295 n bits 528 index `uniqdefghijkl` of table
`deadlock`.`abcdefg` trx id 2E0E lock_mode X waiting
Record lock, heap no 167 PHYSICAL RECORD: n_fields 4; compact format; 
*** WE ROLL BACK TRANSACTION (1)
===================================================
這是在三個線程的狀況下是能夠重現死鎖的,可是爲了更容易調試,試了一下在2個線程的狀況下如何,最終發現重現不了。
這下壞了,多線程調試很麻煩,有時候這個走那個不走的,若是凍結某個線程,有可能致使線程之間死鎖,或者天然執行,那又不能出現死鎖的狀況,由於這個死鎖也是偶然性的,因此最終只有一種方法,那就是在mysql代碼中打印log信息,將鎖、記錄與事務這塊的函數中具備分歧點的地方都加了註釋,而且將有用的信息打印出來,最終分析log文件,才發現了真正死鎖的貓膩。
 
如今將三個致使死鎖的事務的時序圖畫出來:
事務A 事務B 事務C
開始    
表的IX鎖 17    @1    
二級索引行鎖X REC NOTGAP 1059    @2
檢查死鎖 沒事
   
  表IX鎖 17    @3  
  二級索引記錄行鎖 REC NOTGAP X WAIT 1315    @4
檢查死鎖,沒事
 
    表IX鎖 17    @5
   
二級索引記錄行鎖 REC NOTGAP X WAIT 1315    @6
檢查死鎖 沒事
聚簇索引行鎖X REC NOTGAP 1059    @7    
  wait.... suspend.... wait.... suspend....
commit    
  wakeup this trx
將@4的WAIT去掉,成爲1059
 
 
二級索引記錄行鎖 REC X WAIT 291    @8
檢查死鎖 發現死鎖
 
圖1
 
說明:
上面的數字都是源代碼中的關於各類鎖的位圖值:
LOCK_TABLE:16
LOCK_IX:1
LOCK_REC_NOT_GAP:1024
LOCK_WAIT:256
LOCK_REC:32
LOCK_X:3
 
因此鎖@6表示的是 LOCK_REC |  LOCK_REC_NOT_GAP |  LOCK_X |  LOCK_WAIT = 1315
依次類推
 
這裏檢查死鎖的算法大致上說一下,無非是檢查有沒有造成等待環
事務B的鎖@8等待事務C的鎖@6,事務C的鎖@6在等待事務B的鎖@3,此時發現又繞回來了,那麼產生死鎖。
 
到這裏,死鎖的現象如何產生已經解釋清楚,可是,這是爲何呢?
這裏的疑問是:
 
在事務A提交以後,將事務B喚醒了,此時事務B的鎖@4爲REC NOTGAP X(1059),那麼此時這個事務又去檢查鎖的狀況,看看本身事務的鎖有沒有GRANT成功的,若是有則直接使用而且繼續執行,若是沒有則再加鎖,作這個檢查的函數爲lock_rec_has_expl,它作的事情是下面的檢查:
===========================================================
                lock = lock_rec_get_first(block, heap_no);
                 while (lock) {
                                 if  (lock->trx == trx
                                    && !lock_is_wait_not_by_other(lock->type_mode)
                                    && lock_mode_stronger_or_eq(lock_get_mode(lock),
                                                                                                precise_mode & LOCK_MODE_MASK)
                                    && (!lock_rec_get_rec_not_gap(lock)
                                                || (precise_mode & LOCK_REC_NOT_GAP)
                                                || heap_no == PAGE_HEAP_NO_SUPREMUM)
                                    && (!lock_rec_get_gap(lock)
                                                || (precise_mode & LOCK_GAP)
                                                || heap_no == PAGE_HEAP_NO_SUPREMUM)
                                    && (!lock_rec_get_insert_intention(lock))) {
 
                                                 return(lock);
                                }
 
                                lock = lock_rec_get_next(heap_no, lock);
                }
=============================================================
這裏須要知足6個條件:
  1. 首先這個鎖是本身事務的
  2. 這個鎖不是處於等待狀態
  3. 當前鎖的類型與precise_mode是兼容的,precise_mode值是X鎖,由於這裏是要作刪除
  4. 當前鎖不是NOT GAP類型,或者要加的鎖類型是NOTGAP類型的,或者heapno爲1
  5. 當前鎖不是GAP類型,或者要加的鎖類型是GAP類型的,或者heapno爲1
  6. 當前鎖不是意向插入鎖
但此時發現1059(鎖@4)根本不知足第4點啊,由於它首先是NOTGAP鎖,同時heapno不是1,因此沒有找到,因此在外面又從新建立一個鎖,由於此時這行已經有鎖了,那麼它會建立一個REC WAIT X鎖(291),也就是鎖@8。
 
因此即便鎖@4不是處於等待狀態了,此時也不能直接執行呢,而是從新建立了一個鎖。此時致使了死鎖。
 
那麼如今問題又來了,從上圖能夠看到,這個時間序列沒有什麼特別的,或者特殊的一個交叉過程,從而是否是咱們能夠很容易的重現呢?僅僅經過開啓三個會話,都設置爲not autocommit的,由於須要將第一個事務A的提交放在事務B C的後面。
那麼開始了,建立相同的表,刪除同一行記錄。
事務A 事務B 事務C
begin    
delete
刪除行數返回爲1
   
  begin  
  delete 阻塞  
    begin
    阻塞
commit    
  觀察有沒有死鎖
其實並無死鎖
刪除行數返回爲0
 
    刪除行數返回爲0
  圖2
 
按說,上面這個圖與圖1沒有什麼區別,但沒有死鎖?爲何?
其實沒有死鎖是正常的,若是這樣就死鎖了,那mysql簡直不能用了!!!
 
看來仍是有區別的
正常模式下再作一次log分析,從log中看出了大問題......
再將上面詳細的加鎖圖在無死鎖模式下的狀況貼出來:
 
事務A 事務B 事務C
開始    
表的IX鎖 17    @1    
二級索引行鎖X REC NOTGAP 1059    @2
檢查死鎖 沒事
   
聚簇索引行鎖X REC NOTGAP 1059    @7
檢查死鎖 沒事
   
  表IX鎖 17    @3  
  二級索引記錄行鎖 REC X WAIT 291    @4
檢查死鎖,沒事
 
    表IX鎖 17    @5
   
二級索引記錄行鎖 REC X WAIT  291 @6
檢查死鎖 沒事
  wait.... suspend.... wait.... suspend....
commit    
  wakeup this trx
將@4的WAIT去掉,成爲35
 
  執行完成,提交  
    執行完成
圖3
 
此時發現,圖3其實與圖1是同樣的,那爲何圖3能夠正常執行完成,而圖1死鎖了呢?
但認真仔細看了以後,發現有很小的地方是不一樣的,圖3中的鎖@4加上的鎖是291(REC & X & WAIT),而圖1中加的鎖比它多了一個NOTGAP的鎖,鎖@6也是同樣的,圖3的事務A在提交而且喚醒了鎖@4以後,它的鎖類型爲REC+X(35),而圖1中的值也是比它多了一個NOTGAP鎖。
 
如今已經基本定位了問題所在,應該是NOTGAP搞的鬼。可是爲何會有差異呢?
此時還須要回到代碼中查看,經過日誌分析,發現2個在執行下面代碼時走了不一樣的路:
=======================================
                 if (prebuilt->select_lock_type != LOCK_NONE) {
                                ulint        lock_type;
 
                                 if (!set_also_gap_locks
                                    || srv_locks_unsafe_for_binlog
                                    || trx->isolation_level <= TRX_ISO_READ_COMMITTED
                                    || (unique_search
                                                && !UNIV_UNLIKELY(rec_get_deleted_flag(rec, comp)))) {
                                                  goto  no_gap_lock;//直接路到下面  lock_typ e = LOCK_REC_NOT_GAP;處
                                } else {
                                                lock_type = LOCK_ORDINARY;
                                }
                                 if (index == clust_index
                                    && mode == PAGE_CUR_GE
                                    && direction == 0
                                    && dtuple_get_n_fields_cmp(search_tuple)
                                    == dict_index_get_n_unique(index)
                                    && 0 == cmp_dtuple_rec(search_tuple, rec, offsets)) {
no_gap_lock://標記
                                                lock_type = LOCK_REC_NOT_GAP;
                                }
  =======================================  
這裏關鍵的分叉口就是在上面紅色字體部分,死鎖的時候走了 goto  no_gap_lock,而沒有出現死鎖的時候走的是 lock_type = LOCK_ORDINARY;,而   LOCK_ORDINARY表示的是0,什麼都沒有,因此這2條路的不一樣就是差1024(NOTGAP鎖)。
那麼從日誌中發現,走了第一條路是由於條件 (unique_search  && !UNIV_UNLIKELY(rec_get_deleted_flag(rec, comp))是符合的。 rec_get_deleted_flag函數的做用是判斷這條記錄是否是已經打了刪除標誌。
 
如今豁然明白了,若是當前這條要加鎖的記錄尚未打刪除標誌,則加的鎖是NOTGAP類型的鎖,不然就不設置類型,那說明上面的圖1中事務A仍是有一個細節沒有畫出來,正由於這個細節與事務B發生了交叉,致使了事務B在作的時候尚未打了刪除標記,因此就加了NOTGAP鎖,因此致使後面的死鎖。
而正常狀況下,也就是圖2的測試,由於事務A已經完成了全部的操做,只等待提交,此時確定已經打了刪除標誌,則在加鎖時不會加NOTGAP鎖,因此就不會出現死鎖。
 
哎,用一句同事常說的話:我這下真的瞭然了,原來問題這麼複雜,mysql中的貓膩太多了。
 
那如今分析一下緣由吧:
如今已經肯定問題就是出如今上面代碼的判斷中,在上面代碼的上面還有一段註釋:
 
                                  /* Try to place a lock on the index record; note that delete
                                marked records are a special case in a unique search. If there
                                is a non-delete marked record, then it is enough to lock its
                                existence with LOCK_REC_NOT_GAP. */
 
這說明了加NOTGAP鎖的意圖,說明上面代碼的判斷是專門作的,具體緣由就無從查起了,可是註釋中說這是一種特殊狀況,爲何呢?解決方式是把那2行直接去掉就能夠了(測試過不會出現死鎖了),但這個會不會是解決問題的根本緣由,還要等待官方人員的處理。
 
因此到這裏,把完整的死鎖圖貼上來:
 

事務A 事務B 事務C
開始    
表的IX鎖 17    @1    
二級索引行鎖X REC NOTGAP 1059    @2
檢查死鎖 沒事
   
  表IX鎖 17    @3  
  二級索引記錄行鎖 REC NOTGAP X WAIT 1315    @4
檢查死鎖,沒事
 
    表IX鎖 17    @5
   
二級索引記錄行鎖 REC NOTGAP X WAIT 1315    @6
檢查死鎖 沒事

對二級索引記錄加刪除標誌(這個是最關鍵的)
由於這個事件必需要與事務B有交叉點mysql

這個交叉點就是:在鎖@2與@7之間,有事務B加了鎖@4,事務加了鎖@6算法

   
聚簇索引行鎖X REC NOTGAP 1059    @7    
  wait.... suspend.... wait.... suspend....
commit    
  wakeup this trx
將@4的WAIT去掉,成爲1059
 
 
二級索引記錄行鎖 REC X WAIT 291    @8
檢查死鎖 發現死鎖
 
 

思惟發散:
1. 對於已經刪除的記錄(已經提交,但尚未purge),若是再去作刪除操做,則此時還會加鎖麼?加什麼鎖?(這個問題,因爲時間太緊,後面再給出驗證說明,若是有興趣,本身也能夠作一下的)
2. 這個問題是在隔離級別是可重複讀的狀況下存在的,但若是是其它狀況下會出現麼?
3. 若是是根據主鍵刪除,這個問題還會出現麼?
 
總結:在mysql中,其實不少東西都不能按照常理來想的,這個問題原本在達夢與oracle中是根本不可想象的,根本不會出現的,因此纔有一開始以爲不可能的感受,最後才發現,原來是真的。
 
在這裏感謝一下同事們的幫助與討論,感謝勇哥,傑哥
相關文章
相關標籤/搜索