一个Bug看InnoDB锁机制

一个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锁机制)