多版本并发控制:读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了,不同的事务session会看到自己特定版本的数据,版本链.
MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作。其他两个隔离级别够和MVCC不兼容, 因为 READ UNCOMMITTED 总是读取最新的数据行, 而不是符合当前事务版本的数据
行。而 SERIALIZABLE 则会对所有读取的行都加锁。
MVCC机制主要通过隐藏字段、Undo-log日志、ReadView这三个东西实现的,因而这三玩意儿也被称为“MVCC三剑客”!
基于InnoDB引擎,在本次MVCC分析中,只关注事物id(trx_id)和 回滚指针(roll_pointer)两个隐藏列。
MySQL事务机制是基于Undo-log实现的,Undo-log日志中会存储旧版本的数据,但要注意:Undo-log中并不仅仅只存储一条旧版本数据,其实在该日志中会有一个版本链
不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。
当一个事务启动后,首次执行select操作时,MVCC就会生成一个数据库当前的ReadView,通常而言,一个事务与一个ReadView属于一对一的关系(不同隔离级别下也会存在细微差异),ReadView一般包含四个核心内容:
上面四个值很简单,low_limit_id 并不是目前系统中活跃事务的最大ID,因为MySQL的事务ID是按序递增的,因此当启动一个新的事务时,都会为其分配事务ID,而这个low_limit_id则是整个MySQL中,要为下一个事务分配的ID值。
下面上个ReadView的示意图,来好好理解一下它:
①当一个事务尝试改动某条数据时,会将原本表中的旧数据放入Undo-log日志中。
②当一个事务尝试查询某条数据时,MVCC会生成一个ReadView快照
Undo-log主要实现数据的多版本,ReadView则主要实现多版本的并发控制,还是以之前的例子来举例说明:
-- 事务T1:trx_id=1
UPDATE `users` SET user_name = "煎饼狗子" WHERE user_id = 1;
UPDATE `users` SET user_sex = "男" WHERE user_id = 1;
-- 事务T2:trx_id=2
SELECT * FROM `users` WHERE user_id = 1;
目前存在T1、T2两个并发事务,T1目前在修改ID=1的这条数据,而T2则准备查询这条数据,那么T2在执行时具体过程如下:
①当事务中出现select语句时,会先根据MySQL的当前情况生成一个ReadView。
②判断行数据中的隐藏列trx_id与ReadView.creator_trx_id是否相同:
相同:代表创建ReadView和修改行数据的事务是同一个,自然可以读取最新版数据。
不相同:代表目前要查询的数据,是被其他事务修改过的,继续往下执行。
③判断隐藏列trx_id是否小于ReadView.up_limit_id最小活跃事务ID:
小于:代表改动行数据的事务在创建快照前就已结束,可以读取最新版本的数据。
不小于:则代表改动行数据的事务还在执行,因此需要继续往下判断。
④判断隐藏列trx_id是否小于ReadView.low_limit_id这个值:
大于或等于:代表改动行数据的事务是生成快照后才开启的,因此不能访问最新版数据。
小于:表示改动行数据的事务ID在up_limit_id、low_limit_id之间,需要进一步判断。
⑤如果隐藏列trx_id小于low_limit_id,继续判断trx_id是否在trx_ids中:
在:表示改动行数据的事务目前依旧在执行,不能访问最新版数据。
不在:表示改动行数据的事务已经结束,可以访问最新版的数据。
条件 | 是否可以访问 | 说明 |
---|---|---|
trx_id == creator_trx_id | 可以访问该版本 | 说明数据是当前这个事物更改的 |
trx_id < up_limit_id (最小活跃事物id) | 可以访问该版本 | 说明数据已经提交了 |
trx_id > low_limit_id (预分配事物id) | 不可以访问该版本 | 说明该事务是在ReadView生成后才开启 |
up_limit_id <= trx_id <= low_limit_id | trx_id不在trx_ids中,是可以访问该版本 | 说明数据已经提交 |
已提交读(RC)和可重复读(RR)的区别就在于它们生成ReadView的策略不同。