Mysql<七> 事务的MVCC机制原理解析

前言

查看当前运行的事务的SQL语句:

-- 查看当前运行的事务
SELECT
a.trx_id,a.trx_state,a.trx_started,a.trx_query,
b.ID,b.USER,b.DB,b.COMMAND,b.TIME,b.STATE,b.INFO,
c.PROCESSLIST_USER,c.PROCESSLIST_HOST,c.PROCESSLIST_DB, d.SQL_TEXT
FROM
information_schema.INNODB_TRX a
LEFT JOIN information_schema.PROCESSLIST b ON a.trx_mysql_thread_id = b.id
AND b.COMMAND = 'Sleep'
LEFT JOIN PERFORMANCE_SCHEMA.threads c ON b.id = c.PROCESSLIST_ID
LEFT JOIN PERFORMANCE_SCHEMA.events_statements_current d ON d.THREAD_ID = c.THREAD_ID;

MVCC的概念

MVCC Muliversion concurrency control 是用于数据库提供并发访问控制的并发控制技术。是相对于LBCC更好的一种并发控制技术。MVCC核心思想是读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能,这也是为什么现阶段,几乎所有的RDBMS,都支持了MVCC原因。

MVCC 核心理念是数据快照,不同的事务访问不同版本的数据快照,从而实现事务的隔离级别。虽然字面上是说具有多个版本的数据快照,但这并不意味着数据库必须拷贝数据,保存多份数据文件,这样会浪费大量的存储空间。InnoDB通过事务的undo log巧妙地实现了多版本的数据快照。

那为什么MVCC能解决不可重复读的问题?
MVCC 在mysql 中的实现依赖的是 undo logread view

注意MVCC只在RR和RC两个隔离级别下工作。
RU和串行化隔离级别都和 MVCC不兼容 。为什么?

因为RU总是读取最新的数据行,本身就没有隔离性,也不解决并发潜在问题,因此不需要!

而SERIALIZABLE则会对所有读取的行都加锁,相当于串行执行,线程之间绝对隔离,也不需要。

在这里插入图片描述
InnoDB的MVCC是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的事务ID,一个保存了行的回滚指针。每开始一个新的事务,都会自动递增产生一个新的事务id。事务开始时,会把事务id放到当前事务影响的行事务id中。当查询时,需要用当前查询的事务id和每行记录的事务id进行比较。

MVCC实现原理

undo log

位置:回滚段;
结构:单链表;(由于undo log是多版本的链表结构,又被称为版本链)
分类:insert undo log 和 update undo log
控制信息:每次insert,update,delete的数据的链表。

insert undo log

insert操作中产生的 undo log。
因为 insert 操作的记录只对事务本身可见,对于其它事务此记录是不可见的,所以 insert undo log 可以在事务提交后直接删除而不需要进行回收操作。
如下图所示(初始状态):

# 事务1:
Insert into user(id,name,age,address) values (10,'tom',23,'nanjing')

在这里插入图片描述

update undo log

updatedelete 操作中产生的 undo log。
因为会对已经存在的记录产生影响,为了提供 MVCC机制,因此 update undo log 不能在事务提交时就进行删除,而是将事务提交时放到入 history list 上,等待 purge 线程进行最后的删除操作。
如下图所示(第一次修改):

# 事务2:
update user set name='jack',age=10 where id=10;
# 当事务2使用UPDATE语句修改该行数据时,会首先使用写锁锁定改行,将该行当前的值复制到undo log中,然后再真正地修改当前行的值,最后填写事务ID,使用回滚指针指向undo log中修改前的行。

在这里插入图片描述

当事务3进行修改与事务2的处理过程类似,如下图所示(第二次修改):

# 事务3:
update user set name='Lucy',age=11 where id=10;

在这里插入图片描述
为了保证事务并发操作时,在写各自的undo log时不产生冲突,InnoDB采用回滚段的方式来维护undo log的并发写入和持久化。回滚段实际上是一种 Undo 文件组织方式,是多个版本数据的链表,也称之为版本链了。

ReadView

结构:表;
数据:由当前事务id(m_creator_trx_id)创建的表,包含

  • m_up_limit_id:事务id下限,m_ids事务列表中的最小事务id。
  • m_low_limit_id:事务id上限,系统中将要产生的下一个事务id的值。
  • m_creator_trx_id:当前事务id,m_ids中不包含当前事务id

