在mysql 5.7中发现的一个有关repeatable-read的"诡异"现象
这个"诡异"现象是前几日朋友反馈过来,也确实坑了他们一把。作者想以这个case为切入点,更深入了解事务隔离机制以及读一致性方面的内容,所以昨晚对这个case进行了复盘,现将自己了解到的原因分享一下。(ps:可能会遇到这个坑的朋友,请自查.)
测试案例如下:
环境:数据库版本5.7, autocommit=0, tx_isolation=REPEATABLE-READ
表T:create table t (id int primary key , k int );
但这个朋友,在他们测试环境中,得到的结果是3,在生产环境中,得到的结果是2。因为他们之前认为在测试环境得到的结果是3是正确的,也是理所当然的,认为生产也应该是这样的。然而生产环境,得到的结果不是3,而是2,然后被坑了。(PS: 生产环境跟测试环境的事务隔离级别一样,都是REPEATABLE-READ)
这位朋友非常聪明,不是专业的DBA,但也很快意识到测试环境跟生产环境的差异。测试环境因为没有做主从复制进行备份,又不想去管binlog清理的事情,所以把binlog关了。但生产环境需要做主从复制,所以binlog是必须开启的,所以怀疑到binlog的开启跟关闭决定了看到k列的值是2还是3. 然后也被他验证了。 因为朋友认为binlog的开/关,不应该影响到事务数据的可见性,所以在测试环境把binlog关了,但却被这样的设置坑了,觉得这是一个“瓜”,让作者分析跟分享一下。
再次明确一下上面案例的最后一个查询的结果:关闭binlog时, 查询结果得到的K列的值是3;开启binlog ,得到的K列的值是2 。为什么会存在这样的差异:
首先我们来解析一下,在开启binlog的情况下, 查询结果为什么是2?而不是3.
回答:作者在分析代码之前,也会认为得到的结果应该是2, 不是3。因为在可重复读的隔离级别下,在同一事物内,会话1第一次查询得到的K列结果是2,后面虽然执行了update语句,尝试将K列修改成3,但会话2已经将k列修改成3,且已经提交,所以会话1并没有去修改该记录,(从返回结果看到“Rows matched: 1 Changed: 0 Warnings: 0”), 也不会产生新的数据版本,所以看到的还是原来的值(会话2的update语句提交的修改结果对会话1不可见), 即2.(吐槽:update语句执行后,马上去查询,居然得到的结果不是update执行后的预期结果,也算是个小“坑”。)
为什么在关闭binlog的情况下,得到的查询结果是3,而不是2?
回答: 因为事务的隔离级别跟上一个情况的隔离级别相同,且update 语句执行后返回的结果也相同, (Rows matched: 1 Changed: 0 Warnings: 0),都没有真正去更新数据,但却看到的是3, 不是2. 上面的解析已无法适用在这个情况上。
按照现有掌握的知识,确实无法解释为什么在关闭binlog的情况下,得到的值是3,而不是2. 所以只有翻代码,来找出其中的差异。
通过跟踪 ReadView::changes_visible函数,发现在binlog关闭的情况下,会话2的update语句提交的将k的值改成3的修改结果对于会话1确实可见了(表面现象), 但真正的原因并不是会话2的update语句提交的修改结果 对于会话1可见(否则,将打破可重复读的一致性的规则),而是会话1执行update 语句的时候,更新了该行了trx_id, 也就是这行的事务id, 因此也就产生了新的数据版本。 自己生成的数据版本,对事务自己是可见的, 所以session 1 看到的k值是3. 虽然返回的结果是“Rows matched: 1 Changed: 0 Warnings: 0”,但实际上,却生成了新的数据版本,session 1 看到的是自己update语句产生的数据版本,而不是session 2 update语句产生的数据版本。 在开启binlog的情况下, session 1 的update语句,在原值跟新值完全相等的情况下, 不会去对该记录做任何更新,也就不会产生新的数据版本, 所以查询时,看到的K列值还是2. 这就是看到的值产生差异的根本原因。
为什么会有这种差异? 分析代码,跟踪到,在binlog开启或者关闭的下,在mysql_update函数中的下面这个判断条件(mysql_update函数所在文件的821行),会产生不一样的值。
当binlog 关闭时,(!records_are_comparable(table) || compare_records(table)) 为ture ,然后会执行ha_update_row函数,会修改行,生产新的数据版本, 当binlog打开时,则为false ,则不会修改行。
而records_are_comparable 函数,会调用 bitmap_is_subset(table->write_set, table->read_set); 函数(如下图), 真正产生差异的是,这个函数。该函数会判断被修改的列是否是被查询的列的子集。 当binlog关闭时,table->write_set为第2列(k列),table->read_set为第一1列(id列)(sql语句为update t set k=3 where id=1). 所以不是子集关系。
(!records_are_comparable(table) || compare_records(table))
翻译这段代码的意义:当update语句的原值跟新值相等(由compare_records(table) 函数判断),且写的列集合(table->write_set)跟查询列集合(table->read_set)的子集时,不需要修改行。 但当binlog 关闭时, 写的列集合(table->write_set)不是查询列集合(table->read_set)的子集, 所以需要update 行。 当开启binlog时,因为要记录binlog, 所以table->write_set 跟table->read_set则会被设置成包含所有列(源代码就是这样操作的,至于原因,猜测跟binlog为row模式时,修改一列也会包含整行有关。),所以table->write_set 跟table->read_set相等(也是子集),因此不会去更新行。
下面是关于当binlog开启时,将table->write_set 跟table->read_set设置成包含所有列的函数。
该函数首先会判断binlog 是否开启,当binlog开启时,且binlog格式为row的时候,会设置成所有列,所以table->write_set 跟table->read_set相等,则不会去修改行。所以不会产生新的数据版本。但binlog 关闭时,则会修改行。所以最后一个查询语句,将看到不同的数据。
从上面这段代码来看,没有涉及到binlog 为statement模式的情况, 怀疑当binlog格式设置为statement模式时,开启binlog会出现跟关闭binlog一样的结果。作者尚未测试binlog 格式为语句模式的时候,会不会跟row模式有差异,有兴趣的朋友可以测试binlog为statement的情况。