内江建设网站,民宿企业安全文化建设,移动网站视频主持人网,一套企业网站设计图片MVCC是什么#xff1f;
MVCC#xff0c;是MultiVersion Concurrency Control的缩写#xff0c;翻译成中文就是多版本并发控制#xff0c;多个事务同时访问同一数据时#xff0c;调控每一个事务获取到数据的具体版本。和数据库锁一样#xff0c;它也是一种并发控制的解决…MVCC是什么
MVCC是MultiVersion Concurrency Control的缩写翻译成中文就是多版本并发控制多个事务同时访问同一数据时调控每一个事务获取到数据的具体版本。和数据库锁一样它也是一种并发控制的解决方案。主要用于解决脏读、可重复读和部分幻读问题所以MVCC主要被运用于读提交、可重复读这两种事务隔离级别下。 MVCC解决并发读写问题主要依赖于UndoLog、隐藏字段和ReadView读视图这三个部分。其中最重要的就是ReadView和隐藏字段。 MVCC解决了哪些问题
在读未提交事务隔离级别下直接读取UndoLog版本链中的最新记录即可。
在读提交事务隔离级别下通过MVCC中每次读取数据的时候生成一次ReadView解决脏读问题
在可重复读事务隔离级别下通过MVCC中每次开启一次事务的时候生成一次ReadView解决不可重复读问题
在可重复读事务隔离级别下通过MVCC中每次开启一次事务的时候生成一次ReadView读视图后续的查询都是根据这个ReadView读视图来判断哪些数据是可读的即使中途有其他事务插入了新纪录也是查询不出来这条数据的所以就很好了避免幻读问题。
快照读与当前读
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能用更好的方式去处理 读-写冲突 做到即使有读写冲突时也能做到 不加锁 即非阻塞并发读 而这个读指的就是 快照读 , 而非 当前读 。当前读实际上是一种加锁的操作是悲观锁的实现。而MVCC本质是采用乐观锁思想的一种方式。
快照读
快照读又叫一致性读读取的是快照数据。不加锁的简单的 SELECT 都属于快照读即不加锁的非阻塞 读比如这样
SELECT * FROM player WHERE ...
之所以出现快照读的情况是基于提高并发性能的考虑快照读的实现是基于MVCC它在很多情况下 避免了加锁操作降低了开销。
既然是基于多版本那么快照读可能读到的并不一定是数据的最新版本而有可能是之前的历史版本。
快照读的前提是隔离级别不是串行级别串行级别下的快照读会退化成当前读。
当前读
当前读读取的是记录的最新版本最新数据而不是历史版本的数据读取时还要保证其他并发事务 不能修改当前记录会对读取的记录进行加锁。加锁的 SELECT或者对数据进行增删改都会进行当前 读。比如
SELECT * FROM student LOCK IN SHARE MODE; # 共享锁
SELECT * FROM student FOR UPDATE; # 排他锁
INSERT INTO student values ... # 排他锁
DELETE FROM student WHERE ... # 排他锁
UPDATE student SET ... # 排他锁
Read View 在 MVCC 里如何工作的
我们需要了解两个知识
Read View 中四个字段作用聚簇索引记录中两个跟事务有关的隐藏列
什么是ReadView?
那 Read View 到底是个什么东西
Read View 是事务开始或读取数据时创建的一致性视图记录了当前系统中活跃事务的集合。这个视图帮助事务确定哪些版本的数据是可见的。通过 Read View事务能够看到在其开始时已经提交的事务产生的数据版本而忽略未提交的事务的数据版本。 Read View 有四个重要的字段
m_ids 当前活跃的事务编号列表。指的是在创建 Read View 时当前数据库中「活跃事务」的事务id编号列表注意是一个列表“活跃事务”指的就是启动了但还没提交的事务。min_trx_id 最小活跃事务编号。指的是在创建 Read View 时当前数据库中「活跃事务」中事务 id编号最小的事务也就是 m_ids 的最小值。max_trx_id 预分配事务编号。这个并不是 m_ids 的最大值而是创建 Read View 时当前数据库中应该给下一个事务的 id 值也就是全局事务中最大的事务 id 值加1creator_trx_id ReadView 创建者的事务编号。指的是创建该 Read View 的事务的事务 id。
ReadView结构如下图所示 知道了 Read View 的字段我们还需要了解聚簇索引记录中的两个隐藏列。
隐藏列和Undo Log版本链
假设在账户余额表插入一条小林余额为 100 万的记录然后我把这两个隐藏列也画出来该记录的整个示意图如下 对于使用 InnoDB 存储引擎的数据库表它的聚簇索引记录中都包含下面两个隐藏列
trx_id当一个事务对某条聚簇索引记录进行改动时就会把该事务的事务 id赋值给trx_id 隐藏列roll_pointer每次对某条聚簇索引记录进行改动时都会把旧版本的记录写入到 undo 日志中然后这个隐藏列是个指针指向每一个旧版本记录于是就可以通过它找到修改前的记录。 创建student表表结构如下 假设此时插入一条新记录该记录的事务id为8那么此刻该条记录的示意图如下所示 注意insert undo只在事务回滚时起作用当事务提交后该类型的undo日志就没用了它占用的Undo Log Segment也会被系统回收也就是该undo日志占用的Undo页面链表要么被重用要么被释放。 假设之后两个事务id分别为 10 、 20 的事务对这条记录进行UPDATE 更新操作操作流程如下图 每次对记录进行改动都会记录一条undo日志每条undo日志也都有一个 roll_pointer 属性 INSERT 操作对应的undo日志没有该属性因为该记录并没有更早的版本可以将这些 undo日志 都连起来串成一个链表 对该记录每次更新后都会将旧值放到一条 undo日志 中就算是该记录的一个旧版本随着更新次数 的增多所有的版本都会被 roll_pointer 属性连接成一个链表我们把这个链表称之为 版本链 版本链的头节点就是当前记录最新的值。
每个版本中还包含生成该版本时对应的事务id。
判断规则
在创建 Read View 后我们可以将记录中的 trx_id 划分这三种情况 一个事务去访问记录的时候除了自己的更新记录总是可见之外还有这几种判断规则
如果被访问表行记录的 trx_id 值与ReadView中的 creator_trx_id 值相同意味着当前事务在访问它自己修改过的记录所以该版本可以被当前事务访问该版本的记录对当前事务可见。如果被访问表行记录的 trx_id 值小于 Read View 中的 min_trx_id 值表示这个版本的记录是在创建 Read View 前已经提交的事务生成的所以该版本的记录对当前事务可见。如果被访问表行记录的 trx_id 值大于等于 Read View 中的 max_trx_id 值表示这个版本的记录是在创建 Read View 后才启动的事务生成的所以该版本的记录对当前事务不可见。如果被访问表行记录的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之间需要判断 trx_id 是否在 m_ids 列表中 如果表行记录的 trx_id 在 m_ids 列表中表示生成该版本记录的活跃事务依然活跃着还没提交事务所以该版本的记录对当前事务不可见。如果表行记录的 trx_id 不在 m_ids列表中表示生成该版本记录的活跃事务已经被提交所以该版本的记录对当前事务可见。
判断方法
判断方法是先根据 Read View 中的 4 个重要字段先去 Undo Log 中最新的数据行进行比对如果满足下面 Read View 的判断规则中的可见规则则返回当前行的数据如果不满足则继续从Undo Log 链中查找当前行的数据的下一个历史版本数据直到找到满足的条件的数据为止如果查询完Undolog版本链发现没有满足条件的数据则返回 NULL。
可重复读是如何工作的
可重复读隔离级别是启动事务时生成一个 Read View然后整个事务期间都在用这个 Read View。
假设事务 A 事务 id 为51启动后紧接着事务 B 事务 id 为52也启动了那这两个事务创建的 Read View 如下 事务 A 和 事务 B 的 Read View 具体内容如下
在事务 A 的 Read View 中它的事务 id 是 51由于它是第一个启动的事务所以此时活跃事务的事务 id 列表就只有 51活跃事务的事务 id 列表中最小的事务 id 是事务 A 本身下一个事务 id 则是 52。在事务 B 的 Read View 中它的事务 id 是 52由于事务 A 是活跃的所以此时活跃事务的事务 id 列表是 51 和 52活跃的事务 id 中最小的事务 id 是事务 A下一个事务 id 应该是 53。
接着在可重复读隔离级别下事务 A 和事务 B 按顺序执行了以下操作
事务 B 读取小林的账户余额记录读到余额是 100 万事务 A 将小林的账户余额记录修改成 200 万并没有提交事务事务 B 读取小林的账户余额记录读到余额还是 100 万事务 A 提交事务事务 B 读取小林的账户余额记录读到余额依然还是 100 万
接下来跟大家具体分析下。
事务 B 第一次读小林的账户余额记录在找到记录后它会先看这条记录的 trx_id此时发现 trx_id 为 50比事务 B 的 Read View 中的 min_trx_id 值51还小这意味着修改这条记录的事务早就在事务 B 启动前提交过了所以该版本的记录对事务 B 可见的也就是事务 B 可以获取到这条记录。
接着事务 A 通过 update 语句将这条记录修改了还未提交事务将小林的余额改成 200 万这时 MySQL 会记录相应的 undo log并以链表的方式串联起来形成版本链如下图 你可以在上图的「记录的字段」看到由于事务 A 修改了该记录以前的记录就变成旧版本记录了于是最新记录和旧版本记录通过链表的方式串起来而且最新记录的 trx_id 是事务 A 的事务 idtrx_id 51。
然后事务 B 第二次去读取该记录发现这条记录的 trx_id 值为 51在事务 B 的 Read View 的 min_trx_id 和 max_trx_id 之间则需要判断 trx_id 值是否在 m_ids 范围内判断的结果是在的那么说明这条记录是被还未提交的事务修改的这时事务 B 并不会读取这个版本的记录。而是沿着 undo log 链条往下找旧版本的记录直到找到 trx_id 「小于」事务 B 的 Read View 中的 min_trx_id 值的第一条记录所以事务 B 能读取到的是 trx_id 为 50 的记录也就是小林余额是 100 万的这条记录。
最后当事物 A 提交事务后由于隔离级别时「可重复读」所以事务 B 再次读取记录时还是基于启动事务时创建的 Read View 来判断当前版本的记录是否可见。所以即使事物 A 将小林余额修改为 200 万并提交了事务 事务 B 第三次读取记录时读到的记录都是小林余额是 100 万的这条记录。
就是通过这样的方式实现了「可重复读」隔离级别下在事务期间读到的记录都是事务启动前的记录。
读提交是如何工作的
读提交隔离级别是在每次读取数据时都会生成一个新的 Read View。
也意味着事务期间的多次读取同一条数据前后两次读的数据可能会出现不一致因为可能这期间另外一个事务修改了该记录并提交了事务。
那读提交隔离级别是怎么工作呢我们还是以前面的例子来聊聊。
假设事务 A 事务 id 为51启动后紧接着事务 B 事务 id 为52也启动了接着按顺序执行了以下操作
事务 B 读取数据创建 Read View小林的账户余额为 100 万事务 A 修改数据还没提交事务将小林的账户余额从 100 万修改成了 200 万事务 B 读取数据创建 Read View小林的账户余额为 100 万事务 A 提交事务事务 B 读取数据创建 Read View小林的账户余额为 200 万
那具体怎么做到的呢我们重点看事务 B 每次读取数据时创建的 Read View。前两次 事务 B 读取数据时创建的 Read View 如下图 我们来分析下为什么事务 B 第二次读数据时读不到事务 A 还未提交事务修改的数据
事务 B 在找到小林这条记录时会看这条记录的 trx_id 是 51在事务 B 的 Read View 的 min_trx_id 和 max_trx_id 之间接下来需要判断 trx_id 值是否在 m_ids 范围内判断的结果是在的那么说明这条记录是被还未提交的事务修改的这时事务 B 并不会读取这个版本的记录。而是沿着 undo log 链条往下找旧版本的记录直到找到 trx_id 「小于」事务 B 的 Read View 中的 min_trx_id 值的第一条记录所以事务 B 能读取到的是 trx_id 为 50 的记录也就是小林余额是 100 万的这条记录。
我们来分析下为什么事务 A 提交后事务 B 就可以读到事务 A 修改的数据
在事务 A 提交后由于隔离级别是「读提交」所以事务 B 在每次读数据的时候会重新创建 Read View此时事务 B 第三次读取数据时创建的 Read View 如下 事务 B 在找到小林这条记录时会发现这条记录的 trx_id 是 51比事务 B 的 Read View 中的 min_trx_id 值52还小这意味着修改这条记录的事务早就在创建 Read View 前提交过了所以该版本的记录对事务 B 是可见的。
正是因为在读提交隔离级别下事务每次读数据时都重新创建 Read View那么在事务期间的多次读取同一条数据前后两次读的数据可能会出现不一致因为可能这期间另外一个事务修改了该记录并提交了事务。 可重复读事务隔离级别下使用MVCC可能发生幻读现象的场景有哪些
1. 快照读和当前读穿插的场景下使用MVCC依旧会发生幻读
当第一个事务如果先使用快照读来读取范围数据读取期间第二个事务对这个范围进行插入数据的操作然后第一个事务又使用当前读来读取这个范围的数据此时就会出现幻读问题。 例如如下场景
T1 时刻事务 A 先执行「快照读语句」select * from t_test where id 100 得到了 3 条记录。T2 时刻事务 B 往插入一个 id 200 的记录并提交T3 时刻事务 A 再执行「当前读语句」 select * from t_test where id 100 for update 就会得到 4 条记录此时也发生了幻读现象。
要避免这类特殊场景下发生幻读的现象的话就是尽量在开启事务之后马上执行 select ... for update 这类当前读的语句因为它会对记录加 next-key lock从而避免其他事务插入一条新记录。