MVCC的核心问题就是:判断一下版本链中的哪个版本是当前事务可见的!

  • 对于使用 RU 隔离级别的事务来说,直接读取记录的最新版本就好了,不需要Undo log。
  • 对于使用 串行化 隔离级别的事务来说,使用加锁的方式来访问记录,不需要Undo log。
  • 对于使用 RC 和 RR 隔离级别的事务来说,需要用到undo log的版本链。
ReadView怎么产生,什么时候生成?
  • 开启事务之后,在第一次查询(select)时,生成ReadView
  • RC 和 RR 隔离级别的的一个非常大的区别就是它们生成 ReadView 的时机不同;RC是每次查询都刷新ReadView;而RR只在第一次select时刷新。
  • RC 和 RR 隔离级别的差异本质是因为MVCC中ReadView的生成时机不同,详情在案例中分析。
如何判断可见性?

在访问某条记录时,按照下边步骤判断记录的版本链的某个版本是否可见:

循环判断规则如下:被访问undo log版本的事务id与ReadView的关系

  • 小于ReadView中的 m_up_limit_id ,表明生成该版本的事务在生成 ReadView 前已经提交,所以该版本可以被当前事务访问。
  • 等于ReadView中的 m_creator_trx_id ,可以被访问。
  • 大于等于ReadView中的 m_low_limit_id ,在生成 ReadView 后才生成,所以该版本不可以被当前事务访问。
  • 在 m_up_limit_id 和 m_low_limit_id 之间,那就需要判断是不是在 m_ids 列表中。
    1 如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;
    2 如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。

循环判断Undo log中的版本链某一的版本是否对当前事务可见,如果循环到最后一个版本也不可见的话,那么就意味着该条记录对该事务不可见,查询结果就不包含该记录。

MVCC在隔离机制RC和RR的实现案例

RC的MVCC机制实现

初始数据为:

create table t(
id int primary key auto_increment;
name varchar(25)
};

insert table t(name) values ('刘备');

步骤:开启三个事务,隔离等级设置为RC;
在这里插入图片描述
RC隔离等级下:

分析1、 T3时:事务1的T2和T3更新生成undo log,此时事务3查询id=1,得到刘备。
在这里插入图片描述

因为此时的查询生成ReadView的m_low_limit_id为【100】,100属于当前活跃事务,版本链中往下早,只有trx_id=80小于100.此时就会将刘备返回。

分析2、T3时: commit事务1,再次进行事务3查询id=1;

对于RC隔离机制下,ReadView每次查询都会更新ReadView,此时的trx_id=100已经不再活跃事务里了,所以就得到了张飞。

RR的MVCC机制实现

与RC相同,只是将隔离等级设置为RR。
此时得到的分析1是一样的。
分析2是不一样的,因为RR机制下的ReadView实在第一次查询的时候生成,事务提交之前不会结束的。那么事务3在事务1提交的T3时刻再去查询,他的ReadView没变,仍旧显示trx_id=100时活跃状态(尽管事务id=100已经提交),所以还是会读到’刘备‘。

这就是为什么可以实现可重复读。

MVCC下的读操作

快照读:简单的select操作,属于快照读,不加锁。

读到的时该事务可见的版本。

select * from table where ?;
当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。

读到的是整个库中的最新版本。

select * from table where ? lock in share mode; # 加读锁
select * from table where ? for update;# 加写锁
insert into table values ();# 加写锁
update table set ? where ?;# 加写锁
delete from table where ?;# 加写锁
# 所有以上的语句,都属于当前读,读取记录的最新版本。并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁。其中,除了第一条语句,对读取记录加**读锁**外,其他的操作都加的是**写锁**。

总结

1、undo log不会无限累计,当所有对该行的事务操作都提交,那么此时这一行数据对之后所有的事务来说都是历史数据,此时就会在回滚段中删除undo lod,也就是版本链。
2、MVCC机制时实现的读不加锁,读写分离。所以两个事务都在对一行数据写时,此竞争关系一方会被锁机制阻塞。也就是写仍旧加锁
3、MVCC的核心理念时数据快照,由undo log版本链来实现。
4、RC、RR这两个隔离级别的一个很大不同就是生成 ReadView 的时机不同:

  • RC在每一次进行普通 SELECT 操作前都会生成一个 ReadView ,
  • RR在第一次进行普通 SELECT 操作前生成一个 ReadView ,之后的查询操作都重复这个ReadView 。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

四库全书的酷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值