一、什么是MVCC

多版本控制: 指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。

在内部实现中,InnoDB通过undo log保存每条数据的多个版本,并且能够找回数据历史版本提供给用户读,每个事务读到的数据版本可能是不一样的。在同一个事务中,用户只能看到该事务创建快照之前已经提交的修改和该事务本身做的修改。

MVCC只在已提交读(Read Committed)和可重复读(Repeatable Read)两个隔离级别下工作,其他两个隔离级别和MVCC是不兼容的。因为未提交读,总数读取最新的数据行,而不是读取符合当前事务版本的数据行。而串行化(Serializable)则会对读的所有数据多加锁。

MVCC的实现原理主要是依赖每一行记录中两个隐藏字段,undo log,ReadView

 

二、MVCC相关的一些概念

这里我们先来理解下有关MVCC相关的一些概念,这些概念都理解后,我们会通过实际例子来演示MVCC的具体工作流程是怎么样的。

1、事务版本号

事务每次开启时,都会从数据库获得一个自增长的事务ID,可以从事务ID判断事务的执行先后顺序。这就是事务版本号。

也就是每当begin的时候,首选要做的就是从数据库获得一个自增长的事务ID,它也就是当前事务的事务ID。

2、隐藏字段

对于InnoDB存储引擎,每一行记录都有两个隐藏列trx_idroll_pointer,如果数据表中存在主键或者非NULL的UNIQUE键时不会创建row_id,否则InnoDB会自动生成单调递增的隐藏主键row_id。

列名 是否必须 描述
row_id 单调递增的行ID,不是必需的,占用6个字节。 这个跟MVCC关系不大
trx_id 记录操作该行数据事务的事务ID
roll_pointer 回滚指针,指向当前记录行的undo log信息

这里的记录操作,指的是insert|update|delete。对于delete操作而已,InnoDB认为是一个update操作,不过会更新一个另外的删除位,将行表示为deleted,并非真正删除。

3、undo log

undo log可以理解成回滚日志,它存储的是老版本数据。在表记录修改之前,会先把原始数据拷贝到undo log里,如果事务回滚,即可以通过undo log来还原数据。或者如果当前记录行不可见,可以顺着undo log链找到满足其可见性条件的记录行版本。

在insert/update/delete(本质也是做更新,只是更新一个特殊的删除位字段)操作时,都会产生undo log。

在InnoDB里,undo log分为如下两类:

1)insert undo log : 事务对insert新记录时产生的undo log, 只在事务回滚时需要, 并且在事务提交后就可以立即丢弃。

2)update undo log : 事务对记录进行delete和update操作时产生的undo log,不仅在事务回滚时需要,快照读也需要,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被删除。

undo log有什么用途呢?

1、事务回滚时,保证原子性和一致性。
2、如果当前记录行不可见,可以顺着undo log链找到满足其可见性条件的记录行版本(用于MVCC快照读)。

4、版本链

多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本,然后通过回滚指针(roll_pointer),连成一个链表,这个链表就称为版本链。如下:

5、快照读和当前读

快照读: 读取的是记录数据的可见版本(有旧的版本)。不加锁,普通的select语句都是快照读,如:

select * from user where id = 1;

当前读:读取的是记录数据的最新版本,显式加锁的都是当前读

select * from user where id = 1 for update;
select * from user where id = 1 lock in share mode;

6、ReadView

ReadView是事务在进行快照读的时候生成的记录快照, 可以帮助我们解决可见性问题的

如果一个事务要查询行记录,需要读取哪个版本的行记录呢? ReadView 就是来解决这个问题的。 ReadView 保存了当前事务开启时所有活跃的事务列表。换个角度,可以理解为: ReadView 保存了不应该让这个事务看到的其他事务 ID 列表。

ReadView是如何保证可见性判断的呢?我们先看看 ReadView 的几个重要属性

  • trx_ids: 当前系统中那些活跃(未提交)的读写事务ID, 它数据结构为一个List。(重点注意:这里的trx_ids中的活跃事务,不包括当前事务自己和已提交的事务,这点非常重要)

  • low_limit_id: 目前出现过的最大的事务ID+1,即下一个将被分配的事务ID。

  • up_limit_id: 活跃事务列表trx_ids中最小的事务ID,如果trx_ids为空,则up_limit_id 为 low_limit_id。

  • creator_trx_id: 表示生成该 ReadView 的事务的 事务id

访问某条记录的时候如何判断该记录是否可见,具体规则如下:

  • 如果被访问版本的 事务ID = creator_trx_id,那么表示当前事务访问的是自己修改过的记录,那么该版本对当前事务可见;
  • 如果被访问版本的 事务ID < up_limit_id,那么表示生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。
  • 如果被访问版本的 事务ID > low_limit_id 值,那么表示生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。
  • 如果被访问版本的 事务ID在 up_limit_id和m_low_limit_id 之间,那就需要判断一下版本的事务ID是不是在 trx_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;
    如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。

画张图来理解下

这里需要思考的一个问题就是 何时创建ReadView?

上面说过,ReadView是来解决一个事务需要读取哪个版本的行记录的问题的。那么说明什么?只有在select的时候才会创建ReadView。但在不同的隔离级别是有区别的:

在RC隔离级别下,是每个select都会创建最新的ReadView;而在RR隔离级别下,则是当事务中的第一个select请求才创建ReadView(下面会详细举例说明)。

那insert/update/delete操作呢?

这样操作不会创建ReadView。但是这些操作在事务开启(begin)且其未提交的时候,那么它的事务ID,会存在在其它存在查询事务的ReadView记录中,也就是trx_ids中。

 

三、MVCC实现原理分析

1、如何查询一条记录

  1. 获取事务自己事务ID,即trx_id。(这个也不是select的时候获取的,而是这个事务开启的时候获取的 也就是begin的时候)
  2. 获取ReadView(这个才是select的时候才会生成的)
  3. 数据库表中如果查询到数据,那就到ReadView中的事务版本号进行比较。
  4. 如果不符合ReadView的可见性规则, 即就需要Undo log中历史快照,直到返回符合规则的数据;

InnoDB 实现MVCC,是通过ReadView+ Undo Log 实现的,Undo Log 保存了历史快照,ReadView可见性规则帮助判断当前版本的数据是否可见。

2、MVCC是如何实现读已提交和可重复读的呢?

其实其它流程都是一样的,读已提交和可重复读唯一的区别在于:在RC隔离级别下,是每个select都会创建最新的ReadView;而在RR隔离级别下,则是当事务中的第一个select请求才创建ReadView。

看完下面这个例子你应该就明白了。

四、经典面试题:MVCC能否解决了幻读问题呢?

有关这个问题查了很多资料,有的说能解决,有的说不能解决,也有人说能解决部分幻读场景。这里部分解决指的是能解决快照读的幻读问题,不能解决当前读的幻读问题。

 

先说我的结论:

MVCC能解决不可重复读问题,但是不能解决幻读问题,不论是快照读和当前读都不能解决。RR级别解决幻读靠的是锁机制,而不是MVCC机制。

既然网上那么多人说,MVCC解决能解决快照读下的幻读问题, 那这里通过举示例来说明,MVCC解决不了快照读的幻读问题。

假设有张用户表,这张表的 id 是主键。表中一开始有4条数据。

这里是在RR级别下研究(可重复读)。

1、事务A,查询是否存在 id=5 的记录,没有则插入,这是我们期望的正常业务逻辑。

2、这个时候 事务B 新增的一条 id=5 的记录,并提交事务。

3、事务A,再去查询 id=5 的时候,发现还是没有记录。

上面的文章是这样来举例说明,事务A第一次和第二次读到的是一样的,所以认为解决了幻读。我不认为这个是解决了幻读,而是解决了不可能重复读。它保证了第一次和第二次所读到的结果是一样的。

解决幻读了吗?显然没有,因为这个时候如果事务A执行一条插入操作

INSERT INTO `user` (`id`, `name`, `pwd`) VALUES (5, '田七', 'fff');

