MySQL系列:MVCC
1. 什么是多版本并发控制
MVCC(多版本并发控制,Multiversion concurrency control),它是一种在并发时对读写控制的方法。
MVCC 是通过保存数据在某个时间点的快照来实现并发控制的。不管事务执行多长时间,事务内部看到的数据是不受其它事务影响的,根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。
MVCC 使得各个事务在读写数据时能最大程度的降低锁的依赖,在保证事务隔离性的同时,也能让读类型的事务和写操作的事务并发进行。
1.1 MVCC 的思想
通过保存数据的历史版本,通过对数据的多个版本管理来实现数据库的并发控制。这样我们就可以通过比较版本号决定数据是否显示出来,读取数据的时候不需要加锁也可以保证事务的隔离效果。
1.2 MVCC 解决的问题
- 读写之间阻塞的问题
通过 MVCC 可以让读写互相不阻塞,即读不阻塞写,写不阻塞读,这样就可以提升事务并发处理能力 - 降低了死锁的概率
因为 InnoDB 的 MVCC 采用了乐观锁的方式,读取数据时并不需要加锁,对于写操作,也只锁定必要的行 - 解决一致性读的问题
一致性读也被称为快照读,当我们查询数据库在某个时间点的快照时,只能看到这个时间点之前事务提交更新的结果,而不能看到这个时间点之后事务提交的更新结果
1.3 快照读与当前读
快照读(SnapShot Read) 是一种一致性不加锁的读,是InnoDB并发如此之高的核心原因之一。
- 这里一致性指事务读取到的数据,要么是事务开始前就已经存在的数据,要么是事务自身插入或者修改过的数据
快照读
不加锁的简单的 SELECT 都属于快照读,例如:
SELECT * FROM t WHERE id=1;
当前读
当前读就是读取最新数据,而不是历史版本的数据。加锁的 SELECT 就属于当前读,例如:
SELECT * FROM t WHERE id=1 LOCK IN SHARE MODE;
SELECT * FROM t WHERE id=1 FOR UPDATE;
- SELECT ... LOCK IN SHARE MOD:是IS锁(意向共享锁)
- 在符合条件的rows上都加了共享锁,其他session可以读取这些记录,也可以继续添加IS锁,但是无法修改这些记录当前加锁的session执行完成或锁等待超时
- SELECT ... FOR UPDATE :是IX锁(意向排它锁)
- 在符合条件的rows上都加了排它锁,其他session也就无法在这些记录上添加任何的S锁或X锁
2. MVCC 原理
2.1 undo log
undo log 是对事务操作时的一个日志记录,以提供数据回滚功能。
- 事务进行了 insert 操作,则回滚时会逆向解析为 delete
- 事务进行了 delete 操作,则变为 insert 操作,相当于一种逻辑的反向操作
undo log 除了记录下每一次的操作类型、数据情况和事务 ID 外,还包含了上一次的记录指针,以形成完整的历史链路。
MVCC 历史版本数据就来是从 undo log 的记录指针去追溯获取的。
2.2 最近一次的记录指针
每当我们插入一行数据的时候,数据库还会额外的帮我们生成 2 个隐形字段:
- DB_TRX_ID:事务 ID
- DB_ROLL_PTR:回滚记录指针
每次需要追溯历史版本就从 DB_ROLL_PTR 这开始:
关于 MVCC 在 undo log 里所产生的历史版本也不会一直存着,在满足一定条件后则会被标记为清除状态,等待清理。
像 insert 类型的 undo log 则可以在提交事务后就标记为清除状态,因为这是属于新增的,不会有其他事务依赖到。
2.3 Read View
隔离性有四个隔离级别:未提交读、提交读、可重复读、可串行化。事实上 MVCC 只在已提交读和可重复读隔离级别上实现了。
- 未提交读要求最低,只要能获取到数据就返回
- 可串行化天生就把别的请求挡在外面了,不用考虑事务之间的并发执行
在已提交读、可重复读的隔离级别上,MVCC 也会有所不一样的,主要体现在 Read View 的生成上。Read View 是对当时所活跃的事务 id 的维护,包含的字段如下:
- m_ids:当时正在发生的所有事务 id 集合
- m_low_limit_id:当前事务最多能读取到的事务 id,相当于“高水位”警戒线,超过它就不能继续读了
- m_up_limit_id:跟上面相反,属于“低水位”警戒线,所读取的事务 id 都应该比这个值大
- m_creator_trx_id:当前的事务 id
当把这些事务 id 维护到当前事务的 Read View 里后,就可以控制其他事务对当前事务的可见性:
- 比如当前 Read View 的 m_low_limit_id 是 10,那即使后面又有新的事务产生,当前事务也只能读取到这个 id 为 10 的事务为止,毕竟后面的事务是属于新来的
- 比如当前事务需要回滚了,则会根据 undo log 的记录指针以及 Read View 的 m_up_limit_id 去控制回滚
提交读
对于提交读,它在每次 SELECT 的时候都会重新生成 Read View,所以已提交读在同一事务里将有可能读到不一样的提交数据。
可重复读
可重复读只在第一次 SELECT 的时候生成,所以后面读取到的数据都在此处的版本控制内。
3. 总结
MVCC 通过 undo log 的 记录指针获得了一个个的历史版本,就像镜像备份一样,使得数据的读写不必再依赖一份数据,提高了并发执行效率。
不过 undo log 清除线程比较滞后的话,将会导致 undo log 越来越大,影响磁盘操作效率,所以必要的时候可以分配更多的资源给清除线程。