InnoDB 中的多版本并发控制(MVCC)
MVCC(Multiversion Concurrency Control):多版本并发控制,是一个数据库设计理论,解决了读写之间阻塞的问题,使得读操作不阻塞写,写操作不阻塞读,它提高了数据库的并发能力。MVCC 是通过快照来实现的,事务启动时会对整个库拍一个快照。
简单来说,MVCC 的思想就是保存数据的历史版本,通过管理数据行的多个版本实现并发控制;MVCC 没有一个统一的实现标准,典型的有乐观(optimistic)锁实现和悲观(pessimistic)锁实现;
MVCC 只在 READ COMMITTED(读取已提交)
和 REPEATABLE READ(可重复读)
两个隔离级别下工作,其他两个隔离级别和 MVCC 不兼容,READ UNCOMMITTED
总是读取最新的数据行,而不是符合当前事务版本的数据行;SERIALIZABLE
则会对所有读取的行都加锁。
可重复读和读提交在 MVCC 里的区别是:
- 在可重复读隔离级别下,只在事务开始时创建读视图,之后事务里的其他查询都用这个读视图;
- 在读提交隔离级别下,每一个语句执行前都会创建一个读视图;
MySQL 的 InnoDB 实现了 MVCC,InnoDB 的 MVCC 实现依赖:隐藏字段、Read View、Undo log。
首先我们来了解一下快照读和当前读:
快照读和当前读
快照读(SnapShot Read):是一种一致性不加锁的简单 SELECT 操作,读取历史版本的数据或自身插入、修改的数据;
一致性读:指的是事务读取到的数据,要么是存在的历史数据,要么是自身插入或修改的数据;
当前读(Current Read):读取到的是最新的数据;更新数据时都是先读后写的,而这个读,只能读取当前的值,所以称为当前读,当前读必须加锁。以下 SQL 语句均使用当前读:
select ... lock in share mode
select ... for update
insert
update
delete
InnoDB 如何存储多个版本的?
首先,我们每开启一个事务,就会获得一个事务 ID,这个 ID 是自增长的,可以通过 ID 知道事务执行的时间顺序;
然后,InnoDB 在每行记录的后面添加了 3 个隐藏字段:
- DB_TRX_ID (6 字节):表示最近一次对本记录行作修改(insert | update)的事务 ID,对于删除(delete)操作,InnoDB 认为是一个 update 操作,并不会真的删除这条记录,而是设置该行的删除标志位 deleted_bit,将行表示为 deleted;
- DB_ROLL_PTR (7 字节):回滚指针,指向这个记录的 Undo log 信息;
- DB_ROW_ID (6 字节):隐藏的单调递增的行 ID,如果表中没有主键或非空的唯一索引时,InnoDB 会用这个 ID 自动生成聚簇索引;这个字段与 MVCC 关系不大;
回滚指针将数据行的所有历史版本通过链表连接起来,每个历史版本都保存了当时的事务 ID,这样我们可以通过遍历链表来找到历史版本;
Read View
读视图,也叫做一致性视图(Consistent Read View),是用来做可见性判断的,InnoDB 为每一个事务创建了一个数组,数组保存了事务启动瞬间,当前正在活跃的事务的 ID,活跃指的是已经启动但未提交。
Read View 的几个重要字段:
trx_ids: 当前系统活跃的事务 ID 数组
low_limit_id: 创建当前 read view 时「已提交事务 ID 的最大值 + 1」
up_limit_id: 创建当前 read view 时「 trx_idx 里的活跃事务 ID 的最小值」
creator_trx_id: 创建当前 read view 的事务版本号;
在事务启动瞬间会创建一个事务视图,一个数据版本对于一个事务视图来说,除了事务自己的更新总是可见(事务 ID = creator_trx_id)以外,可见性有三种情况:
- 数据版本对应的事务 ID >= low_limit_id,即数据版本在事务视图创建之后提交,不可见;
- 数据版本对应的事务 ID < up_limit_id,即数据在事务视图创建之前就已经存在了,可见;
up_limit_id <= 数据版本对应的事务 ID < low_limit_id,此时将事务 ID 与数组 trx_ids 的元素匹配:
- 匹配不到,说明产生 read view 的时候事务已经提交,可见;
- 匹配到了,事务 ID = creator_trx_id,事务自己生成的数据,可见;
- 匹配到了,事务 ID != creator_trx_id,说明产生 read view 的时候事务正在活跃,不可见;
Undo log
Undo log 保存的是事务的相反操作逻辑,当一个事务需要读取记录行时,如果当前记录行不可见,就会顺着 Undo log 链表找到满足其可见性条件的记录行版本;注意:Undo log 并不是真的保存历史数据,而是记录事务的相反操作,当需要回滚时就执行相反操作,从而得到历史数据。
在 InnoDB 里,Undo log 分为以下两类:
- Insert Undo log:事务中 insert 操作产生的 Undo log,只在事务回滚时需要,在事务提交后即可删除;个人理解:因为 insert 是插入了一条新数据,不存在历史版本,所以 Undo log 也就没有存在的必要;
Update Undo log:事务中 update 和 delete 操作产生的 Undo log,不仅在事务回滚时需要,快照读也需要,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被 purge 线程删除;
purge 线程任务是清理 deleted_bit 为 true 的记录;
MVCC 具体实现
- 获取事务的 ID;
- 在事务启动瞬间创建一个事务视图 read view;
- 将事务 ID 与事务视图匹配,判断查询的数据可见性,如果可见,则返回数据;
- 如果不可见,则到回滚日志中找到满足可见性的历史版本数据。
MVCC 解决了哪些问题?
读写之间阻塞的问题:通过 MVCC 可以让读写之间互相不阻塞,即读不阻塞写,写不阻塞读,提高了并发能力;
提高并发的演进思路:
- 普通锁,只能串行执行;
- 读写锁,可以实现读读并发;
- 多版本并发控制,可以实现读写并发。
- 降低了死锁的概率:在 InnoDB 中,MVCC 采用乐观锁的方式实现,读取数据不加锁,写数据时也只锁必要的行,锁减少了,也就降低了死锁发生的概率;
参考资料:
- 数据库基础(四)Innodb MVCC实现原理
- MySQL 45讲 08讲 事务到底是隔离的还是不隔离的
转载自:https://segmentfault.com/a/1190000041979437