当前位置: 首页 > 后端技术 > Java

所有相同条件的mysqlselect语句,为什么读出的内容不一样?

时间:2023-04-01 19:08:20 Java

假设当前数据库中存在下表。按照老规矩,在innodb引擎的repeatablereadisolationlevel下,默认还是会出现下面的内容。可以看到,线程1也读取了age>=3的数据。1条数据是第一次读取,这是原始状态。之后线程2也把id=2的age字段改成了3,此时线程1读取了两次,一次读取的结果还是原来的,而另一次读取的结果是两次,不同的是是否添加或不更新。为什么同样的条件下,都读出来了,读出来的数据却不一样呢?可重复阅读不是要求每次读出的内容都一样吗?要回答这个问题。我需要从盘古如何创造世界的话题开始。打扰一下。我发脾气了。然后开始说事务是如何回滚的。事务回滚是如何实现的?当我们执行一个事务时,一般遵循begin格式;操作1;操作2;操作3;xxxxx....提交;一个操作,可以包含各种逻辑。只要是在执行逻辑,就有可能报错。回想一下事务的ACID里面有个A,原子性,整个事务是一个整体,要么一起成功,要么一起失败。如果失败了,就需要让执行到一半的事务有能力回到事务未执行前的状态。这是回滚。执行事务的代码类似于以下内容。开始;尝试:操作1;操作2;操作3;xxxxx....提交;除了异常:回滚;复制代码如果执行rollback可以回到事务执行前的状态,说明mysql需要知道一些行,执行事务前的数据是什么样子的。数据库是怎么做到的?这是关于undolog,记录了一行数据,以及事务执行前的情况。比如id=1的那一行数据,如果name字段从“小白”更新为“小白调试”,就会增加一个undolog来记录之前的数据。由于可以有很多并发执行的事务,undolog也可能有很多,在log中加入事务的id(trx_id)字段,表示哪个undolog是在哪个事务下产生的。同时将它们以链表的形式组织起来,在undolog中加入一个指针(roll_pointer)指向之前的undolog,从而形成版本链。有了这个版本链,当一个事务在执行中途失败,会直接回滚。这时候可以跟随版本链,回到执行事务之前的状态。什么是当前读取和快照读取?有了上面的undologversionchain,我们可以在表头看到最新的数据,以及之后的所有旧数据版本。不管是最新的还是旧的数据版本,我们都称之为数据快照。当前读取,读取的是版本链的头部,也就是最新的数据。快照读取是指读取版本链中的其中一个快照。当然,如果快照刚好是表头,那么快照读取的结果和当前读取的结果是一样的。我们平时执行的普通select语句,比如下面的,就是快照读。从phone_no=2的用户中选择*;复制代码和特殊的select语句,比如share模式加锁,select后更新,都属于本期阅读。另外,insert、update、delete操作都是写操作。既然是写,肯定是在写最新的数据,所以才会触发当前的读。那么问题来了。当前读取读取的是版本链的头部,那么当当前读取执行时,是否有可能其他事务恰好生成更新的快照来替换当前头部,成为新的头部?不是这次吗?你不是在看最新的数据吗?答案是否定的,不管是select...forupdate这样的(特殊)读操作,还是insert、update这样的写操作,都会锁定这一行数据。undologsnapshots的产生也是在写操作的情况下产生的,在执行写操作之前也必须先获取锁。因此写操作需要阻塞等待当前读操作完成,只有拿到锁后才能更新版本链。读视图数据库中可以并发执行很多事务,每个事务都会分配一个事务ID,是递增的,越新的事务ID越大。对于数据表中一行数据的undolog版本链,每条undolog也有一个transactionid(trx_id),也就是创建undolog的transactionid。并不是所有的事务都会产生undolog,也就是说一行数据的undolog版本链中只有一些事务id。但是所有的交易都可能访问到这行数据对应的版本链。而且,虽然版本链上有很多undolog快照,但并不是所有的undolog都可以读取。毕竟有些undo日志还没有被创建它们的事务提交,随时可能失败回滚。现在问题来了,现在有一个事务通过快照读取来读取undolog版本链,它能读取哪些快照呢?它应该读取哪个快照?这里我们将介绍阅读视图的概念。它就像一个有上限和下限的滑动窗口。整个数据库有那么多事务,这些事务又分为committed和uncommitted。未提交的意思是这些事务还在进行中,也就是所谓的活跃事务。所有活动交易的id形成m_ids。而最小的事务id就是读视图的下边界,称为min_trx_id。在读视图生成的那一刻,将所有事务中最大的事务id加1,即读视图的上边界,称为max_trx_id。概念太多,有点乱?没关系,继续往下看,后面会有例子。事务可以读取哪些快照?有了这些基本信息,我们先来看看一个事务在读视图中可以读到哪些快照。记住一个大前提:一个事务只能读取自己产生的undolog数据(事务提交与否无关紧要),或者其他事务已经提交的数据。既然事务(姑且称之为事务A)有了读视图,那么无论我们看哪个undolog版本链,我们都可以把读视图放在版本链上。版本链分为几个部分。版本链快照的trx_id<读视图的min_trx_id从上面的描述我们可以知道,读视图的m_id来自数据库中所有活跃事务的id,最小的min_trx_id是读视图,因为事务id是按时间递增的,所以如果版本链快照的trx_id小于min_trx_id,那么这些一定是inactive(committed)的事务id,这些快照可以被事务A读取版本链快照的trx_id>=readview的max_trx_idmax_trx_id是在事务A创建readview的那一刻产生的,比当时所有数据库已知的transactionid都大。所以如果undolog版本链上的一个快照包含一个大于max_trx_id的trx_id,说明这个快照超出了事务A的“理解范围”,不应该被读取。min_trx_idofreadview<=versionchainsnapshottrx_id=3)返回。此时,事务2假设其事务trx_id=3,执行更新操作,生成新的undolog快照。此时线程1第二次执行了普通select,仍然是snapshotread。由于是可重复读,所以会复用之前的读视图,再次执行读操作。这里,重点关注id=2的那一行数据,从versionlistheader开始遍历,readview的第一个snapshottrx_id=3>=max_trx_id=3,所以不可读,遍历下一个snapshottrx_id=1=3。但是线程1第三次读取,执行了selectforupdate,变成了当前读取。它直接读取undolog版本链中最新的行快照,所以可以读取到id=2,age=3,所以最终返回的结果满足age>=3的数据有2条。总的来说,由于快照读取和当前读取的数据读取规则不同,我们看到的结果是不一样的。看到这里大家应该明白了,所谓的可重复读肯定是每次都读到相同的数据,这里的“读”指的是快照读。如果下次面试官问你,在repeatableread隔离级别下,每次读取的数据都是一样的吗?你应该知道怎么回答吧?总结事务通过undolog实现回滚功能,从而实现事务的原子性(Atomicity)。多个事务产生的undolog形成一个版本链。读取快照时,事务根据读视图决定读取哪个快照。当前读时事务直接读取最新的快照版本。MySQL的InnoDB引擎通过MVCC提高了读写并发性。最后,原创文章和更新文章的阅读量最近都在稳步下降。想了想,晚上辗转反侧。我有一个不成熟的要求。离开广东好久了,好久没有人叫我花花公子了。大家可以在评论区叫我帅哥吗?我善良单纯的愿望能实现吗?如果真的不会说,能不能帮我点个右下角的点赞看一下?废话不多说,一起畅游在知识的海洋里