MySQL死鎖分析

死鎖場景1:并發(fā)插入重復(fù)key

場景重現(xiàn)

表結(jié)構(gòu)如下:

CREATE TABLE t1 (i INT, PRIMARY KEY (i)) ENGINE = InnoDB;

三個(gè)session按順序執(zhí)行下面的操作

Session 1:

START TRANSACTION;
INSERT INTO t1 VALUES(1);

Session 2:

START TRANSACTION;
INSERT INTO t1 VALUES(1);

Session 3:

 START TRANSACTION;
INSERT INTO t1 VALUES(1);

Session 1:

ROLLBACK; 

然后Session2 和 Session3發(fā)生死鎖

原因分析

分析之前,我們要知道:

  1. 隱式鎖:事務(wù)發(fā)現(xiàn)當(dāng)前記錄沒有鎖競爭,則暫時(shí)不加鎖,直接操作
  2. 隱式鎖轉(zhuǎn)換為顯示鎖:后續(xù)發(fā)現(xiàn)鎖競爭的事務(wù),會(huì)為之前的事務(wù)加鎖
  3. 發(fā)生"duplicate-key"錯(cuò)誤,則會(huì)在"duplicate index record"上設(shè)置共享GAP鎖
  4. 刪除記錄會(huì)導(dǎo)致鎖繼承
  5. mysql表有2個(gè)偽記錄:infimum表示最小記錄,supremum表示最大記錄
  6. Session1,Session2,Session3簡稱:S1,S2,S3。主鍵為1的記錄,簡稱:row1

基于以上事實(shí),我們梳理下發(fā)生死鎖的原因

  1. S1插入row1時(shí),由于沒有鎖競爭,不加鎖直接插入

    image.png
  1. S2/S3插入row1時(shí),發(fā)現(xiàn)row1上有活動(dòng)的事務(wù)S1,幫S1在row1上加記錄鎖
image.png
  1. S2/S3插入row1時(shí),發(fā)現(xiàn)主鍵重復(fù), 對(duì)row1請(qǐng)求"共享next-key鎖"
image.png
  1. "共享next-key鎖"與"記錄鎖"沖突,S2/S2進(jìn)入等待隊(duì)列
image.png
  1. S1回滾:刪除row1,將row1上的鎖繼承給supremum,然后授予給S2/S3
image.png
  1. row1被刪除后,S2/S3重新定位到infinum,對(duì)其下一條記錄supremum加"插入意向鎖"

? S2對(duì)supremum加"插入意向鎖",而S3持有supremum的共享GAP鎖

? S3對(duì)supremum加"插入意向鎖",而S2持有supremum的共享GAP鎖

? 于是S2與S3發(fā)生死鎖

image.png

關(guān)鍵代碼分析

"............"表示省略部分代碼

1 S2/S3 插入流程代碼分析

1.1 分析入口:row0ins.cc類的row_ins_clust_index_entry_low方法
/**
調(diào)用路徑是:row_insert_for_mysql -> row_insert_for_mysql_using_ins_graph
          -> row_ins_step -> row_ins -> row_ins_index_entry_step
          -> row_ins_index_entry -> row_ins_clust_index_entry
          -> row_ins_clust_index_entry_low
*/
dberr_t row_ins_clust_index_entry_low(...........){
   //如果發(fā)生"duplicate-key"錯(cuò)誤
  if (!index->allow_duplicates && n_uniq &&
      (cursor->up_match >= n_uniq || cursor->low_match >= n_uniq)) {
      ............  
     
      // 此方法會(huì)請(qǐng)求共享鎖,并加入等待鎖的隊(duì)列
      err = row_ins_duplicate_error_in_clust(flags, cursor, entry, thr, &mtr);
      if (err != DB_SUCCESS) {
       err_exit:
        mtr.commit();
        //如果共享鎖無法立即獲取,直接返回,上層方法會(huì)將此線程休眠
        //Session2和Session3無法立即獲取共享鎖后,會(huì)從這里返回后休眠
        goto func_exit;
      }
      ............
  }
  ............ 
  /**
    處理完"duplicate-key"的情況后,開始執(zhí)行插入流程
    獲取了共享鎖后的Session2和Session3,會(huì)在這個(gè)方法里面申請(qǐng)“插入意向鎖”,從而發(fā)生死鎖
    
    后續(xù)調(diào)用路徑:-> btr_cur_ins_lock_and_undo -> lock_rec_insert_check_and_lock
            -> lock_rec_insert_check_and_lock 
            -> rec_lock.add_to_waitq(此方法會(huì)檢測死鎖)
  */
  err = btr_cur_optimistic_insert(flags, cursor, &offsets, &offsets_heap,
                                      entry, &insert_rec, &big_rec, n_ext, thr,
                                      &mtr);
  
  ............ 
}
1.2 請(qǐng)求"共享GAP鎖"的相關(guān)代碼分析
/**
 為重復(fù)記錄,設(shè)置共享鎖,如果鎖沖突,則加入等待隊(duì)列
 Checks if a unique key violation error would occur at an index entry
 insert. Sets shared locks on possible duplicate records. Works only
 for a clustered index!
 
 */
