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有交叉點

這個交叉點就是:在鎖@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中是根本不可想象的,根本不會出現的,所以才有一開始覺得不可能的感覺,最后才發現,原來是真的。
 
在這里感謝一下同事們的幫助與討論,感謝勇哥,傑哥


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM