美文网首页
一个Bug看InnoDB锁机制

一个Bug看InnoDB锁机制

作者: yqh__ | 来源:发表于2018-04-22 08:34 被阅读0次

    一个Bug引发的死锁

    最近有用户反映,系统时不时会出现报错的现象。登陆生产环境查看日志,发现MySQL出现了死锁。根据报错信息排查,在生产环境发现了如下代码。

    session.query(TblMsg.id).filter(TblMsg.key == key).with_for_update()
    
    obj_msg = TblMsg(
        group_id=group_id,
        key=key,
        sep=sep
    )
    session.add(obj_msg)
    session.commit()
    

    为了使逻辑更加清晰,代码我已经简化过了。经常运行到会话提交事务时,就产生死锁。下面我们就来一步步分析,产生死锁的原因以及解决策略。

    InnoDB行锁的种类

    首先,我们来看看InnoDB中行锁的种类。

    Recodrd Lock: 行锁,每行级别加锁。
    Gap Lock: 间隙锁,锁住一个范围,但并不包括要锁的那个值。
    Next key Lock: 行锁和间隙锁的结合,即锁住范围也锁住要锁的值,是Gap Lock和Next key Lock的结合。

    事务的隔离级别

    提及到锁,那么就一定离不开事务的隔离级别。


    lock1.png

    这里我们着重关注READ COMMITEDREPEATABLE READ,MySQL默认的事务隔离级别为REPEATABLE READ。我们都知道在REPEATABLE READ这个隔离级别下,理论上是会出现幻读的。为了解决这个问题InnoDB引入了间隙锁这个概念。

    Gap Lock

    Gap Lock在RR隔离级别上才会生效,其他事务隔离级别不会出现Gap Lock。间隙锁的出现还和索引有关系。列如update xxx set a=1 where a=2;这个语句。

    lock2.png

    现在主要来看看a为辅助索引的情况,借此来研究Gap Lock的技术细节。首先我们创建一张表。

    create table `test_gap_lock`(
        id unsigned int primary key,
        number int,
        key `key_number` ("number"),
    );
    
    然后在db中插入数据
    INSERT INTO `test_gap_lock`(
      `id`, `number`
    )VALUE
      (1,2),
      (3,4),
      (6,5),
      (8,5),
      (10,5),
      (13,11);
    

    在辅助索引a上存在的next-key lock为(-∞, 2], (2, 4], (4, 5], (5, 11],(11, +∞)
    现在我们启动两个session

    # session 1
    BEGIN ;
    SELECT * from `test_gap_lock` WHERE number=4 FOR UPDATE ;
    
    # session 2
    INSERT INTO `test_gap_lock`(`id`, `number`) VALUE (100, 3); # 阻塞
    INSERT INTO `test_gap_lock`(`id`, `number`) VALUE (5, 5); #阻塞
    INSERT INTO `test_gap_lock`(`id`, `number`) VALUE (7, 5); #成功
    

    现在来解释一下,因为number为辅助索引,现在手动给number为4的值加一把写锁时,会锁住4附近的间隙。即(2, 4]和(4, 5]这个区间。插入number值为3的行,在这个范围内,所以会阻塞住。

    二、三次插入同样的值,第二次插入阻塞,第三次插入成功,则和索引的排列有关。在辅助索引中,如果索引上的值相同,那么则按照聚集索引的顺序进行排列。因为id=5的这次插入在id=3和id=6这两行数据之间,所以被阻塞住。而id=7的插入,不在这个范围内,所以能插入成功。

    解决问题

    有了上面的知识,这个Bug的产生的原因就显而易见了。在并发的条件下,sql的执行顺序可能产生一下的情况。

    # session 1
    session.query(TblMsg.id).filter(TblMsg.key == key).with_for_update()
    
    # session 2
    session.query(TblMsg.id).filter(TblMsg.key == key).with_for_update()
    
    # session 1 阻塞
    session.add(obj_msg)
    session.commit()
    
    # session 2 阻塞
    session.add(obj_msg)
    session.commit()
    

    key为表上的一个二级索引,当手动加锁的时候,锁的性质变为NEXT-KEY Lock。不仅锁住key值,同时也锁住间隙。session 1锁住了相应的间隙,session 2也锁住了相应的间隙。如果这个时候session 1锁住的间隙,正好是session 2要插入的值。session 2锁住的间隙,是session 1要插入的值。就会出现死锁。

    解决问题的办法很简单,有两种策略:
    1、 使用Unique key或主键作为筛选条件,从next-key lock退化为recode lock。
    2、 事务的隔离级别从RR退回到RC,或者手动设置参数关闭gap lock。这种更改最为简单,但可能出现幻读,所以需要确定幻读不会影响业务的正常运行。

    相关文章

      网友评论

          本文标题:一个Bug看InnoDB锁机制

          本文链接:https://www.haomeiwen.com/subject/aceqlftx.html