参考链接:https://zhuanlan.zhihu.com/p/52977862
1、MVCC概念
是什么?
多版本并发控制(Multi-Version Concurrency Control)
有什么用?
- 提升并发性能,在一定程度上避免了加锁操作。
- MVCC大都实现了非阻塞的读操作,写操作也只锁定必要的行。
怎么做到的
- 通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始时间的不同,每个事务对同一张表,同一时刻,看到的数据可能是不一样的。-------- 摘自《高性能MYSQL第三版》
- 不同的存储引擎对MVCC的实现是不同的,典型的有乐观并发控制和悲观并发控制。
2、InnoDb的MVCC实现
有什么用?
- 提高并发性能,实现事务间数据隔离的同时,在很多情况下避免了加锁。
通俗的讲就是MVCC通过保存数据的历史版本,根据比较数据的版本号来决定数据的是否显示,在不需要加读锁的情况就能达到事务的隔离效果,最终可以在读取数据的时候可以同时进行修改,修改数据时候可以同时读取,极大的提升了事务的并发性能。
InnoDB MVCC实现的核心知识点
事务版本号
- 每次事务开启前都会从数据库获得一个自增长的事务ID,可以从事务ID判断事务的执行先后顺序。
可以通过select * from information_schema.innodb_trx
命令查看事务列表
image.png
数据表的隐藏列
表里的每一行数据都有以下几个隐藏列
-
DB_TRX_ID
:记录操作该数据事务的事务ID; -
DB_ROLL_PTR
:指向上一个版本数据在undo log 里的位置指针; -
DB_ROW_ID
:隐藏ID ,当创建表没有合适的索引作为聚集索引时,会用该隐藏ID创建聚集索引;
Undo log(回滚日志)
- Undo log 主要用于记录数据被修改之前的日志
在表信息修改之前先会把表里的原数据拷贝到undo log 里,当事务进行回滚时可以通过undo log 里的日志进行数据还原。
下图展示了user表中id=1001的一条数据,可能的存储情况
image.png
Undo log 的用途
(1)保证事务进行rollback时的原子性和一致性,当事务进行回滚的时候可以用undo log的数据进行恢复。
(2)用于MVCC快照读的数据,在MVCC多版本控制中,通过读取undo log的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本。
一条数据在undo日志里存在多个版本,回滚操作会不会乱掉?
答:不会的
- 还是以上图user表的存储情况来看,id=1001的记录,虽然有四个版本的数据,但其实只有DB_TRX_ID=100的记录可以回滚,而且他也只可以回滚成DB_TRX_ID=99,(地址=111111111)的副本。
- 原因有以下几点:
- 这四个版本的事务,只有事务ID=100的是没有提交的,其他的一定提交了
- 因为:MYSQL在更改语句里会默认加上写锁,若更改后事务没有提交,其他事务是不能进行写操作的。
-
所以根据上图来看,这四个事务一定按照如此的顺序更改并提交的:DB_TRX_ID=80、DB_TRX_ID=96、DB_TRX_ID=99、DB_TRX_ID=100。不会存在DB_TRX_ID的事务回滚成DB_TRX_ID=80的数据。
image.png
- 为什么前三个事务都提交了,但undo log里的数据没有清掉呢?这涉及到了事务回滚日志回收的逻辑,而这个回收节点与下面的read view息息相关,下文会介绍到这一点。
事务版本号、表格的隐藏列、undo log的关系
我们模拟一次数据修改的过程来让我们了解下事务版本号、表格隐藏的列和undo log他们之间的使用关系。
-
首先准备一张原始原始数据表
image.png
- 开启一个事务A: 对user_info表执行 update user_info set name =“李四”where id=1 会进行如下流程操作
1、 首先获得一个事务编号104(当前系统最大事务号为104,事务新建后最大事务号变为105)
2、把user_info表id=1的行修改前的数据拷贝到undo log
3、修改user_info表 id=1的数据
4、把修改后的数据事务版本号改成当前事务版本号,并把DB_ROLL_PTR 地址指向undo log数据地址。 - 最后执行完结果如图:
![](https://img.haomeiwen.com/i7392673/4e9eb67929faf452.png)
Read view
- MVCC 中维护了一个 ReadView 结构,主要存储当前数据库活跃状态的(未提交)的事务Id号
- 在innodb 中每个事务开启后都会得到一个read_view的copy副本(这个副本生成的时机不是事务开始时,事务开启时会确定当前事务的TRX_ID,但read view副本的生成时机取决于第一次查询(insert、update、delete语句都不会创建副本,只有select才会)在什么时候)。副本主要保存了当前数据库系统中正处于活跃(没有commit)的事务的ID号,其实简单的说这个副本中保存的是系统中当前不应该被本事务看到的其他事务id列表。
- 事务中Select时,会通过事务copy的read_view来筛选数据,对指定的某一条数据不同的版本进行匹配,确定返回哪个版本的数据
Read view 的几个重要属性:
属性 | 备注 |
---|---|
trx_ids | 当前系统活跃(未提交)事务版本号集合。注意,trx_ids并不包含当前事务的id |
low_limit_id | 创建当前read view 时“目前出现过的最大事务id加1”。 |
up_limit_id | 创建当前read view 时“活跃事务列表的最小事务ID,如果活跃事务列表为空,则up_limit_id 为 low_limit_id” |
creator_trx_id | 创建当前read view的事务版本号; |
事务不同隔离级别copy read_view的时机
RC(read commit) 级别下同一个事务里面的每一次查询都会获得一个新的read view副本。这样就可能造成同一个事务里前后读取数据可能不一致的问题(重复读)
![](https://img.haomeiwen.com/i7392673/40f38429d30e4b56.png)
RR(重复读)级别下的一个事务里只会获取一次read view副本,从而保证每次查询的数据都是一样的。
![](https://img.haomeiwen.com/i7392673/8ea72c3960b19bfa.png)
READ_UNCOMMITTED 级别的事务不会获取read view 副本。
级别 | 时机 | -- |
---|---|---|
RU | 不需要 | |
RC | 每次Select时都会获取最新的 | 事务在begin之后,执行每条select(读操作)语句时,快照会被重置,即会重新创建一个快照(read view)。 |
RR | 第一次Select语句执行时 | 只会创建一次快照(read view),作用于当前数据库的所有表的Select语句,即:即便是Select的表A,后面再SelectB也不会再次copy read_view了 |
序列化 | 不需要 |
Select时,Read view 匹配指定行的版本号流程:
- 注意:RC和RR两种隔离级别,read view匹配流程都是一致的。区别仅仅是生成read view副本的时机。
- 事务ID=creator_trx_id,显示
- 数据的事务ID等于creator_trx_id ,那么说明这个数据就是当前事务自己生成的,自己生成的数据自己当然能看见
- 数据的
DB_TRX_ID
(事务ID)<up_limit_id
, 显示- 事务copy read view前数据已提交(如果未提交,
up_limit_id
<最小活跃事务id>就是它了)
显示。
- 事务copy read view前数据已提交(如果未提交,
- 数据事务ID>=
low_limit_id
,不显示- 事务copy read view后创建的数据(他比copy时的系统最大事务ID要大,不管提没提交都不显示)
-
up_limit_id
<=数据事务ID<low_limit_id
, 需要匹配trx_ids
(活跃事务集合)- 事务大于最小活跃事务id,小于当时系统最大事务id,这种情况需要判断当时此事务有没有提交,如果已经提交了就显示,未提交就不显示(防止脏读),折旧需要匹配trx_ids。
- 情况1: 数据事务ID not in trx_ids, 显示
- 事务copy read view时已经提交了(如果copy时未提交,他应该在活跃(未提交的)的事务列表里),这种情况数据则可以显示。
- 情况2:数据事务ID in trx_ids,不显示
- 如果事务ID存在trx_ids则说明copy read view时,数据还没有提交,为了防止脏读,就不显示。
- 当前记录不满足read view条件时候,从undo log里面获取数据,循环以上过程,直到找到可以显示的数据或遍历完所有的历史版本。
- 通过DB_ROLL_PTR找到上一个版本的数据在undo log里的地址,并循环以上过程。
RC不可重复读的原因
综合以上流程,可以看到RC和RR的区别在于:RC每次select语句都会重新构建read view,这就可能导致:两个或多个read view在走匹配流程时,在第2->5这个过程可能不一致。
- 比如有一条数据,在走第一个read view的匹配时,数据事务id的大于up_limit_id,所以不显示。在走第二个read view时,若系统最大ID增长了且此数据的事务提交了,那这个数据就会通过2-5的匹配流程。这就是为什么RC不可重复读的原因
Innodb实现MCC的原理
![](https://img.haomeiwen.com/i7392673/b8475c3a514cad7c.png)
模拟MVCC实现流程
-
创建user_info表,插入一条初始化数据
image.png
- 事务A和事务B同时对user_info进行修改和查询操作
事务A:update user_info set name =”李四”
事务B:select * fom user_info where id=1
问题:
先开启事务A ,在事务A修改数据后但未进行commit,此时执行事务B。最后返回结果如何。
流程如下:
image.png
执行流程说明:- 事务A:开启事务,首先得到一个事务编号102;
- 事务B:开启事务,得到事务编号103;
-
事务A:进行修改操作,首先把原数据拷贝到undolog,然后对数据进行修改,标记事务编号和上一个数据版本在undo log的地址。
image.png
-
事务B: 此时事务B获得一个read view ,read view对应的值如下
image.png
-
事务B: 执行查询语句,此时得到的是事务A修改后的数据
image.png
-
事务B: 把数据与read view进行匹配
数据事务ID为102 等于up_limit_id (这里不小于up_limit_id)
数据事务ID为102 小于low_limit_id
数据事务ID为102 存在于 trx_ids
数据事务ID为102 不等于creator_trx_id
发现不满足read view显示条件,所以从undo lo获取历史版本的数据再和read view进行匹配,最后返回数据如下。
image.png
快照读和当前读
快照读
快照读是指读取数据时不是读取最新版本的数据,而是基于历史版本读取的一个快照信息(mysql读取undo log历史版本) ,快照读可以使普通的SELECT 读取数据时不用对表数据进行加锁,从而解决了因为对数据库表的加锁而导致的两个如下问题
1、解决了因加锁导致的修改数据时无法对数据读取问题;
2、解决了因加锁导致读取数据时无法对数据进行修改的问题;
当前读
当前读是读取的数据库最新的数据,当前读和快照读不同,因为要读取最新的数据而且要保证事务的隔离性,所以当前读是需要对数据进行加锁的(Update delete insert select ....lock in share mode select for update 为当前读)
MVCC能解决脏读问题?
能
- 脏读是指:读取到其他事务未提交的数据,随后此数据又被回滚了
- InnoDB实现的MVCC,应用于RC和RR两种隔离级别,可以解决脏读问题
RR级别下MVCC是否有解决幻读问题?
不完全能
-
幻读是指:幻读是一次事务中前后数据量发生变化,用户产生不可预料的问题
MVCC 在RR级别下 可以解决部分幻读, 但不能完全解决. -
Mysql官方给出的幻读解释是:
只要在一个事务中,第二次select多出了row就算幻读。- a事务先select,b事务 insert确实会加一个gap锁,但是如果b事务commit,这个gap锁就会释放(释放后a事务可以随意dml操作)。
- a事务再select出来的结果在MVCC下还和第一次select一样;这一步没有出现幻读。
- 接着a事务不加条件地update,这个update会作用在所有行上(b事务已经提交,包括b事务新加的)。
- a事务再次 select就会出现b事务中的新行(幻读出现),并且这个新行已经被 update修改了。
这个例子会出现幻读问题,事务a的UPDATE语句执行之后,会把更新语句的WHERE条件覆盖到的所有数据都进行更新,这就绕过了read view匹配的过程。而当前事务所做的任何更新,对本事务所有SELECT查询都变的可见(此UPDATE语句会把涉及的所有记录原数据拷贝到undolog,然后对数据进行修改,并标记事务编号为事务a的编号。再SELECT时就可以匹配到了),因此最后输出的结果是UPDATE执行后更新的所有记录。
能解幻读的场景
1、开启事务1,获得事务ID为1。
2、事务1执行查询,得到readview。
3、开始事务2。
4、执行insert。
5、提交事务2。
6、执行事务1的第二次查询 (因为这里是RR级别,所以不会再去获得readview,还是使用第一次获得的readview)
7、最后得到的结果是,插入的数据不会显示,因为插入的数据事务ID大于等于 readview里的最大活跃事务ID。
不能解决幻读的场景
1、开启事务1,获得事务ID为1。
2、事务1执行查询,得到readview。
3、开启事务2,执行insert语句。
4、提交事务2。
5、事务1执行第二次查询,此时查不到新插入的数据。
6、事务1执行update语句,此语句的where条件覆盖了事务2insert的数据。比如:'UPDATE user SET score = 0'
7、事务1执行第三次查询,可以查到新插入的数据。
- 但其实这个场景,也可能导致了不可重复读的问题,比如事务一执行的语句是
SELECT * FROM user
,第三次执行的查询的结果和前两次是不一样的。
为什么要避免长事务
- 影响undo 日志的回收
- 锁资源不能得到释放
长事务对undo日志回收的影响。
- undo 日志产生时机:每个事务对记录的更改,都会在undo log里生成一个记录的copy。事务提交后,copy数据不会立刻删除。
- undo日志删除时机:没有比此copy记录的事务id更早的read view时。
为什么read view会影响undo日志删除呢?
长事务意味着系统里会存在很老的read view,这些老的read view随时可能会用到数据库里的任何数据(比它事务id大的数据)。所以在这个长事务提交前,这些回滚记录都必须保留。 - 比如(rr级别下)
有一条数据,事务id为99
1、事务一启动,执行查询语句,查询到事务id为99的数据,当前事务id为100。
2、事务二启动,执行更新语句,事务id为101,copy事务id为99的数据到undo log,并新增一条相同的记录但事务id为101到表里
3、事务一再次执行,需要查询undo log里的copy数据。
4、此时若事务一不提交,他需要一直查询undo 里的copy数据(事务id为99的数据)。(如果undolog里的此数据回收了,事务一就会出现问题了。。)
可以通过语句查询长事务:
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60
网友评论