前言
在上一篇文章中,我们知道了可重复读的隔离级别采用 MVCC (multi-version concurrency control——多版本并发控制) 机制实现较高的隔离性,确保事务之间的隔离性和一致性。另外,MySQL 在读已提交的隔离级别下也实现了MVCC 机制。
那么什么是MVCC?又该如何实现MVCC?
MVCC 简介
MySQL 中的 MVCC(Multi-Version Concurrency Control)机制是一种并发控制机制,用于实现在数据库系统中对读写操作的并发控制,但这种并发机制不依靠加锁实现,是基于乐观锁实现的无锁并发机制。
MVCC 机制的主要思想是为每个事务生成一个单独的数据快照,这个快照包含了事务开始时数据库的所有数据。当事务对数据进行更新时,会在磁盘上创建新的数据版本,而不是直接修改原有数据,同时在事务提交时,存储引擎会为每一条数据记录将事务的版本信息记录下来。
MVCC 实现
MVCC 机制的实现主要依赖于以下三个重要的元素:
- Undo Log:用于存储事务对数据进行修改之前的数据版本,当事务回滚或发生了并发冲突时,可以利用 Undo Log 来恢复数据。
- Read View:用于事务读取数据时确定可见的数据版本,当事务开始时会生成一个 Read View,其包含了事务的启动时间戳,用于判断数据是否对事务是可见的。
- 版本链:MySQL 通过实现每条数据的多个版本(undo 日志版本),将事务对数据的更新以及版本信息按链表的方式进行存储,这样能够保证并发事务之间的读写操作不会相互影响。
小鱼先给各位同学讲解下版本链和一致性视图(read-view),先建立一个基本认识。
- 版本链,也可以被称为undo 日志版本链,一行数据在被事务修改后,MySQL 会将修改前的数据保存在undo 日志中,并使用两个隐藏字段(trx_id【事务id】、roll_pointer【滚动指针】)将这些记录按事务提交时间顺序串联起来,形成该行数据的历史版本链。
- 一致性视图(read-view)
- 在可重复读隔离级别下,当事务开启并**执行查询语句**时,MySQL 会生成当前事务的一致性视图,并且**在事务结束之前,该视图不会变化**。在事务中任何查询结果都需要从相应的版本链中的最新数据开始逐条与read-view 进行对比,从而得到最终的快照结果。
- 在**读已提交**隔离级别下,每次查询都会生成新的一致性视图。
一致性视图组成:由执行查询时所有未提交事务id 的数组和最大的事务id 组成,其中数组里事务 id 最小的为 min_id,最大的事务 id 为max_id。
一致性视图实例
实例数据表以 MySQL事务(一)MySQL事务隔离级别、锁机制 为例。
隔离级别默认为可重复读隔离级别。
客户端A:
begin; -- 事务开启,但未生成事务id
update mylock set name = 'a1' where id=1; -- 修改后会写入undo log,生成事务id。
-- trx_id=100
客户端B:
begin;
update mylock set name = 'b1' where id=2;
-- trx_id=200
客户端C:
begin;
update userlock set name = 'lilei10' where id=1;
-- trx_id=300
commit;
客服端D:事务D 第一查询,生成事务D 的一致性视图(read view)
begin;
select * from userlock where id=1; -- 查询不生成事务id
-- read view:[100, 200](未提交的事务id数组), 300(max_id)
-- +----+---------+---------+
-- | id | name | balance |
-- +----+---------+---------+
-- | 1 | lilei10 | 200 |
-- +----+---------+---------+
客户端E:只开启事务,暂不操作
begin;
客户端A:
update userlock set name='lilei1' where id=1;
update userlock set name='lilei2' where id=1;
客户端D:事务D 的第二次查询,可以看到一致性视图不更改。
select * from userlock where id=1;
-- read view:[100, 200](未提交的事务id数组), 300(max_id)
-- +----+---------+---------+
-- | id | name | balance |
-- +----+---------+---------+
-- | 1 | lilei10 | 200 |
-- +----+---------+---------+
客户端A:
commit;
客户端B:
update userlock set name='lilei3' where id=1;
update userlock set name='lilei4' where id=1;
客户端D:事务 D 的第三次查询,同样可以看到一致性视图不更改。
select * from userlock where id=1;
-- read view:[100, 200](未提交的事务id数组), 300(max_id)
-- +----+---------+---------+
-- | id | name | balance |
-- +----+---------+---------+
-- | 1 | lilei10 | 200 |
-- +----+---------+---------+
客户端B:
commit;
客户端E:事务 E 第一次查询,生成事务E 的一致性视图(read view)
select * from userlock where id=1;
-- read view:[](非提交的事务id数组), 300(max_id)
-- +----+--------+---------+
-- | id | name | balance |
-- +----+--------+---------+
-- | 1 | lilei4 | 200 |
-- +----+--------+---------+
版本链实例如下图所示:
小结
版本链对比的规则:
- 如果 row 的 trx_id 落在【trx_id < min_id】区间,表示 trx_id 这个版本是已提交的事务生成的,所以该版本的数据可见。
- 如果 row 的 trx_id 落在【trx_id > max_id】区间,表示 trx_id 版本是由之后启动的事务(客户端D 执行查询语句之后)生成的,所以该版本数据不可见。(若 row 的 trx_id 就是当前自己的事务是可见的)。
- 如果 row 的 trx_id 落在【min_id <= trx_id <= max_id】区间,这种情况可能存在两种数据。 - 若 row 的 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,该版本数据不可见。(若 row 的 trx_id 就是当前自己的事务是可见的) - 若 row 的 trx_id 不在视图数组中,表示这个版本是已经提交了的事务生成的,该版本数据可见。
对于删除数据的情况,我们可以将删除语句看出特殊的修改(update),同样会复制一份版本链的最新数据,并将trx_id 修改为删除操作的trx_id,同时需要在本条记录的头信息(record header)中将删除标志(deleted_flag)设置为true,表示当前记录已经被删除。在查询时也是按照上述规则进行,只是如果头信息中的删除标志(deleted_id)为true 时,不返回数据。
特别注意:
begin
和start transaction
命令执行后,trx_id不会立即生成,在执行第一个修改操作时,事务才真正开始,此时生成trx_id,trx_id 由mysql 按照事务的启动顺序分配。