最终 事务A 提交事务,发现报错了。这就很奇怪,查的时候明明没有这条记录,但插入的时候 却告诉我 主键冲突,这就好像幻觉一样。这才是幻读问题。

所以说MVCC是不能解决的,要想解决还是需要锁。

这里事务A能正常的插入的前提就是其它事务不能插入id=5并提交成功。要解决这个问题也很简单,就是事务A先获得id=5这个排它锁。

我们可以在事务A第一次查询的时候加一个排他锁

select *  from `user` where id = 5 for update

那么事务B的插入动作永远属于堵塞状态,直到事务A插入成功,并提交。那么最终是事务B报主键冲突而回滚。但事务A不会因为查询的时候没有这条记录,插入失败。也就解决了幻读问题。

所以说 RR级别下解决幻读问题靠的是锁机制,而不是MVCC机制。

 

幻读是什么?

  “幻读”指,同一个事务里面连续执行两次同样的sql语句,可能导致不同结果的问题,第二次sql语句可能会返回之前不存在的行。

可以解决的情况
mysql里面实际上有两种读,一种是“快照读”,比如我们使用select进行查询,就是快照读,在“快照读”的情况下是可以解决“幻读”的问题的。使用的就是MVCC,就是mvcc利用历史版本信息(快照)来控制他能读取的数据的范围。

另外一种读是:“当前读”。对于会对数据修改的操作(update、insert、delete)都是采用当前读的模式,此外,下面两个语句也是当前读:

1、select * from table where ? lock in share mode; (加共享锁)

2、select * from table where ? for update; (加排它锁)

因此总结一下,下面几个语句都是当前读,都会读取最新的快照数据,都会加锁(除了第一个加共享锁,其他都是互斥锁):

select * from table where ? lock in share mode; 
select * from table where ? for update; 
insert; 
update; 
delete;

在执行这几个操作时会读取最新的记录,即使是别的事务提交的数据也可以查询到。比如要update一条记录,但是在另一个事务中已经delete掉这条数据并且commit了,如果update就会产生冲突,所以在update的时候需要知道最新的数据。读取的是最新的数据,并且需要加锁(排它锁或者共享锁)。

 

 MVCC下的幻读

幻读是⼀个事务按照某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录,⽽这个记录来⾃另⼀个事务添加的新记录,也就是说幻读是指新插⼊的⾏。
在 REPEATABLE READ 隔离级别下,事务 A 第⼀次执⾏普通的 SELECT 语句时⽣成了⼀个 ReadView(且在 RR 下只会⽣成⼀个RV),之后事务 B 向 user 表中新插⼊⼀条记录并提交。
ReadView 并不能阻⽌事务 A 执⾏ UPDATE 或者 DELETE 语句来改动这个新插⼊的记录(由于事务 B 已经提交,因此改动该记录并不会造成阻塞),但是这样⼀来,这条新记录的 trx_id 隐藏列的值就变成了事务 A 的事务 id。之后 A 再使⽤普通的 SELECT 语句去查询这条记录时就可以看到这条记录了,也就可以把这条记录返回给客户端。
因为这个特殊现象的存在,我们也可以认为 MVCC 并不能完全禁⽌幻读。

解决幻读问题

我们知道数据库的读操作分为当前读和快照读,⽽在 RR 隔离级别下,MVCC 解决了在快照读的情况下的幻读,⽽在实际场景中,我们可能需要读取实时的数据,⽐如在银⾏业务等特殊场景下,必须是需要读取到实时的数据,此时就不能快照读。
毫⽆疑问,在并发场景下,我们可以通过加锁的⽅式来实现当前读,⽽在 MySQL 中则是通过Next-Key Locks来解决幻读的问题。
Next-Key Locks包含两部分:记录锁(⾏锁,Record Lock),间隙锁(Gap Locks)。记录锁是加在索引上的锁,间隙锁是加在索引之间的。
Record Lock记录锁,单条索引记录上加锁。
Record Lock 锁住的永远是索引,不包括记录本⾝,即使该表上没有任何索引,那么innodb会在后台创建⼀个隐藏的聚集主键索引,那么锁住的就是这个隐藏的聚集主键索引。
记录锁是有 S 锁和 X 锁之分的,当⼀个事务获取了⼀条记录的 S 型记录锁后,其他事务也可以继续获取该记录的 S 型记录锁,但不可以继续获取 X 型记录锁;当⼀个事务获取了⼀条记录的 X 型记录锁后,其他事务既不可以继续获取该记录的 S 型记录锁,也不可以继续获取 X型记锁。
Gap Locks间隙锁,对索引前后的间隙上锁,不对索引本⾝上锁。前开后开区间。
MySQL 在 REPEATABLE READ 隔离级别下是可以解决幻读问题的,解决⽅案有两种。

 可以使⽤ MVCC ⽅案解决
