MySQL Repeatable Read 的幻读问题

MySQL Repeatable Read 的幻读问题

Scroll Down

前言

在梳理 MySQL 事物和锁这一块知识的时候,发现其实自己了解的只是冰山一角,经过认真的查阅和研究之后,发现这一块的知识其实还真的有很多的学问。所以后面还是需要去亲自研读一下《高性能MySQL》这本书。

正文

谈到 MYSQL 的事物, 相信大家对这几个概念都不会陌生:

  • 四大特性:ACID
  • 并发问题
    • 脏读
    • 不可重复读
    • 幻读
  • 隔离级别
    • Read Uncommitted(读未提交)
    • Read Committed(读提交)
    • Repeatable Read(可重读)
    • Serializable(可串行化)

MySQL 的默认事物隔离级别是 RR (Repeatable Read) ,可重复读级别是能够解决脏读、不可重复读的这两个事物并发问题的,但是幻读的问题仍会存在,如果使用Serializable的隔离级别,对于高并发的业务来说是不实际的。那么 MySQL 是如何解决幻读这个棘手的问题呢?

没错,MySQL 通过MVCC(多版本并发控制)和Gap Lock(间隙锁)这两个机制解决了幻读的问题~

那么这两种方式具体又是如何实现的呢?

这里我们先讲一下 MySQL 的**"读"的区别**。因为在事物隔离级别中的**"读"**,分为快照读(snapshot read)当前读(current read)

1. 当前读(current read)

select...lock in share mode (共享读锁)
select...for update
update , delete , insert

读取的是最新的数据,并且对读取的记录加锁,阻塞其他事物对当前事物查询数据的修改和插入,从而解决幻读。

实现方式:Next-key Locks(临键锁)

官方对临键锁的定义是:

A next-key lock is a combination of a record lock on the index record and a gap lock on the gap before the index record.

简单来说就是 临键锁 = 行记录锁 + 间隙锁

行锁、间隙锁这个锁的概念,下文会介绍。所以这里我们只要知道的是,对于事物的当前读模式,是通过 Gap-Key-Locks 解决的。

2. 快照读(snapshot read)

select * from table

单纯的 select 操作

Read Committed 隔离级别:每次select都生成一个快照读。

Read Repeatable 隔离级别:开启事务后第一个select语句才是快照读的地方,而不是一开启事务就快照读。

实现方式:undolog 和 MVCC

undolog 用于数据的撤回操作,它记录了修改的反向操作,比如,插入对应删除,修改对应修改为原来的数据,通过undo log 可以实现事务回滚,并且可以根据 undo log 回溯到某个特定的版本的数据。

MVCC 就是上文提到的多版本并发控制。

实现方式

MVCC

MVCC 只是工作在两种事务级别底下:

  • Read Committed
  • Repeatable Read

MVCC 每行记录后面保存三个隐藏的列,其中其主要作用的有两列

DB_TRX_ID: 记录行数据创建时的事物版本号。每开启一个事物,事物版本号都会递增。

DB_ROLL_PTR: 记录行数据何时过期(或者被删除)。指向前一个版本的 undolog 记录,组成 undo 链表。如果更新了行,则撤消日志记录包含在更新行之前重建行内容所需的信息。

  • INSERT

    插入数据时, DB_TRX_ID 记录的是当前新增数据的事物版本号

  • DELETE

    删除数据时,DB_ROLL_PTR 记录的是当前操作删除的事物版本号

  • UPDATE

    1. 更新数据时,插入一条新的数据,DB_TRX_ID 记录的是当前更新数据的事物版本号

    2. 将旧数据的 DB_ROLL_PTR 值更新为当前更新数据的事物版本号

  • SELECT

    查询规则如下:

    1. 查找数据行版本号早于当前事务版本号的数据行记录,也就是说(当前的查询的事物版本号 >= DB_TRX_ID 中的值)

      这样就能确保事物读取的数据,要么是当前事物已经存在的,要么是当前事物所操作过的。

    2. 查找删除版本号要么为NULL,要么大于当前事务版本号的记录。也就是说 (DB_ROLL_PTR IS NULL OR DB_ROLL_PTR > 当前的查询的事物版本号)

      这样确保查询出来的数据行记录在事务开启之前没有被删除。

Record Locks(行锁)

官方对行锁的定义是:

A record lock is a lock on an index record. For example, SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE; prevents any other transaction from inserting, updating, or deleting rows where the value of t.c1 is 10.

简单来说,就是一个排他锁,以阻止其他事务插入,更新,删除。

Gap Locks(间隙锁)

官方对间隙锁的定义是:

A gap lock is a lock on a gap between index records, or a lock on the gap before the first or after the last index record. For example, SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE; prevents other transactions from inserting a value of 15 into column t.c1, whether or not there was already any such value in the column, because the gaps between all existing values in the range are locked.

举一个简单的例子:A、B、C 三个同学排队依次站成一排,此时新来了一个 E 同学,那要如何才能不让 E 同学插队呢。其实把A、B、C三个人的间隙都封锁上,这样子 E 就不能穿插进来了。而这个锁也就可以理解为间隙锁。

当然,间隙锁也是有条件的:

  1. 使用普通索引锁定;
  2. 使用多列唯一索引;
  3. 使用唯一索引锁定多行记录;

Next-key Locks(临键锁)

上文也提到了 临键锁 = 行锁 + 间隙锁,其实也就是它即锁定了当前行,也锁定范围区间。

总结

  1. MySQL 的默认隔离级别 Repeatable Read,RR 级别下会存在幻读问题,但是 MySQL 通过 MVCCGap Locks 解决幻读问题。
  2. MySQL 在事务隔离级别下的读取方式有两种:快照读和当前读
  3. 对于快照读来说,幻读的解决是依赖 MVCC 解决;而对于当前读则依赖于 Gap Locks 解决
  4. Gap Lock 在InnoDB的唯一作用就是防止其他事务的插入操作,以此防止幻读的发生
  5. MVCC 通过版本号 来处理事务,防止加锁,来提高访问效率