MySQL死锁分析

死锁场景1:并发插入重复key

场景重现

表结构如下:

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

三个session按顺序执行下面的操作

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发生死锁

原因分析

分析之前,我们要知道:

  1. 隐式锁:事务发现当前记录没有锁竞争,则暂时不加锁,直接操作
  2. 隐式锁转换为显示锁:后续发现锁竞争的事务,会为之前的事务加锁
  3. 发生"duplicate-key"错误,则会在"duplicate index record"上设置共享GAP锁
  4. 删除记录会导致锁继承
  5. mysql表有2个伪记录:infimum表示最小记录,supremum表示最大记录
  6. Session1,Session2,Session3简称:S1,S2,S3。主键为1的记录,简称:row1

基于以上事实,我们梳理下发生死锁的原因

  1. S1插入row1时,由于没有锁竞争,不加锁直接插入

    image.png
  1. S2/S3插入row1时,发现row1上有活动的事务S1,帮S1在row1上加记录锁
image.png
  1. S2/S3插入row1时,发现主键重复, 对row1请求"共享next-key锁"
image.png
  1. "共享next-key锁"与"记录锁"冲突,S2/S2进入等待队列
image.png
  1. S1回滚:删除row1,将row1上的锁继承给supremum,然后授予给S2/S3
image.png
  1. row1被删除后,S2/S3重新定位到infinum,对其下一条记录supremum加"插入意向锁"

​ S2对supremum加"插入意向锁",而S3持有supremum的共享GAP锁

​ S3对supremum加"插入意向锁",而S2持有supremum的共享GAP锁

​ 于是S2与S3发生死锁

image.png

关键代码分析

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

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

1.1 分析入口:row0ins.cc类的row_ins_clust_index_entry_low方法
/**
调用路径是: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(...........){
   //如果发生"duplicate-key"错误
  if (!index->allow_duplicates && n_uniq &&
      (cursor->up_match >= n_uniq || cursor->low_match >= n_uniq)) {
      ............  
     
      // 此方法会请求共享锁,并加入等待锁的队列
      err = row_ins_duplicate_error_in_clust(flags, cursor, entry, thr, &mtr);
      if (err != DB_SUCCESS) {
       err_exit:
        mtr.commit();
        //如果共享锁无法立即获取,直接返回,上层方法会将此线程休眠
        //Session2和Session3无法立即获取共享锁后,会从这里返回后休眠
        goto func_exit;
      }
      ............
  }
  ............ 
  /**
    处理完"duplicate-key"的情况后,开始执行插入流程
    获取了共享锁后的Session2和Session3,会在这个方法里面申请“插入意向锁”,从而发生死锁
    
    后续调用路径:-> btr_cur_ins_lock_and_undo -> lock_rec_insert_check_and_lock
            -> lock_rec_insert_check_and_lock 
            -> rec_lock.add_to_waitq(此方法会检测死锁)
  */
  err = btr_cur_optimistic_insert(flags, cursor, &offsets, &offsets_heap,
                                      entry, &insert_rec, &big_rec, n_ext, thr,
                                      &mtr);
  
  ............ 
}
1.2 请求"共享GAP锁"的相关代码分析
/**
 为重复记录,设置共享锁,如果锁冲突,则加入等待队列
 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(...........)
{
  ...........
  ...........
      //隔离级别>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 加”共享锁“
        // 后续调用路径  -> 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通过此方法请求"共享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行”上是否有和当前锁模式冲突的锁
    const lock_t *wait_for =
        lock_rec_other_has_conflicting(mode, block, heap_no, trx);

    if (wait_for != NULL) {
      //Session1已经获取了row1的记录锁,因此与当前锁模式冲突
      switch (sel_mode) {
        ...........
        case SELECT_ORDINARY:
                    //创建锁对象
          RecLock rec_lock(thr, index, block, heap_no, mode);
          //进入等待队列
          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 请求"插入意向锁"的相关代码分析
dberr_t lock_rec_insert_check_and_lock(..........)  
{
    //获取当前定位记录的下一条记录
  //对应我们的例子,回滚后row1已经被删除
  //因此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上是否有和“插入意向锁”冲突的锁
  //对应我们的例子:S1回滚后,supremum列继承了row1的“共享GAP锁”
  //此时S2/S3定位到的next_rec就是supremum,因此发生“锁冲突”
  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;
        //创建锁,进入等待队列,此方法内部会进行死锁检测
    err = rec_lock.add_to_waitq(wait_for);

    trx->owns_mutex = false;

    trx_mutex_exit(trx);

  } else {
    //没有冲突,不加锁(隐式锁)
    err = DB_SUCCESS;
  }

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

2 S1 回滚流程代码分析

调用路径:trx_rollback_for_mysql —> trx_rollback_to_savepoint_low

static void trx_rollback_to_savepoint_low(...........)
{
    ............
      
    //此方法会执行”锁继承“
    //调用路径:btr_cur_optimistic_delete_func -> lock_rec_inherit_to_gap
    //对应我们的例子:row1删除后,row1的gap锁继承给了supremum
    //由(infimum,row1] 变成了 (infimum,supremum]
    que_run_threads(thr);
  
    .............
  }
  if (savept == NULL) {
    //此方法会执行”锁授予“
    //调用路径:lock_rec_dequeue_from_page —> lock_rec_grant
    //对应我们的例子:将共享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 锁分裂继承与迁移

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • 记录一次比较诡异的mysql死锁日志。系统运行几个月来,就在前几天发生了一次死锁,而且就只发生了一次死锁,整个排查...
    枫叶_huazhe阅读 2,019评论 0 5
  • 涉及死锁的 authorized_user 表的 DDL 死锁日志 根据 MySQL 日志分析出来的涉及死锁的 S...
    Wcy100阅读 6,405评论 7 8
  • 记录一次比较诡异的mysql死锁日志。系统运行几个月来,就在前几天发生了一次死锁,而且就只发生了一次死锁,整个排查...
    枫叶_huazhe阅读 902评论 0 5
  • 1. mysql锁知多少 我们进行insert,update,delete,select会加锁吗,如果加锁,加锁步...
    liwsh阅读 4,976评论 0 4
  • 上杭路 周六和朋友一起出门的好日子。小段时间没有见面的朋友,还有住在一起但出差一周的朋友,三个人饱食一顿之后又因为...
    乐播报阅读 253评论 0 1