文章目录
1. 什么是MVCC机制?
首先来重新认识一下可重复读和串行化:
- 可重复读:
mysql
的默认隔离级别是可重复读,也就是在一个事务中,多次查询结果都相同。就算有其他事务修改了数据,也不会影响当前事务的查询结果。读操作不加锁,使用mvcc机制 - 串行化:读和写操作也加行锁,其他事务的所有读写操作都会被阻塞。
可重复读和串行化其实都是为了保证多次读到的数据一致,确保了隔离性。如果没有隔离性保障,那当其他事务修改数据时,java代码中的数据就在不断变化,就无法写代码了。
可重复读的这个隔离性就是靠MVCC(Multi-Version Concurrency Control)
机制来保证的,对一行数据的读和写两个操作默认是不会通过加锁互斥来保证隔离性,避免了频繁加锁互斥。而在串行化隔离级别为了保证较高的隔离性是通过将所有操作加锁互斥来实现的,频繁加锁导致效率低下!
注意:mysql在读已提交 和 可重复读 两个隔离级别都实现了MVCC
机制!
2. MVCC机制底层是怎么实现的
mvcc机制其实就是通过undo日志版本链 与 read view机制来实现的!
2.1 undo日志版本链
undo日志版本链的作用:记录某个数据历次被修改的版本,并通过回滚指针连接起来,当发生数据回滚时可以通过undo版本链恢复到以前的数据。
如图:开启一个事务(还未提交),对id = 1
的数据不停的修改,则会生成一个undo版本链,记录当前事务中id = 1
的数据的各个版本,后期可直接回滚到某个版本!
比如下完订单会减库存,减完库存之前的值会记录在undo
日志里,如果减完库存后边的业务抛异常了,数据库库存数据会回滚,怎么回滚,就是通过undo
日志版本来找到记录在undo
日志中的老数据!
一行数据被多个事务依次修改过后,在每个事务修改完后,Mysql
会保留修改前的数据undo
回滚日志,并且用两个隐藏字段trx_id
和roll_pointer
把这些undo
日志串联起来形成一个历史记录版本链(见下图)
trx_id
:事务id,用来记录当前事务的id,同一个事务中的不同操作,id一致,事务id是递增的。roll_pointer
:行指针。通过roll_pointer
来找到上一次修改的行,方便日志回滚和read_view
判断(下边会讲)
注意:
begin
开启事务命令并不是一个事务的起点,在执行到它们之后的第一个修改操作InnoDB表的语句, 事务才真正启动,才会向mysql申请事务id,mysql内部是严格按照事务的启动顺序来分配事务id的。
- ①:undo日志版本链同一条数据只有一个
- ②:查询不会产生undo日志版本链
2.2 read view机制
read view一致性视图的生成时机:
- ①:在可重复读隔离级别,当前事务中,每次执行查询sql时都只会使用第一次生成的一致性视图
read-view
,该read-view
视图在事务结束前都不会变化。注意:begin
开启事务时并不会生成一致性视图,只有查询语句开始执行时才会生成 - ②:在读已提交隔离级别,当前事务中,每次执行查询sql时都会生成一个一致性视图
read-view
,该read-view
视图在每次查询时都会重新生成。
由以上两点区别,导致不同的隔离级别查询结果不一致!
一致性视图 read-view 由什么组成?
read-view一般由两部分组成:
- ①:一部分由执行查询时所有未提交
事务id数组 =【100,200】
(数组里最小的id为min_id) - ②:另一部分是已提交的
最大事务id = 300
(max_id),因为trx_id在数据库中是递增的。
如下图:
如上图所示,
- 只有查询事务开启的时候会生成一个一致性视图read-view,【100,200】,300。
- 而undo版本链中存储的是每一次修改的结果(包括已提交和未提交的操作)
- 事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。
3. undo版本链和read-view的对比规则
可见:可查询到的数据
不可见:不可查询到的数据
在查询事务中的对比规则如下:
-
①:从
undo
日志版本链中自上而下取第一个数据版本的事务id,拿着事务id(trx_id
)去跟read-view
的三个区域做对比,三个区域如下图 -
②:如果
trx_id
落在绿色部分( trx_id <read-view
的min_id 100
),表示这个版本是已提交的事务生成的,这个数据是可见的; -
③:如果
trx_id
落在红色部分( trx_id>read-view
的max_id 300
),表示这个版本是由将来启动的事务生成的,是肯定不可见的; -
④:.如果
trx_id
落在黄色部分(min_id 100 <= trx_id <= max_id 300
),那就包括两种情况 :- --------a. 若
trx_id
在视图数组【100,200】
中,表示这个版本是由还没提交的事务生成的,不可见,当前自己的事务是可见的; - --------b. 若
trx_id
不在视图数组【100,200】
中,表示这个版本是已经提交了的事务生成的,可见。
- --------a. 若
4. 结合案例解释mvcc机制实现可重复读过程
如下图所示红框内select1的read-view视图内容包括未提交的事务【100,200】和已提交的最大事务 300。所以read-view为【100,200】,300
对应的undo版本链和一致性视图如下所示:
由undo版本链一致性视图read-view对比即可查得当前数据!
查询流程如下:
- ①:事务
100 、200、300
由于是对id = 1
的数据的修改操作,修改记录会被被记录在undo
日志版本链中 - ②:然后开启一个事务,查询
id = 1
的数据,此次查询操作在可重复读隔离级别下生成了一个read-view
一致性视图,该read-view
视图由 未提交的事务id数组
和已创建的最大事务id
,为:【100,200】,300
- ③:查询结果就是
undo
日志版本链 与read-view
对比的结果,对比过程就是如下:- (1)mysql底层根据read-view
【100,200】300
生成三个范围区间,如上图所示。 - (2)然后去
undo
版本链中从上向下找到第一个数据版本,也就是1 - lilei300 - 300
这个数据版本,因为id = 300
这个事务最后修改的数据,位于undo
日志版本链的最顶端,所以被第一个取出,取出事务id300.
- (3)拿事务id
300
去范围区间内查找。如果落在绿色部分(事务id<100
),则直接可见,查询结束.但查找结果发现300
落在黄色部分【100,200】,300
,此时还有两种情况:- ------ a:如果事务id落在未提交事务数组
【100,200】
中,则代表结果不可见,300
并未落在该区间 - ------ b:如果事务id没有落在未提交事务数组
【100,200】
中,则代表已提交的事务,结果是可见的。300
刚好落在黄色取域【100,200】,300
中的未提交事务数组【100,200】
之外,对比结果就是undo
日志版本链上的事务id = 300
的这条数据在本次查询中是可见的!
- ------ a:如果事务id落在未提交事务数组
- (1)mysql底层根据read-view
- ④:查询结果因为等于
undo
日志版本链 与read-view
对比的结果,所以查询结果就是lilei300
后续有别的查询同样根据这些规则去比对 产生查询结果!
注意:
- 可重复读隔离级别下:一个事务中,多次查询的read-view不变
- 读已提交隔离级别下:一个事务中,每次查询的read-view都会改变!