前提:undo log(原子性) and redo log(持久性)
此前的文章中,我们介绍了 mysql 事务隔离级别,其中非常粗略的介绍了 MVCC:
mysql 锁机制与四种隔离级别
MVCC 全称是 multiversion concurrency control,即多版本并发控制,是 innodb 实现事务并发与回滚的重要功能。
具体的实现是,在数据库的每一行中,添加额外的三个字段:
DB_TRX_ID – 记录插入或更新该行的最后一个事务的事务 ID
DB_ROLL_PTR – 指向改行对应的 undolog 的指针
DB_ROW_ID – 单调递增的行 ID,他就是 AUTO_INCREMENT 的主键 ID
快照读与当前读
innodb 拥有一个自增的全局事务 ID,每当一个事务开启,在事务中都会记录当前事务的唯一 id,而全局事务 ID 会随着新事务的创建而增长。
同时,新事务创建时,事务系统会将当前未提交的所有事务 ID 组成的数组传递给这个新事务,本文的下面段落我们成这个数组为 TRX_ID 集合。
快照读
正如我们前面介绍的,每当一个事务更新一条数据时,都会在写入对应 undo log 后将这行记录的隐藏字段 DB_TRX_ID 更新为当前事务的事务 ID,用来表明最新更新该数据的事务是该事务。
当另一个事务去 select 数据时,读到该行数据的 DB_TRX_ID 不为空并且 DB_TRX_ID 与当前事务的事务 ID 是不同的,这就说明这一行数据是另一个事务修改并提交的。
那么,这行数据究竟是在当前事务开启前提交的还是在当前事务开启后提交的呢?
如上图所示,有了上文提到的 TRX_ID 集合,就很容易判断这个问题了,如果这一行数据的 DB_TRX_ID 在 TRX_ID 集合中或大于当前事务的事务 ID,那么就说明这行数据是在当前事务开启后提交的,否则说明这行数据是在当前事务开启前提交的。
对于当前事务开启后提交的数据,当前事务需要通过隐藏的 DB_ROLL_PTR 字段找到 undo log,然后进行逻辑上的回溯才能拿到事务开启时的原数据。
这个通过 undo log + 数据行获取到事务开启时的原始数据的过程就是“快照读”。
当前读
很多时候,我们在读取数据库时,需要读取的是行的当前数据,而不需要通过 undo log 回溯到事务开启前的数据状态,主要包含以下操作:
insert
update
select … lock in share mode
select … for update
MVCC 与不可重复读、幻读的问题
不可重复读与幻读
“不可重复读”与“幻读”是两个数据库常见的极易混淆的问题。
不可重复读指的是,在一个事务开启过程中,当前事务读取到了另一事务提交的修改。
幻读则指的是,在一个事务开启过程中,读取到另一个事务提交导致的数据条目的新增或删除。
可重复读解决不可重复读与幻读问题的原理
那么,可重复读的隔离级别是否解决了不可重复读与幻读问题呢?
上面我们提到,对于正常的 select 查询 innodb 实际上进行的是快照读,即通过判断读取到的行的 DB_TRX_ID 与 DB_ROLL_PTR 字段指向的 undo log 回溯到事务开启前或当前事务最后一次更新的数据版本,从而在这样的场景下避免了可重复读与幻读的问题。
针对已存在的数据,insert 和 update 操作虽然是进行当前读,但 insert 与 update 操作后,该行的最新修改事务 ID 为当前事务 ID,因此读到的值仍然是当前事务所修改的数据,不会产生不可重复读的问题。
但如果当前事务更新到了其他事务新插入并提交了的数据,这就会造成该行数据的 DB_TRX_ID 被更新为当前事务 ID,此后即便进行快照读,依然会查出该行数据,产生幻读(其他事务插入或删除但未提交该行数据的情况下会锁定该行,造成当前事务对该行的更新操作被阻塞,所以这种情况不会产生幻读问题,有关事务间的锁,不在本篇文章的讨论范围内,接下来的文章我们会进一步讨论)