MVCC(Multi-Version Concurrency Control)多版本并发控制机制
使用串行化隔离级别时,mysql会将所有的操作加锁互斥,来保证并发安全。这种方式必然降低并发性能。mysql在读已提交和可重复读隔离级别下,对一行数据的读和写两个操作默认是不会通过加锁互斥来保证隔离性,避免了频繁加锁互斥。那么具体是如何实现的呢?首先要了解两个概念。
准备
建表语句
CREATE TABLE `product` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `price` int DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8;
undo日志版本链
我们向product表插入一条数据
INSERT INTO mysql_demo.product (id, name, price) VALUES (1, 'apple', 10);
此时mysql会同时向undo日志里写入一条记录。 trx_id为插入操作的事务id。这里随便写了一个80,意思一下。 roll_pointer后面再说。
这时候又来了一个事务,对数据进行了修改。比如事务id 300,修改price为20。此时mysql同样会在undo日志里写入一条记录。并且roll_pointer会指向前一条记录
以此类推,后续又有新的事务来操作这条记录,就会形成一条版本链,这条链就是undo日志版本链。
每条数据对应着有一个undo日志版本链。
对于insert和update操作,mysql会向undo日志里添加一条记录。select操作不会产生记录。
对于删除的情况可以认为是update的特殊情况,会将版本链上最新的数据复制一份,然后将trx_id修改成删除操作的trx_id,同时在该条记录的头信息(record header)里的(deleted_flag)标记位写上true,来表示当前记录已经被删除,在查询时按照上面的规则查到对应的记录如果delete_flag标记位为true,意味着记录已被删除,则不返回数据。
在来看下什么是read view。
一致性试图read view机制
read view的生成
可重复读隔离级别:事务开启后,首次执行任何select时会生成当前事务的read-view,在事务结束前不会变化。
读已提交隔离级别:事务开启后,每次执行select时都会重新生成read-view。
read view的组成
这个视图由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成。
我们来举个例子。
Transaction 80: 开启事务,插入一条记录。并且commit;
Transaction 100:开启事务,执行update。生成事务id 100。这里需要注意begin和select不会生成事务id,所以加了一条无关的update,生成事务id。update内容可以忽略。
begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个修改操作InnoDB表的语句,事务才真正启动,才会向mysql申请事务id
mysql内部是严格按照事务的启动顺序来分配事务id的
Transaction 200:同上
Transaction 300:把价格修改成20了。并且commit了。
select 1: select 不生成事务id。 事务开启后,首次执行任何select时会生成当前事务的read-view。
Transaction 400:把价格修改成18了。
read view的组成 = 未提交事务id数组(数组里最小的id为min_id) + 已创建的最大事务id(max_id)组成
此时未提交事务id有100,200(80 已经提交了)。最小的id为100。 已创建的最大事务id为300。(注意read view 是在第5步生成的,此时还没有Transaction 400)
因此 read view为[100,200],300 min_id为100 ,max_id为300。 [100,200] 为视图数组。
此时对应的undo日志版本链如下
那么read view 的作用是什么呢?
read view的作用
根据上面的结果,我们可以将事务进行分类。因为事务的id是有序递增的。所以我们可以得出以下结论
因为未提交事务的最小id(min_id)为100,所以小于100的事务都是已提交的。( Transaction 80)
因为已创建的最大事务id(max_id)为300,所以大于300的区域都是未开启事务。 (Transaction 400) 未开启理解为在执行select的时候没有开启。
介于min_id和max_id之间的事务,包含了未提交和已提交的事务。 (Transaction 100,200,300)
那么mysql是如何通过read view和undo日志版本链实现并发事务之间的隔离的呢?那就需要看下版本链比对规则了。
版本链比对规则
事务里的每一条select都需要从对应版本链里的最新数据开始逐条跟read-view做比对,按照比对规则得到最终的快照结果。下面我们来看下版本链比对规则。
如果 row 的 trx_id 落在绿色部分( trx_id
如果 row 的 trx_id 落在灰色部分( trx_id>max_id ),表示这个版本是由将来启动的事务生成的
row 的 trx_id 就是当前自己的事务是可见的;
否则不可见;
如果 row 的 trx_id 落在黄色部分(min_id <=trx_id<= max_id),那就包括两种情况
若 row 的 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,
若 row 的 trx_id 就是当前自己的事务是可见的
否则不可见;
若 row 的 trx_id 不在视图数组中,表示这个版本是已经提交了的事务生成的,可见。
知道了版本链的比对规则,下面我们通过实例来看下,mysql的MVCC机制是如何工作的。
实战演练
可重复读Repeatable-Read(RR)
我们先以可重复读Repeatable-Read(RR)为例
可重复读隔离级别:事务开启后,首次执行任何select时会生成当前事务的read-view,在事务结束前不会变化。
案例一
我们先以上面的情况为例来进行分析。此时的情况如下:
read view为 [100,200],300
undo日志版本链如下
套用版本链比对规则
首先在版本链中找到最新数据。
Transaction 300,trx_id = max_id。此时继续比对, trx_id 不在视图数组中,可见。
返回Transaction 300记录的数据信息。price = 20;
案例二
Transaction 400,在第10行执行了一次update。
Transaction 100,在第11,12行执行了两次update。然后select 1 13行执行了一次select。 我们来分析下这个select。
因为RR隔离级别首次执行任何select时会生成当前事务的read-view,在事务结束前不会变化。所以read view为 [100,200],300。没有变化。
undo日志版本链如下
套用版本链比对规则
首先在版本链中找到最新数据。
Transaction 100,trx_id = min_id。继续分析 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见。
第二行Transaction 100,分析同上
第三行Transaction 400, trx_id > max_id,不可见。
Transaction 300,trx_id = max_id。此时继续比对,trx_id 不在视图数组中,可见。
返回Transaction 300记录的数据信息。price = 20;
案例三
继续向下Transaction 100,在第15行commit。Transaction 200,在第15,16行执行了两次update。然后select1 17行执行了一次select。 我们来分析下这个select。
因为RR隔离级别首次执行任何select时会生成当前事务的read-view,在事务结束前不会变化。所以read view为 [100,200],300。没有变化。
undo日志版本链如下
套用版本链比对规则
首先在版本链中找到最新数据。
Transaction 200,min_id < trx_id < max_id。继续分析 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见。
下一行Transaction 200,分析同上.
Transaction 100,trx_id = min_id。继续分析 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见。
下一行Transaction 100,分析同上。
下一行Transaction 400, trx_id > max_id,不可见。
Transaction 300,trx_id = max_id。此时继续比对,trx_id 不在视图数组中,可见。
返回Transaction 300记录的数据信息。price = 20;
案例四
继续select2 17行执行了一次select。 我们来分析下这个select。
RR隔离级别首次执行任何select时会生成当前事务的read-view。read view为 [200,400],400。
undo日志版本链如下
套用版本链比对规则
首先在版本链中找到最新数据。
Transaction 200,trx_id = min_id。继续分析 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见。
下一行Transaction 200,分析同上.
Transaction 100,trx_id < min_id。表示这个版本是已提交的事务生成的,这个数据是可见的;
返回 price = 16。
案例五
我们再来看一下如果select1 如果有update操作(update操作会创建事务id,我们假设是 500)。Transaction 500 此时是如何读取到更新后的数据的。
来分析下15行。
RR隔离级别首次执行任何select时会生成当前事务的read-view,在事务结束前不会变化。read view为 [100,200],300。
undo日志版本链如下
套用版本链比对规则
首先在版本链中找到最新数据。
Transaction 400,trx_id > max_id(read view是第一次select时生成的,此时max_id仍然是 300)。表示这个版本是由将来启动的事务生成的,是不可见的
Transaction 500,trx_id > max_id。表示这个版本是由将来启动的事务生成的,但row 的 trx_id 就是当前自己的事务是可见的;所以可见
返回 price = 8。
结论:通过以上案例,我们可以知道。 MVCC机制在RR中首次查询时会固定read view。后续和其他事务隔离开了,其他事务对数据的操作不会影响到当前事务。
读已提交Read-Committed(RC)
我们再以读已提交Read-Committed(RC)为例
读已提交隔离级别:事务开启后,每次执行select时都会重新生成read-view。
案例一
第9行没有变化,我们来看第13行。
read view的组成 = 未提交事务id数组(数组里最小的id为min_id) + 已创建的最大事务id(max_id)组成
未提交事务id数组 100,200,400 ; min_id 100 ; max_id 400
read view为 [100,200,400],400。
undo日志版本链如下
套用版本链比对规则
首先在版本链中找到最新数据。
Transaction 100,trx_id = min_id。继续分析 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见。
下一行Transaction 100,分析同上.
Transaction 400, trx_id = max_id。继续分析 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见。
Transaction 300,min_id < trx_id< max_id。不在视图数组中,表示这个版本是已经提交了的事务生成的,可见。
返回 price = 20。
案例二
来看第17行。
read view的组成 = 未提交事务id数组(数组里最小的id为min_id) + 已创建的最大事务id(max_id)组成
未提交事务id数组 200,400 ; min_id 200 ; max_id 400
read view为 [200,400],400。
undo日志版本链如下
套用版本链比对规则
首先在版本链中找到最新数据。
Transaction 200, trx_id = min_id。继续分析 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见。
同上
Transaction 100, trx_id
返回 price = 16。
OK,就分析到这里吧。希望对你有所帮助!
读已提交Read-Committed(RC)
我们再以读已提交Read-Committed(RC)为例
读已提交隔离级别:事务开启后,每次执行select时都会重新生成read-view。
案例一
第9行没有变化,我们来看第13行。
read view的组成 = 未提交事务id数组(数组里最小的id为min_id) + 已创建的最大事务id(max_id)组成
未提交事务id数组 100,200,400 ; min_id 100 ; max_id 400
read view为 [100,200,400],400。
undo日志版本链如下
套用版本链比对规则
首先在版本链中找到最新数据。
Transaction 100,trx_id = min_id。继续分析 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见。
下一行Transaction 100,分析同上.
Transaction 400, trx_id = max_id。继续分析 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见。
Transaction 300,min_id < trx_id< max_id。不在视图数组中,表示这个版本是已经提交了的事务生成的,可见。
返回 price = 20。
案例二
来看第17行。
read view的组成 = 未提交事务id数组(数组里最小的id为min_id) + 已创建的最大事务id(max_id)组成
未提交事务id数组 200,400 ; min_id 200 ; max_id 400
read view为 [200,400],400。
undo日志版本链如下
套用版本链比对规则
首先在版本链中找到最新数据。
Transaction 200, trx_id = min_id。继续分析 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见。
同上
Transaction 100, trx_id
返回 price = 16。
OK,就分析到这里吧。希望对你有所帮助!