MySQL的MVCC机制是如何解决不可重复读问题的

MySQL再可重复读隔离级别下可以解决不可重复读这个问题,再一个事务中,同样的sql查询语句再一个事务里多次执行查询结果相同,就算其它的事务对查询到的结果有修改也不会影响到当前事务sql查询语句的结果.
这个隔离性就是靠MVCC机制来保证的,MVCC:即Multi-Version Concurrency Control,多版本并发控制;对一行数据的读和写两个操作默认是不会通过加锁互斥来保证隔离性,避免频繁加锁互斥,而在串行化隔离级别为了保证较高的隔离性是通过所有操作加锁互斥来实现的.
MySQL再读已提交和可重复读这两个隔离级别下分别实现了MVCC机制.

MVCC机制:

undo日志版本链

undo日志版本链是指一行数据被多个事务依次修改过后,再每个事务修改完后,MySQL会保留修改前的数据到undo回滚日志,并且用trx_id(事务id)roll_pointer(回滚指针) 两个隐藏字段把这些undo日志串联起来形成一个历史记录版本链;
在这里插入图片描述

read view机制

当事务开启时;执行任何查询sql时会生成当前事务的一致性试图read-view,这个试图是由执行查询时所有未提交事务id数组已创建的最大事务id(max_trx_id) 组成,事务里的任何的sql查询结果需要从对应的版本链里的最新数据开始逐条跟生成的一致性试图read-view做比对从而得到最终的查询结果;但是不同的隔离级别中,生成read-view的策略不同:

  • 读已提交:每次执行查询sql时都会重新生成最新的read-view
  • 可重复读:执行事务中第一条查询sql时生成read-view,并且再事务结束之前都不会发生变化
read-view与版本链匹配规则
  1. 如果查询sql的trx_id<min_trx_id: 表示是已提交事务生成的,这个数据是可见的;
  2. 如果查询sql的trx_id>max_trx_id: 表示是由当前事务开始后的事务生成的,这个数据.是不可见的
  3. 如果查询sql的min_trx_id<=trx_id<=max_trx_id:
    3.1 若trx_id再试图数组中,表示这个版本是由还未提交的事务生成的,不可见
    3.2 若trx_id不再试图数组中,表示这个版本是已经提交了事务生成的,可见
  4. 特殊情况: 对于删除的情况可以认为是修改的特殊情况,会继续再版本链的头部添加数据,trx_id就是删除数据事务的id,同时再该条记录的头信息(record header)里的(delete_flag)标记位写true,来表示这条数据已经被删除,在查询的时候如果按照read-view与版本链匹配规则,匹配到的版本头信息中delete_flag如果为true的话,不再返回数据直接返回null;
    在这里插入图片描述

注意:start transaction/begin并不是一个事务的起点,再使用这个命令时并不会马上向MySQL申请一个trx_id,只有当事务第一次执行sql语句时,事务才会真正的启动,才会向MySQL申请trx_id,MySQL内部时严格按照事务的启动顺序来分配trx_id的.

演示匹配过程(读已提交)

在这里插入图片描述

  1. 新开启一个事务执行select * from account where id = 1;read-view就是(0001,0001)0001,版本链只有一条:0000;原始数据,插入后就没在修改过的数据;trx_id(0000) < min_trx_id(0001),表示是已提交数据生成的,可见;查询出0000中的版本数据返回
    在这里插入图片描述
  2. 新开起一个事务0002执行update account set balance = 800 where id = 1;修改id为1的数据,然后提交事务;此时0001事务再次执行select * from account where id = 1;进行查询,读已提交的隔离级别下,每次执行查询sql时都会重新生成最新的read-view,那么此时的read-view就是(0001,0001)0002,按照比较规则先与0002对比,min_trx_id(0001) < trx_id(0002)<=max_trx_id(0002);0002是已经创建的最大试图id,并不在当前试图中,可见;将版本0002中的数据查询返回
    在这里插入图片描述
    3.新开起一个事务0003执行update account set balance = 900 where id = 1;修改id为1的数据,然后提交事务;此时0001事务再次执行select * from account where id = 1;进行查询,读已提交的隔离级别下,每次执行查询sql时都会重新生成最新的read-view,那么此时的read-view就是(0001,0001)0003,按照比较规则先与0002对比,min_trx_id(0001) < trx_id(0002)<=max_trx_id(0003);0003是已经创建的最大试图id,并不在当前试图中,可见;将版本0003中的数据查询返回
    在这里插入图片描述以上面的演示为例,假设是可重复读的隔离级别,只会再第一步的时候创建一个一致性试图read-view(0001,0001)0001,
    1. 第一次查询获取版本0000的数据,
    2. 第二次查询先于0002做对比,0002<max_trx_id(0001):表示是由当前事务开始后的事务生成的,这个数据是不可见的;然后匹配0000的版本,trx_id(0000) < min_trx_id(0001),表示是已提交数据生成的,可见;查询出0000中的版本数据返回
    3. 第三次查询先与0003做对比,0003<max_trx_id(0001):表示是由当前事务开始后的事务生成的,这个数据是不可见的;再与0002做对比,0002<max_trx_id(0001):表示是由当前事务开始后的事务生成的,这个数据同样是不可见的;最后匹配0000的版本,trx_id(0000) < min_trx_id(0001),表示是已提交数据生成的,可见;查询出0000中的版本数据返回

总结:可以看的出来,再读已提交的隔离级别下,每次的查询都会重新创建一个新的read-view,导致每次查询的read-view都是最新的,所以再版本链的匹配中,总是能拿到其他事务已经提交的最新数据,造成了不可重复读问题的出现,再可重复读的情况下,就只会再执行第一条查询sql的时候创建一个read-view,并且再事务结束之前都不会发生改变;这也是为什么再可重复读的隔离界别下为什么能解决不可重复读的真正原因

Guess you like

Origin blog.csdn.net/qq_43135259/article/details/119547312