最近再次碰到之前遇到的断言失败的bug,错误信息如下:
InnoDB: Failing assertion: trx->isolation_level == TRX_ISO_READ_UNCOMMITTED
这是一个已知的bug(bug#62037),在MySQL5.5.22版本中被fix掉,在lauchpad上可以看到具体是如何修复的
如何重现
重现case,使用gdb的non-stop模式很容易重现:
启动gdb,使用non-stop
set target-async 1
set pagination off
set non-stop on
断点:
row0upd.c:2033 (Percona Server5.5.18)
case:
session 1:
CREATE TABLE `t1` ( `a` int(11) DEFAULT NULL, `b` text, `c` text ) ENGINE=InnoDB DEFAULT CHARSET=gbk;
insert into t1 values (1,repeat(‘b’, 7000), repeat(‘c’, 100));
update t1 set c = concat(c, repeat(‘c’, 2000));
这时候会因为更新列c而导致b列的数据被外部存储(b的列长最大),停在断点
session 2:执行查询
select * from t1 where a = 1;
该bug主要包含两个问题:
问题一:运行时断言失败
主要原因是:
1.更新记录时,undo中只记录了那些被更新的列,而由于其他列更新,导致某个列需要外部存储时,这个列的值不会写入undo。
也就是说,在从函数btr_cur_pessimistic_update中返回后,记录上某个未被更新的列,可能存储的是无效的指针
例如上述场景b列被选出来外部存储,因为它的长度最大,因此b列的数据被修改成一个尚未生效的值全为0的指针;
在悲观更新返回后,由于先mtr commit,再更新外部存储数据,这导致block及索引上的排他锁都被释放掉;这时候其他连接的查询是可以看到这条记录的。
2.虽然MVCC保证查询可以看到修改过的列,但1提到的未修改但被选作外部存储的列没有做undo,因此查询看到的是指针,导致触发断言失败,实例crash
解决:
官方的解决办法是在悲观更新和更新完外部存储列后,才进行mtr commit,这可以保证中间不会有其他查询看到未完成的更新(被阻塞住)
问题二:断言失败crash后,无法crash recovery
原因:
如果在完成悲观更新和写入外部存储列的过程中crash,记录更新可能完成了,但外部存储列是失败的,在crash recovery后,记录依然维持在不一致的状态。这样只要一访问该记录,就会报和问题一一样的断言crash错误
解决:
将上述逻辑修改为:
1.对原记录进行修改,mtr不提交(用btr_mtr代表该mtr)
2.扩展新的外部存储页(不能使用btr_mtr之前释放掉的page,暂时没搞明白为什么…),对新的外部存储页的写入(包括初始化)使用另外一个mtr(称为blob_mtr),对记录上指针的更新使用btr_mtr
3.commit blob_mtr
4.commit btr_mtr
这样在crash recovery的时候,就会先恢复blob页,再恢复记录上的操作;最差的情况就是丢失更新,ibd中存在一个有部分数据的无效blob页,但记录本身仍然能保持一致的状态
另外在插入记录时也可能触发该bug,例如,当一条记录被标记删除,但未被purge掉时,如果再次插入该聚集索引记录,被标记删除的记录就会被更新成现在的记录,这时候触发bug的逻辑就和上述Update的逻辑类似了。