唯一键是数据库设计中常用的索引类型,主要用于约束数据,不允许出现重复的键值记录。可以想象,如果唯一键约束失效了,将可能产生可怕的逻辑错误。本文主要讨论下最近mysql爆出来的两个唯一键约束失效导致二级索引corruption的问题。
影响版本:mysql 5.6.21之前,5.6.12之后的版本
上述修复带来了严重的退化,在rc隔离级别下,使用delete + 并发insert冲突键值的场景,将可能触发唯一键失效,我们简单描述下冲突产生的过程:
开启一个session,执行flush tables tbname for export,这会使purge操作停下来;
删除某条记录,其二级索引为uk1, 执行的是标记删除,由于purge被我们人为的停止,因此这条记录不会立刻被清理掉;
插入记录,包含唯一索引记录uk1,由于step 2的记录还在(没被purge),因此需要检查唯一性,在函数<code>row_ins_scan_sec_index_for_duplicate</code>中,根据隔离级别在记录上加s not gap 锁,唯一性检查后提交mtr释放block锁;
和step 3 类似,另外一个session也插入uk1, 同样加上s not gap锁,因为s锁是相容的,因此可以成功加上锁,提交mtr释放block锁;
两个session现在可以进行插入,因为受block x锁限制,插入过程是顺序的。但两次插入都能成功,原因是在做插入锁检查时,会检查相邻记录是否存在与(lock_x
lock_gap
lock_insert_intention)相冲突的锁,而gap 锁和not gap的s锁是不冲突的(参考函数<code>lock_rec_has_to_wait</code>), 因此两次插入都能顺利进行下去。
直接把针对 bug#68021 的补丁给revert了。也就是说,在检查duplicate key时总是加gap类型的s锁(lock_ordinary),这样上述过程的加锁类型可以归纳为:
如上描述,这里会有一定的几率发生死锁,并且死锁信息通常让人无法捉摸,如果你发现两条插入相同唯一键的sql出现在死锁信息里,那有很大的可能是这个问题导致的。
影响版本:mysql 5.1 ~ mysql 5.7全系列版本,上游已确认,尚未fix。
这个问题是最近percona的开发人员alexey发现的,触发条件是一次delete + 并发replace into操作,delete和replace操作相同的唯一键值。
和insert操作不同,通过replace into、load datafile replace、insert…on duplicate执行的sql,在检查唯一建约束时,总是给冲突的记录加lock_ordinary类型的x锁 (而非上例的s锁)。
问题产生的场景如下:
和上例一样,先让purge线程暂时停止下来;
删除包含uk1的记录,由于purge已经停止了,记录会留在物理文件中不会被及时清理掉;
执行replace into,插入一条包含uk1的记录,由于存在标记删除但尚未清理的冲突键值,且当前操作为replace into,因此给记录加lock_ordinary类型的x锁;完成冲突检测后,提交mtr释放block锁;
开启另外一个session执行replace into,同样插入冲突键值uk1,由于step 3 已经加了x锁,因此这里再加x锁产生锁等待,进入等待队列。这时候我们查看innodb_locks表,会发现已经存在两个锁对象了
开启purge线程,purge操作会清理掉之前标记删除的物理记录,然而在step3 和step4上已经在这条记录上加了记录锁,记录被清掉了,对应的锁记录也需要做处理,innodb会尝试将锁继承给下一条记录,我们来看看锁继承的逻辑,调用函数<code>lock_rec_inherit_to_gap</code>:
当满足如下条件时,不会做锁继承:
锁类型为插入意向锁
<code>srv_locks_unsafe_for_binlog</code>打开且锁类型为x锁
锁对应事务的隔离级别小于等于rc且锁类型为x锁
由于当前的隔离级别为rc,并且replace into操作加的是x锁,因此锁没有被相邻记录继承,我们从innodb_locks系统表中也可以发现这一点:
唤醒第二个replace 操作(正在等待x锁),执行插入操作成功;
唤醒第一个replace 操作,由于已经完成duplicate key检测,插入成功。
从上述逻辑可以看出,当purge线程被激活后,记录和记录锁对象都被移除了,purge操作悄悄的破坏了innodb的加锁协议。
我们来看看另外一个在repeatable read 隔离级别下,唯一键“失效”的问题,考虑如下执行序列。
b列是唯一键,session1成功插入一条刚被删除的相同键值,并且能查询出来两条相同键值的记录。看起来似乎是唯一键约束被破坏了,这实际上和innodb的内部实现有关。
在上述序列中,session 2执行删除操作,将唯一键进行标记删除,由于session1 已经开启了一个活跃的视图,根据repeatable-read的可见性原则,session 2所做的数据变更对session 1而言是不可见的,purge线程也无法去物理清理该记录。只要session 1不提交事务,总应该能看到被标记删除的记录(1,2)。
当session 1插入相同唯一键值记录(2,2)时,会检查到文件中存在冲突的唯一建,但修改该唯一键的事务已经提交,因此session 1认为插入记录(2,2)是合法的,完成插入后,唯一索引页上就存在两条物理记录,并且对session 1都是可见的。
这个问题是不是bug很难界定,毕竟他没有违反rr级别下可见性原则,唯一索引数据本身也是完好的,据我所知,postgresql也遵循相同的逻辑。