MySql在InnoDB下如何解决幻读问题

795 阅读5分钟

问题主干:

1.什么是幻读,幻读在InnoDB下哪种事务隔离级别之下会出现;
2.MySql在InnoDB 如何解决幻读问题的

幻读的定义:

幻读指的是两次查询同一个范围的数据,后一次查询到了前一次没有查询到的数据,就好像出现了幻觉,所以称为幻读。主要关注的是查询条件范围内,数据的增删。

幻读在InnoDB下哪种事务隔离级别之下会出现

事务隔离级别脏读不可重复读幻读
读未提交(read-uncommitted)
不可重复读(read-committed)
可重复读(repeatable-read)
串行化(serializable)

MySql在InnoDB 下如何解决幻读问题的

解决幻读的问题,首先要理解快照读和当前读这两个概念。

快照读: 普通的select语句,不包含for update的和lock in share mode的select语句。
当前读: 读取的是最新的数据,并且需要先获取对应记录的锁,包含以下这些 SQL 类型:

  • select ... lock in share mode 
  • select ... for update
  • 增删改(MySQL默认加锁)

MVCC机制(uodo_log + readView)是否能解决快照读场景下的幻读问题:

什么是MVCC?

MVCC机制的实现就是通过ReadViewundo_log版本链,使得不同的事务会根据数据版本链对比规则读取同一条数据在版本链上的不同版本数据。

MVCC最大的优势:读不加锁,读写不冲突。在读多写少的应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能。Mysql在读已提交和可重复读隔离级别下都实现了MVCC机制。

什么是undo_log版本链?

undo_log版本链是指某一行数据被多个事务依次修改过后,Mysql会保留修改前的数据undo回滚日志,并且用两个隐藏字段trx_id和roll_pointer把这些undo日志串联起来形成一个历史记录版本链,如下图:

image.png

可以理解为一个单链表,表尾的数据为最新的数据,而表头的数据为最古老的数据。

说了版本链我们再来看看ReadView。已提交读和可重复读的区别就在于它们生成ReadView的策略不同, 已提交读的事务中的每一次查询都会生成一个新的ReadView,而可重复读的事务中则不会。

什么是ReadView?

通俗点说,就是在已提交读或者可重复读的隔离级别下,我们执行查询的时候,MYSQL内部会给我们维护一份活跃的事务列表,即begin了但还没有conmmit的事务id,比如ReadView为[30,60],那么说明30之前的事务都是已经提交的,60之后的事务是在当前ReadView生成之后才begin的。

举个例子 ,在已提交读隔离级别下

比如此时有一个事务id为100的事务,修改了name,使得的name等于小明2,但是事务还没提交。则此时的版本链是:

image.png

那此时另一个事务发起了select 语句要查询id为1的记录,那此时生成的ReadView 列表只有[100]。那就去版本链去找了,首先肯定找最近的一条,发现trx_id是100,也就是name为小明2的那条记录,发现在列表内,所以不能访问。这时候就通过指针继续找下一条,name为小明1的记录,发现trx_id是60,小于列表中的最小id,所以可以访问,直接访问结果为小明1。

那这时候我们把事务id为100的事务提交了,并且新建了一个事务id为110也修改id为1的记录,并且不提交事务

image.png

这时候版本链就是

image.png

这时候之前那个select事务又执行了一次查询,要查询id为1的记录。

如果你是已提交读隔离级别,这时候你会得到一个新的ReadView,那你的活动事务列表中的值就变了,变成了[110]。

按照上的说法,你去版本链通过trx_id对比查找到合适的结果就是小明2,因为trx_id=110没有提交事务。

如果你是可重复读隔离级别,这时候你的ReadView还是第一次select时候生成的ReadView,也就是列表的值还是[100]。所以select的结果是小明1。

看到这里,其实已经能看出来,光靠 MVCC 是解决不了幻读问题的。MVCC在已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,假设在前后两次查询操作中间往数据库插入或者删除数据,两次读取的数据结果列表是不同的,这也就刚好是文章最开始时对"幻读“的定义。而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView,可想而知,MVCC也无法解决可重复读隔离级别下的幻读问题。

解决当前读场景下幻读的问题(事务隔离级别:可重复读)

核心机制:Next-Key Lock可以解决幻读的原因。(记录锁+间隙锁)

举个例子,有一个表,id列上有 1,3,4, 并且id列的索引为普通索引

当执行SQL1 select * from order where id < 3 for update 时,mysql会通过Next-Key Lock锁住(1,3]和(3,4]这两个区间。当执行SQL2 select * from order where id>3 for update时,这时sql会阻塞等待,直到SQL1提交事务。这就是Next-Key Lock可以解决幻读的原因。

Next-Key Lock 只存在于可重复读这个事务隔离级别中,也就是只能解决可重复读下的幻读问题。

MySql InnoDB 可重复读事务隔离级别下其他类型的锁

记录锁(Record Locks)

记录锁就是为某行记录加锁,它封锁该行的索引记录

-- id 列为主键列或唯一索引列
SELECT * FROM table WHERE id = 1 FOR UPDATE;

id 为 1 的记录行会被锁住。

需要注意的是:id 列必须为唯一索引列主键列,否则上述语句加的锁就会变成临键锁

同时查询语句必须为精准匹配=),不能为 ><like等,否则也会退化成临键锁(感谢评论区 @decodes 提醒)。

在通过 主键索引唯一索引 对数据行进行 UPDATE 操作时,也会对该行数据加记录锁

-- id 列为主键列或唯一索引列
UPDATE SET age = 50 WHERE id = 1;

间隙锁(Gap Locks)

间隙锁基于非唯一索引,它锁定一段范围内的索引记录。:使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据

SELECT * FROM table WHERE id BETWEN 1 AND 10 FOR UPDATE;

即所有在(1,10)区间内的记录行都会被锁住,所有id 为 2、3、4、5、6、7、8、9 的数据行的插入会被阻塞,但是 1 和 10 两条记录行并不会被锁住。

参考:
juejin.cn/post/698888…
cloud.tencent.com/developer/a…
blog.csdn.net/weixin_4213…
juejin.cn/post/684490…
www.cnblogs.com/chenhh-for-…
juejin.cn/post/685003…