逃不开躲不掉的mvcc
无论是为了提高事务的吞吐, 还是事务的隔离级别, 我们都逃不开mvcc的概念. 这个概念并非是分布式数据库引入的新概念,MySQL , Postgres数据库就使用了这些技术.
TiKV的源码中, 写操作在txn.rs
中使用事务写入, 读操作使用reader.rs
中使用MvccReader
来获取数据. 上一篇文章中并没有详细的说明TiKV读写数据的具体流程, 就是因为牵扯到MVCC
和分布式事务.这次在介绍完MVCC后,会将TiKV的读写过程做一个详细的描述.
什么是MVCC
MVCC是解决并发访问控制的一种技术. 假设在没有MVCC的时候, 当有写操作, 对数据进行上锁, 那么所有的读操作都会被阻塞.
那么解决这个问题的最原始方法就是读写分离, 一个数据有两个版本, 一个是读版本, 一个是写版本.
写操作操作写版本, 写完之后移动到读版本中.
MVCC的方案比这个复杂.
InnoDB实现MVCC的方法是,它存储了每一行的三个额外的隐藏字段,6字节的DB_ROW_ID,6字节的DB_TX_ID,7字节的DB_ROLL_PTR(指向对应回滚段的地址)。
在InnoDB中基本的结构是:
事务2执行UPDATE之后
事务3执行UPDATE之后
具体的副本创建规则
INSERT
设置当前系统版本号作为DB_ROW_ID; DB_TX_ID是当前的事务ID, DB_ROLL_PTR 是 NULLUPDATE
先将当前的数据复制到undo log
中, 插入一条新记录, DB_TX_ID是当前的事务ID, DB_ROLL_PTR是之前复制到undo log
中的地址.
通过这样, 每行的数据的多版本信息通过undo log
串联起来. 因此B+树中保存着数据的最新版本,低版本的数据都在undo log
中保存.
Read View来解决事务的隔离性
在Innodb中,创建一个新事务的时候,innodb会将当前系统中的活跃事务列表(trx_sys->trx_list)创建一个副本(read view),副本中保存的是系统当前不应该被本事务看到的其他事务id列表。
当用户在这个事务中要读取该行记录的时候,innodb会将该行当前的版本号与该read view进行比较。
具体算法
- 设该行的当前事务id为trx_id,read view中最早的事务id为
up_limit_id
, 最新的事务id为low_limit_id
。 - 如果
trx_id < up_limit_id
的话,那么表明该行记录所在的事务已经在本次新事务创建之前就提交了,所以该行记录的当前值是可见的。跳到步骤6. - 如果
trx_id > low_limit_id
的话,那么表明该行记录所在的事务在本次新事务创建之后才开启,所以该行记录的当前值不可见.跳到步骤5。 - 如果
up_limit_id<=trx_id<=low_limit_id
, 那么表明该行记录所在事务在本次新事务创建的时候处于活动状态,从up_limit_id
到low_limit_id
进行遍历,如果trx_id等于他们之中的某个事务id的话,那么不可见。跳到步骤5;如果trx_id不等于其中的任何一个id, 那么数据可见. - 从该行记录的
DB_ROLL_PTR
指针所指向的回滚段中取出最新的undo log
的版本号,将它赋值该trx_id,然后跳到步骤2. - 将该可见行的值返回。
具体示例
时间序列 | 事务1 | 事务2 | 事务3 |
---|---|---|---|
1 | begin | ||
begin | |||
Insert into demo(v) values(1); 假设此时事务号1 | |||
Insert into demo(v) values(2); 假设此时事务号2 | |||
select * from demo; 此时创建read view, up_limit_id = 1, low_limit_id = 2 活跃事务列表为(2) (注意当前事务不在活跃事务列表中) | |||
Insert into demo(v) values(3); 事务号为3 | |||
Insert into demo(v) values(4); 事务号为4 | |||
Insert into demo(v) values(5); 事务号为5 | |||
select * from demo; 此时创建read view, up_limit_id = 1, low_limit_id = 5; 活跃事务列表为(1,2);因此只能看到3, 4, 5 | |||
select * from demo; 此时创建read view, up_limit_id = 1, low_limit_id = 5; 活跃事务列表为(1);因此能看到2, 3, 4, 5 | |||
select * from demo; 此时创建read view, up_limit_id = 1, low_limit_id = 2 活跃事务列表为(2) 因此能看到1 (RR隔离级别 read view 创建后在事务内部不会更改) |
注意的几点:
- Read View视图是在进行
SELECT
之前创建的,而不是在事务刚begin时创建的。 - RR 级别下Read View复制全局Read View;RC 不复制Read View, 而是共享全局Read View.
- RR 级别下事务内Read View一旦创建就不变化了。
参考
- https://m.imooc.com/article/17290
- https://github.com/zhangyachen/zhangyachen.github.io/issues/68