static  row_ins_duplicate_error_in_clust(...........)
{
  ...........
  ...........
      //隔離級(jí)別>RC 且 不是特殊表,則加LOCK_ORDINARY鎖(LOCK_ORDINARY就是next-key鎖)
      lock_type = ((trx->isolation_level <= TRX_ISO_READ_COMMITTED) ||
                   (cursor->index->table->skip_gap_locks()))
                      ? LOCK_REC_NOT_GAP
                      : LOCK_ORDINARY;

      /* We set a lock on the possible duplicate: this
      is needed in logical logging of MySQL to make
      sure that in roll-forward we get the same duplicate
      errors as in original execution */

      if (flags & BTR_NO_LOCKING_FLAG) {
        /* Do nothing if no-locking is set */
        err = DB_SUCCESS;
      } else if (trx->duplicates) {
        /* If the SQL-query will update or replace
        duplicate key we will take X-lock for
        duplicates ( REPLACE, LOAD DATAFILE REPLACE,
        INSERT ON DUPLICATE KEY UPDATE). */
        
                // INSERT ON DUPLICATE KEY UPDATE 等語句需要加”排他鎖“
        err =
            row_ins_set_exclusive_rec_lock(lock_type, btr_cur_get_block(cursor),
                                           rec, cursor->index, offsets, thr);
      } else {
        // 普通insert 加”共享鎖“
        // 后續(xù)調(diào)用路徑  -> lock_clust_rec_read_check_and_lock
        //             -> lock_rec_lock -> lock_rec_lock_slow -> rec_lock.add_to_waitq
        err = row_ins_set_shared_rec_lock(lock_type, btr_cur_get_block(cursor),
                                          rec, cursor->index, offsets, thr);
      }
  
  ...........
  ...........
 
}
// S2/S3通過此方法請(qǐng)求"共享GAP鎖"
static dberr_t lock_rec_lock_slow(...........) {
  ...........
    
  if (lock_rec_has_expl(mode, block, heap_no, trx)) {
    /* The trx already has a strong enough lock on rec: do
    nothing */
    err = DB_SUCCESS;
  } else {
    //判斷“mysql行”上是否有和當(dāng)前鎖模式?jīng)_突的鎖
    const lock_t *wait_for =
        lock_rec_other_has_conflicting(mode, block, heap_no, trx);

    if (wait_for != NULL) {
      //Session1已經(jīng)獲取了row1的記錄鎖,因此與當(dāng)前鎖模式?jīng)_突
      switch (sel_mode) {
        ...........
        case SELECT_ORDINARY:
                    //創(chuàng)建鎖對(duì)象
          RecLock rec_lock(thr, index, block, heap_no, mode);
          //進(jìn)入等待隊(duì)列
          err = rec_lock.add_to_waitq(wait_for);
          break;
      }
    }else if (!impl) {
            //顯示鎖
      lock_rec_add_to_queue(LOCK_REC | mode, block, heap_no, index, trx);
      err = DB_SUCCESS_LOCKED_REC;
    } else {
      //隱式鎖
      err = DB_SUCCESS;
    }
    ............
    ............
}
1.3 請(qǐng)求"插入意向鎖"的相關(guān)代碼分析
dberr_t lock_rec_insert_check_and_lock(..........)  
{
    //獲取當(dāng)前定位記錄的下一條記錄
  //對(duì)應(yīng)我們的例子,回滾后row1已經(jīng)被刪除
  //因此s2/s3重新定位獲取的:rec就是infimum,  next_rec就是supremum
  const rec_t *next_rec = page_rec_get_next_const(rec);
    //next_rec的heap_no
  ulint heap_no = page_rec_get_heap_no(next_rec);

    ...........
    
  //獲取的鎖模式:插入意向鎖
  const ulint type_mode = LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION;
  //判斷next_rec上是否有和“插入意向鎖”沖突的鎖
  //對(duì)應(yīng)我們的例子:S1回滾后,supremum列繼承了row1的“共享GAP鎖”
  //此時(shí)S2/S3定位到的next_rec就是supremum,因此發(fā)生“鎖沖突”
  const lock_t *wait_for =
      lock_rec_other_has_conflicting(type_mode, block, heap_no, trx);

  if (wait_for != NULL) {
    RecLock rec_lock(thr, index, block, heap_no, type_mode);

    trx_mutex_enter(trx);

    trx->owns_mutex = true;
        //創(chuàng)建鎖,進(jìn)入等待隊(duì)列,此方法內(nèi)部會(huì)進(jìn)行死鎖檢測
    err = rec_lock.add_to_waitq(wait_for);

    trx->owns_mutex = false;

    trx_mutex_exit(trx);

  } else {
    //沒有沖突,不加鎖(隱式鎖)
    err = DB_SUCCESS;
  }

  ...........
}

2 S1 回滾流程代碼分析

調(diào)用路徑:trx_rollback_for_mysql —> trx_rollback_to_savepoint_low

static void trx_rollback_to_savepoint_low(...........)
{
    ............
      
    //此方法會(huì)執(zhí)行”鎖繼承“
    //調(diào)用路徑:btr_cur_optimistic_delete_func -> lock_rec_inherit_to_gap
    //對(duì)應(yīng)我們的例子:row1刪除后,row1的gap鎖繼承給了supremum
    //由(infimum,row1] 變成了 (infimum,supremum]
    que_run_threads(thr);
  
    .............
  }
  if (savept == NULL) {
    //此方法會(huì)執(zhí)行”鎖授予“
    //調(diào)用路徑:lock_rec_dequeue_from_page —> lock_rec_grant
    //對(duì)應(yīng)我們的例子:將共享GAP鎖(infimum,supremum],授予給了S2/S3
    trx_rollback_finish(trx);
    MONITOR_INC(MONITOR_TRX_ROLLBACK);
  } else {
    trx->lock.que_state = TRX_QUE_RUNNING;
    MONITOR_INC(MONITOR_TRX_ROLLBACK_SAVEPOINT);
  }
  ............
}

