MySQL之MVCC

背景

在前面的MySQL锁文章中有提到过,MyISAM表的读操作与写操作,以及写操作之间是串行的。

并且默认情况下写操作命令的执行优先于读操作执行,即使读请求早于写请求到达,写锁也会插队到读请求前面,因为MySQL认为写请求一般比读请求要重要。

InnoDB中,就是通过多版本并发控制(MVCC)来解决读请求和写请求的并发问题,提高数据库的并发能力。并且在InnoDB中是默认读不加锁,读写不冲突的。

两个概念

快照读

不加锁的select操作就是快照读,即不加锁的非阻塞读。当我们执行select xxx from table_name where xxx=yyy语句时,就是快照读。

不是所有的隔离级别下都支持快照读。

  • 读未提交: 不支持,未提交读总是读取最新的数据行,而不是符合当前事务版本的数据行
  • 读已提交:支持,每次select都生成一个快照读
  • 可重复读:支持,开启事务后执行第一个select语句才生成快照读,而不是一开启事务就生成快照读, 并且整个事务只生成一个快照读
  • 串行化:不支持,在此隔离级别下,读请求都是当前读。

当前读

读取的是最新版本, 并且对读取的记录加锁, 阻塞其他事务同时改动相同记录,避免出现安全问题。

可执行以下语句来进行当前读:

1
select ... for update

或者

1
select ... lock in share mode

除此之外,还有隐式的当前读,如执行insert/update/delete语句时,这些语句会获取排他锁,自然保证了数据是最新的。

ps: 这里获取的排他锁,其实就是InnoDB中的next-key lock(即record lock + gap lock)

实现

隐藏列

MySQL表中除了我们创建的字段列以外,还有一些隐藏列字段,正是通过一些隐藏列字段来实现MVCC

DB_TRX_ID

最近修改(修改/插入)事务id(6字节):记录创建这条记录/最后一次修改该记录的事务id

DB_ROLL_PTR

回滚指针(7字节):指向这条记录的上一个版本(存储于rollback segment里)

DB_ROW_ID

隐藏主键(6字节):隐含的自增id,如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引

Undo Log 版本链

【数据库】MySQL的ReadView

undo log 版本链是基于 undo log 实现的。undo log 中主要保存了数据的基本信息,比如说日志开始的位置、结束的位置,主键的长度、表id,日志编号、日志类型。

此外,undo log 还包含两个隐藏字段 trx_idroll_pointertrx_id 表示当前这个事务的 idMySQL 会为每个事务分配一个 id,这个 id 是递增的。roll_pointer 是一个指针,指向这个事务之前的 undo log

示例

执行:

1
INSERT INTO student VALUES (1, '张三');

继续执行:

1
UPDATE student SET name='李四' WHERE id=1;

继续执行:

1
UPDATE student SET name='王五' WHERE id=1;

如此,数据行的每一次变化都会记录在undo log中。为了保证事务并发操作时,在写各自的undo log时不产生冲突,InnoDB采用回滚段的方式来维护undo log的并发写入和持久化。回滚段实际上是一种undo文件组织方式。

psselect操作不会记录到undo log中,因为并没有改变数据行。

Read View(读视图)

由上面的Undo Log版本链,我们知道在MySQL中同一行数据会有多个版本,那在事务进行数据读取的时候,怎么判断数据具体应该读取哪一个版本呢?

在执行当前读时,明显我们是要读取数据行的最新版本,这个是确定的。

当执行快照读时,我们可以根据当前事务的事务id来判断,具体该读取数据行的哪一个版本。

ps:每个事务开启时,都会被分配一个id,这个id是默认递增的,所以事务越新,id越大

MySQL执行快照读时,会生成数据库系统当前的一个快照,根据事务状态或者事务id,大概有以下几类信息

  • 已提交事务列表
  • 未提交事务列表(活跃事务)
  • 还未分配事务id(最大活跃事务id + 1)

当前事务id可见的数据,是根据事务id来进行判断的。

大于最大事务id,不可见
在活跃事务id集合中的,不可见

小于最小的活跃事务id的,可见
事务id为本事务id的,可见

总结

Read View就是协助MVCC来实现并发读写时,保证事务能正确读取数据行信息。

可重复读的实现

可重复读的隔离级别下,我们在事务中,多次读取数据,前后总是一致的。这个就是依赖MVCC来实现的。

在可重复读的隔离级别下,事务中第一个执行select语句的时刻,数据库才会生成一个Read View,事务后续的快照读都会使用这个Read View来读取数据。

MVCC解决幻读了吗

MySQL(九):MVCC能否解决幻读问题

幻读:

事务A执行多次读取操作过程中,由于在事务提交之前,事务B(insert/delete/update)写入了一些符合事务A的查询条件的记录,导致事务A在之后的查询结果与之前的结果不一致,这种情况称之为幻读

可重复读的隔离级别下

快照读时,事务读取数据都是通过Read View来读取的,并且事务中第一个执行select语句的时刻,数据库才会生成一个Read View,事务后续的快照读都会使用这个Read View来读取数据。

这样并不会读取到其他事务提交的数据行,保证了多次读取数据的结果是一致的,所以解决了幻读的问题。

当前读时,事务总是会读取数据行的最新版本,在不做其他限制的情况下,MVCC并不会阻止其他事务进行数据行的插入,所以不能解决幻读的问题。

结论

MVCC在快照读的情况下可以解决幻读问题,但是在当前读的情况下不能解决,需要配合间隙锁才能解决

可重复读隔离级别一定不会发生幻读吗?

不一定,在事务中交叉使用 当前读快照读 时,仍然会发生幻读问题。

情况一:

  1. 事务A先查询,此时数据库没有记录a
  2. 事务B插入记录a,并提交事务
  3. 事务A更新不存在的记录a
  4. 然后事务A就可以看到事务B提交的记录a了,即出现了幻读

情况二:

  1. 事务A执行快照读,查询记录行数
  2. 事务B插入一条数据,并提交事务
  3. 事务A执行当前读,查询记录行数,行数不一致,即出现了幻读