也可以采⽤加锁⽅案解决(间隙锁)。
但是在使⽤加锁⽅案解决时有问题,就是事务在第⼀次执⾏读取操作时,那些幻影记录尚不存在,我们⽆法给这些幻影记录加上记录锁。所以我们可以使⽤间隙锁对其锁。
索引对间隙锁会产⽣什么影响?

1 对主键或唯⼀索引,如果当前读时,where 条件全部精确命中(=或in),这种场景本⾝就不会出现幻读,所以只会加锁,也就是说间隙锁会退化为⾏锁(记录锁)。
2 ⾮唯⼀索引列,如果 where 条件部分命中(>、<、like等)或者全未命中,则会加附近间隙锁。例如,某表数据如下,⾮唯⼀索引2,6,9,9,11,15。如下语句要操作⾮唯⼀索引列 9 的数据,间隙锁将会锁定的列是(6,11],该区间内⽆法插⼊据。
3  对于没有索引的列,当前读操作时,会加全表间隙锁,⽣产环境要注意。

 

小结:

MySQL InnoDB的可重复读并不保证避免幻读,需要应⽤使⽤加锁读来保证。⽽这个加锁读使⽤到的机制就是next-key locks。Read Committed隔离级别:每次select都⽣成⼀个快照读。
Read Repeatable隔离级别:开启事务后第⼀个select语句才是快照读的地⽅,⽽不是⼀开启事务就快照读。
在RR级别下,快照读是通过MVVC(多版本控制)和undo log来实现的,当前读是通过加record lock(记录锁)和gap lock(间隙锁)来实现的。
在mysql中,提供了两种事务隔离技术,第⼀个是mvcc,第⼆个是next-key技术。这个在使⽤不同的语句的时候可以动态选择。不加lock inshare mode之类的快照读就使⽤mvcc。否则当前读使⽤next-key。

mvcc的优势是不加锁,并发性⾼。缺点是不是实时数据。
next-key的优势是获取实时数据,但是需要加锁。

在rr级别下,mvcc完全解决了重复读,但并不能真正的完全避免幻读,只是在部分场景下利⽤历史数据规避了幻读
要完全避免幻读,需要⼿动加锁将快照读调整为当前读(mysql不会⾃动加锁),然后mysql使⽤next-key locks完全避免了幻读,⽐如rr下,锁1(0,2,3,4),另⼀个线程的insert 3即被阻塞,在rc下,另⼀个线程仍然可以⼤摇⼤摆的插⼊,如本线程再次查询⽐如count,则会不⼀致。

 

原文地址:http://www.cnblogs.com/yizhiamumu/p/16804566.html

1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长! 2. 分享目的仅供大家学习和交流,请务用于商业用途! 3. 如果你也有好源码或者教程,可以到用户中心发布,分享有积分奖励和额外收入! 4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解! 5. 如有链接无法下载、失效或广告,请联系管理员处理! 6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需! 7. 如遇到加密压缩包,默认解压密码为"gltf",如遇到无法解压的请联系管理员! 8. 因为资源和程序源码均为可复制品,所以不支持任何理由的退款兑现,请斟酌后支付下载 声明:如果标题没有注明"已测试"或者"测试可用"等字样的资源源码均未经过站长测试.特别注意没有标注的源码不保证任何可用性