參考

死鎖分析

MySQL · 特性分析 · innodb 鎖分裂繼承與遷移

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 記錄一次比較詭異的mysql死鎖日志。系統(tǒng)運(yùn)行幾個(gè)月來,就在前幾天發(fā)生了一次死鎖,而且就只發(fā)生了一次死鎖,整個(gè)排查...
    楓葉_huazhe閱讀 2,398評(píng)論 0 5
  • 涉及死鎖的 authorized_user 表的 DDL 死鎖日志 根據(jù) MySQL 日志分析出來的涉及死鎖的 S...
    Wcy100閱讀 6,588評(píng)論 7 8
  • 記錄一次比較詭異的mysql死鎖日志。系統(tǒng)運(yùn)行幾個(gè)月來,就在前幾天發(fā)生了一次死鎖,而且就只發(fā)生了一次死鎖,整個(gè)排查...
    楓葉_huazhe閱讀 1,038評(píng)論 0 5
  • 1. mysql鎖知多少 我們進(jìn)行insert,update,delete,select會(huì)加鎖嗎,如果加鎖,加鎖步...
    liwsh閱讀 5,205評(píng)論 0 4
  • 上杭路 周六和朋友一起出門的好日子。小段時(shí)間沒有見面的朋友,還有住在一起但出差一周的朋友,三個(gè)人飽食一頓之后又因?yàn)?..
    樂播報(bào)閱讀 293評(píng)論 0 1

友情鏈接更多精彩內(nèi)容