概述
MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
MVCC 原理
MVCC的实现,利用到了数据库的隐式字段,undo log和ReadView。首先来看隐式字段,其实mysql在表中的每行记录的后面,都隐式的记录了DB_TRX_ID(最近修改(修改/插入)事务ID),DB_ROLL_PTR(回滚指针,指向这条记录的上一个版本),DB_ROW_ID(自增ID,如果数据表没有主键,则默认以此ID简历聚簇索引)这几个隐藏的字段。
undo log分为两种,分别为insert undo log,在insert新记录时产生的undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃,还有update undo log,事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除。MVCC利用到的是update undo log。
redolog
binglog
undolog
概述
MVCC底层依赖Mysql的undo log,undo log记录了数据库的操作,因为undo log是逻辑日志,可以理解为delete一条记录的时候,undo log会记录一条对应的insert记录,update一条记录的时候,undo log会记录一条相反的update记录,当事务失败需要回滚操作时,就可以通过读取undo log中相应的内容进行回滚,MVCC就利用到了undo log。
版本链
假设存在 account 表,字段如下
id | name | age | DB_TRX_ID | DB_ROLL_PTR |
---|---|---|---|---|
insert undo log
insert undo log是在insert操作中产生的undo log。因为insert操作的记录只对事务本身可见,对于其它事务,此记录是不可见的,所以insert undo log可以在事务提交后直接删除而不需要进行purge操作。
insert account(name,age) value('lilei',25)
id | name | age | DB_TRX_ID | DB_ROLL_PTR |
---|---|---|---|---|
1 | lilei | 25 | null | null |
update undo log
update undo log是update或delete操作中产生的undo log。因为会对已经存在的记录产生影响,为了提供 MVCC机制,因此update undo log不能在事务提交时就进行删除,而是将事务提交时放到入history list上,等待purge线程进行最后的删除操作。
insert
- 现在有一个事务A修改了这条记录,把name改为tom,这个时候的操作流程为:
- 事务A首先对该行记录加上行锁
- 然后将该行记录拷贝到undo log中,作为一个旧的版本
- 拷贝完之后将该行name修改为tom,然后将该行的DB_TRX_ID的值改为事务A的id,此时假设事务A的id为1,将该行的DB_POLL_PTR指向拷贝到undo log的那条记录
- 事务提交后,释放锁
此时的情况如下:
- 此时又有一个事务B来修改这条记录,把age改为28,这时候的操作流程为:
- 事务B对改行记录加上行锁
- 将该行记录拷贝到undo log中,作为一个旧的版本,此时发现undo log已经有记录了,那么新的一条undo log作为链表的表头插入到该行记录的undo log的最前面
- 拷贝完后将该行的age改为28,然后将该行的DB_TRX_ID的值改为事务B的id,此时假设事务B的id为2,将该行的DB_POLL_PTR指向拷贝到undo log的那条记录
- 事务提交后释放锁 此时的情况如下:
从上面我们可以看到,不同的事务或者相同的事务对同一行记录进行的修改,会使得该行记录的undo log形成一个版本链,undo log的链首就是最近一次的旧记录,而链尾就是最早一次的旧记录。
现在我们来假设一种情况,先假设事务A和事务B都没有提交,这时候有一个事务C,修改了name为tom的记录,把age改成了30,然后把事务提交,事务C的id为3,同样的,会插入一条记录到undo log中,此时的undo log版本链链首记录的DB_TRX_ID为3。
delete
删除一条记录需要经历2个阶段
-
delete mark阶段(简单标记) :事务运行期间进行了delete操作,只会将记录的deleted_flag属性标记为1,不会做其他任何操作
-
purge阶段(进行真正的删除) :事务提交后,才可能进入purge阶段,但是purge阶段并不是事务提交后立刻进行的,而是由purge线程负责,purge线程检测到记录符合条件后才会进行purge操作——purge操作会将记录由正常记录链表移除,加入页面的垃圾链表头部,并调整页面属性信息
delete mark阶段就会生成delete mark undo log
- 此时又有一个事务C来删除该记录,这时候的操作流程为:
- 事务C对改行记录加上行锁
- 事务C对改行记录打删除标记
- 将该行记录拷贝到undo log中,作为一个旧的版本,此时发现undo log已经有记录了,那么新的一条undo log作为链表的表头插入到该行记录的undo log的最前面
- 事务提交后释放锁 此时的情况如下:
ReadView
ReadView一致性视图主要是由两部分组成:所有未提交事务的ID数组和已经创建的最大事务ID组成(实际上ReadView还有其他的字段,但不影响这里对MVCC的讲解)。比如:[100,200],300。事务100和200是当前未提交的事务,而事务300是当前创建的最大事务(已经提交了)。当执行SELECT语句的时候会创建ReadView,但是在读取已提交和可重复读两个事务级别下,生成ReadView的策略是不一样的:
- 读取已提交级别是每执行一次SELECT语句就会重新生成一份ReadView,
- 可重复读级别是只会在第一次SELECT语句执行的时候会生成一份,后续的SELECT语句会沿用之前生成的ReadView(即使后面有更新语句的话,也会继续沿用)。
数据结构
m_ids: 当前有哪些事务正在执行,且还没有提交,这些事务的 id 就会存在这里
min_trx_id: 是指 m_ids 里最小的值
max_trx_id: 下一个要生成的事物id,因为事物id是递增的 肯定比当前所有事物id要大
creator_trx_id: 创建read view的事物id
匹配规则
构造ReadView
如:[100,200],300
(其中min_id指向ReadView中未提交事务数组中的最小事务ID,而max_id指向ReadView中的已经创建的最大事务ID)
- 如果落在绿色区间(DB_TRX_ID < min_id):这个版本比min_id还小(事务ID是从小往大顺序生成的),说明这个版本在SELECT之前就已经提交了,所以这个数据是可见的。或者(这里是短路或,前面条件不满足才会判断后面这个条件)这个版本的事务本身就是当前SELECT语句所在事务的话,也是一样可见的;
- 如果落在红色区间(DB_TRX_ID > max_id):表示这个版本是由将来启动的事务来生成的,当前还未开始,那么是不可见的;
- 如果落在黄色区间(min_id <= DB_TRX_ID <= max_id):这个时候就需要再判断两种情况:
- 如果这个版本的事务ID在ReadView的未提交事务数组中,表示这个版本是由还未提交的事务生成的,那么就是不可见的;
- 如果这个版本的事务ID不在ReadView的未提交事务数组中,表示这个版本是已经提交了的事务生成的,那么是可见的。 如果在上述的判断中发现当前版本是不可见的,那么就继续从版本链中通过回滚指针拿取下一个版本来进行上述的判断。
演示过程
假设:
- 版本链已存在,如上面分析
- 当前事务隔离级别为RR,既第一次SELECT语句执行的时候会生成一份,后续的SELECT语句会沿用之前生成的ReadView
Transaction 1 | Transaction 2 | Transaction 3 | 无事务id | 无事务id | |
---|---|---|---|---|---|
t1 | begin; | begin; | begin; | begin; | |
t2 | update account name='tom' where name='lilei' | ||||
t3 | update account age='28' where name='tom' | ||||
t4 | commit; | ||||
t5 | delete form account where id =1 | ||||
t6 | select * from account where id =1 | ||||
t7 | commit | ||||
t8 | select * from account where id =1 | ||||
t9 | select * from account where id =1 |
从左往右分别是五个事务,从上到下是时刻点。当执行update 或者delete 语句时生成事务id。
在t1时刻点,五个事务分别开启了事务(如上所说,这个时候还没有生成事务ID)。
在t2时刻点,第一个事务执行了一条UPDATE语句,生成了事务ID为1。
在t3时刻点,第二个事务执行了一条UPDATE语句,生成了事务ID为2。
在t4时刻点,第二个事务执行了commit 提交事务
在t5时刻点,第三个事务执行了一条delete语句,生成了事务ID为3。
在t6时刻点,执行查询,生成ReadView,此时的ReadView 如下
- ReadView
[1,3],2 解释:事务数组[1,3],已完成的事务2
-
版本链
-
比对规则: 取版本链中第一个事务3 进行比较 min_id <3 <=max_id,但是 事务id:3 不在已提交事务范围内,故不可见
再去版本链第二个事务2 进行比较 min_id <=2 <=max_id,且id:2 在已提交事务范围内,故可见
id | name | age |
---|---|---|
1 | tom | 28 |
在t7时刻点,第三个事务执行commit
在t8时刻点,执行查询,生成ReadView,此时的ReadView 如下
因为数据库隔离级别设置为RR,故ReadView 和T6 一致,结论也和T6 一致
在t9时刻点,执行查询,ReadView 详情如下:
[1],3 解释:事务数组[1],已完成的事务3
- 版本链
去版本链第一个事务:3 进行比较 min_id<=3<= max_id,故可见
id | name | age |
---|---|---|