MySQL的多版本并发控制(MVCC).
一、快照读与当前读
快照读(SnapShot Read) 是一种一致性不加锁的读,是 InnoDB 并发如此之高的核心原因之一。
在 READ COMMITTED 事务隔离级别下,一致性不加锁的读是指,总是读取被锁定行的最新一份快照数据,因此其它事务修改了该行数据,该事务也能读取到,这也贴合了 RC 隔离级别下允许不可重复读的问题;
在 REPEATABLE READ 事务隔离级别下,一致性不加锁的读是指,事务读取到的数据,要么是事务开始前就已经存在的数据,要么是事务自身插入或者修改过的数据。(下面将以此隔离级别说明);
不加锁的简单的 SELECT 都属于快照读,例如:
SELECT * FROM t WHERE id=1;
与快照读相对应的则是当前读(Current Read),当前读就是读取最新数据,而不是历史版本的数据。加锁的 SELECT 就属于当前读,例如:
SELECT * FROM t WHERE id=1 LOCK IN SHARE MODE;
SELECT * FROM t WHERE id=1 FOR UPDATE;
SELECT...FOR UPDATE 对读取的行记录加一个 X 锁,其它事务不能对已锁定的行加上任何锁。
SELECT...LOCK IN SHARE MODE 对读取的行记录加一个 S 锁,其它事务可以向被锁定的行加 S 锁,但是如果加 X 锁,则会被阻塞。
二、基于快照读的多版本并发控制
多版本并发控制技术的英文全称是:Multiversion Concurrency Control,简称 MVCC,是通过保存数据的历史版本,通过对数据行的多个版本管理来实现数据库的并发控制。这样我们就可以通过比较版本号决定数据是否显示出来,读取数据的时候不需要加锁也可以保证事务的隔离效果(可以理解成乐观锁)。
多版本并发控制(MVCC)只在可重复读(REPEATABLE READ)和提交读(READ COMMITTED)两个隔离级别下工作,其他两个隔离级别都和 MVCC 不兼容,因为未提交读(READ UNCOMMITTED),总是读取最新的数据行,而不是符合当前事务版本的数据行;而可串行化(SERIALIZABLE) 则会对所有读取的行都加锁。
MySQL 的大多数事务型存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑,它们一般都同时实现了多版本并发控制(MVCC)。不仅是 MySQL,包括 Oracle、PostgreSQL 等其他数据库系统也都实现了 MVCC,但各自的实现机制不尽相同,因为 MVCC 没有一个统一的实现标准,典型的有乐观(optimistic)并发控制和悲观(pessimistic)并发控制。
MVCC 的流程过程非常类似于 SVN 等版本控制系统的流程,或者说 SVN 等版本控制系统就是使用的 MVCC 思想。
三、多版本并发控制解决了哪些问题?
1. 读写之间阻塞的问题
通过 MVCC 可以让读写互相不阻塞,即读不阻塞写,写不阻塞读,这样就可以提升事务并发处理能力。
提高并发的演进思路:
- 普通锁,只能串行执行;
- 读写锁,可以实现读读并发;
- 数据多版本并发控制,可以实现读写并发。
2. 降低了死锁的概率
因为 InnoDB 的 MVCC 采用了乐观锁的方式,读取数据时并不需要加锁,对于写操作,也只锁定必要的行。
3. 解决一致性读的问题
一致性非锁定读也被称为快照读,这也是 InnoDB 存储引擎的默认读取方式,当我们查询数据库在某个时间点的快照时,只能看到这个时间点之前事务提交更新的结果,而不能看到这个时间点之后事务提交的更新结果。
四、InnoDB 的 MVCC 是如何工作的?
1. InnoDB 是如何存储记录的多个版本的?
事务版本号: 每开启一个事务,我们都会从数据库中获得一个事务 ID(也就是事务版本号),这个事务 ID 是自增长的,通过 ID 大小,我们就可以判断事务的时间顺序。
行记录的隐藏列: InnoDB 的叶子段存储了数据页,数据页中保存了行记录,而在行记录中有一些重要的隐藏字段:
DB_ROW_ID
:6-byte,隐藏的行 ID,用来生成默认聚簇索引。如果我们创建数据表的时候没有指定聚簇索引,这时 InnoDB 就会用这个隐藏 ID 来创建聚集索引。采用聚簇索引的方式可以提升数据的查找效率。DB_TRX_ID
:6-byte,操作这个数据的事务 ID,也就是最后一个对该数据进行插入或更新的事务 ID。(InnoDB 的插入、更新、删除都会更新该事务 ID,同时删除会将一个特殊位标记为已删除)DB_ROLL_PTR
:7-byte,回滚指针,也就是指向这个记录的 Undo Log 信息。
Undo Log: InnoDB 将行记录快照保存在了 Undo Log 里,我们可以在回滚段中找到它们,如下图所示,回滚指针将数据行的所有快照记录都通过链表的结构串联了起来,每个快照的记录都保存了当时的 db_trx_id,也是那个时间点操作这个数据的事务 ID。这样如果我们想要找历史快照,就可以通过遍历回滚指针的方式进行查找。
InnoDB 存储引擎对于 DELETE 操作,仅仅是将记录的 delete flag 设置为 1 ,记录并没有被物理删除,即记录还是存在于 B+ 树中,这样设计是因为 InnoDB 存储引擎支持 MVCC,所以记录不能在事务提交时立即进行处理,这时其他事务可能在引用此行,那么该条记录什么时候被真正删除呢?这个是由 Purge Thread 线程来处理,Purge Thread 会判断该记录是否不被任何其他事务引用,那么就可以进行真正的 delete 操作。
参考链接:MySQL的多版本并发控制(MVCC)