题图:pixabay
# MVCC 是什么东西?
Multi Version Concurrency Control的简称。代表多版本并发控制。
# MVCC 的作用是什么?
避免因为写锁的阻塞而造成读数据的并发阻塞问题。可以这么理解,在引擎层做了一个逻辑上(注意是逻辑上,不是物理上)的读写分离。
它是通过保存数据的多个历史版本,根据版本号来决定数据是否对事务可见。在InnoDB内部中,会记录一个全局的活跃读写事务ID数组,其主要根据事务ID 大小用来判断事务的可见性。
所以它可以使数据不用加锁就达到事务隔离的效果。
# 序列
- 事务日志
- 版本+模型
- 一致性视图
# 几种事务日志?
binlog。Mysql服务层产生的日志,用来做数据复制与同步以及故障恢复的。
redo log。Mysql数据日志记录,Mysql的写入流程是先写日志,再写磁盘,这里的日志就是redo log。这就是Mysql的WAL技术(Write-Ahead Logging)(Redis的写入也是这种方式)。具体一点就是,当一条数据需要更新的时候,InnoDB引擎会先把数据写入redo log,然后更新内存(change buffer),到这里就算成功了。数据会在适当的时候(什么是适当的时候?后面单独讲)去刷磁盘。
undo log。跟 redo log 是相反的,回放日志记录。它的生成方式是这样的:
insert => delete
insert into t(a) values(1); => id = 1 --- delete t where id = 1;
1
2
3delete => insert
delete t where id = 1; => id => 1, a => 1 --- insert into t(id, a) values(1, 1);
1
2
3update => update
update t set a = 2 where id = 1; => id => 1, a => 1 --- update t set a = 1 where id = 1;
1
2
3
# 版本模型
事务版本号
在InnoDB下,每个事务都有一个唯一的事务ID(transaction id),它是在事务开始的时候向事务系统申请(全局ID生成器)的。
数据版本
每一次的插入及更新操作都会视为一个版本。这个版本的架构如下:
- trx_id。事务ID
- roll ptr。指向上一个数据版本的指针,用于回退版本
根据这两个字段可以形成一个单链表,使得数据都可以溯源;在查询的时候,就可以有选择性的展示哪个版本的数据。那这个有选择性的展示是怎么实现的呢?
# 一致性视图
InnoDB为每个事务维护了一个数组,这个数组用来保存这个事务启动的瞬间,当前活跃的事务ID。这个数组里有两个水位值:
- 低水位:事务ID 最小值
- 高水位:事务ID 最大值 + 1
这两个水位值就构成了当前事务的一致性视图(Read-View)
当前事务(S1)在启动的瞬间,会生成一个活跃事务ID 数组。某个数据版本的trx_id。会有以下几种情况:
【【1,2,5】,【4,6】,【7,8】】
- (trx_id == 2 && trx_id < low woter)落在绿色区域(【1,2,5】)。表示这个事务是已提交的或者就是自己。那这个数据版本对S1是可见的
- 落在黄色区域(【4,6】)。
- (trx_id == 4 && trx_id >= low woter && trx_id < hign woter)如果trx_id in active array。说明这个数据版本的事务还没提交。对S1不可见
- (trx_id == 5 && trx_id not in 【low woter, hign woter】)如果trx_id not in active array。对S1可见。为什么会有这种情况呢?
- 黄绿蓝区域是由水位来隔开的
- 因为低水位往后的且小于高水位的都会在黄色区域
- 当前事务是6,事务4未提交,事务5已经提交了
- (trx_id == 8 && trx_id >= hign woter)落在红色区域(7,8)。在S1启动的时候,我这个版本还没生成呢。对S1不可见
# 下面来举个例子看下
create table t(name varchar, age int, sex int, address varchar) engine=InnoDB;
insert into t(name, age, sex, address) values('tom', 23, 2, 'beijing');
2
3
trx_id = 1 | trx_id = 2 | trx_id = 3 | trx_id = 4 | trx_id = 5 |
---|---|---|---|---|
begin; update t set age = 24, address = ‘nanjing’; commit; | ||||
begin; | ||||
begin; active=>[2,3] | ||||
begin;active=>[2,3,4] | ||||
begin;active=>[2,3,4,5] | ||||
update t set age = 5; commit; | ||||
update t set age = 6;commit | ||||
update t set age = 7;commit | ||||
select age from t; |
我们来画一下以上事务的版本结构
捋一下 trx_id = 1 的事务是怎么读到数据的?
- 找到 age = 3 版本的 trx_id = 3。判断 >= 当前事务的高水位,不可见。继续根据 ptr 找 age = 4 的版本。
- 找到 age = 4 版本的 trx_id = 4。判断 >= 当前事务的高水位,不可见。继续根据 ptr 找 age = 5 的版本。
- 找到 age = 5 版本的 trx_id = 5。判断 >= 当前事务的高水位,不可见。继续根据 ptr 找 age = 24 的版本。
- 找到 age = 24 版本的 trx_id = 1。判断 < 当前事务的低水位,可见。
这么一套下来,trx_id = 1 的事务 不管在什么时候查询,看到的数据都是 age = 24。这就是一致性视图。
# 引用
MySQL · 引擎特性 · InnoDB 事务系统 (opens new window)