MySQL 千万数据量深分页优化,微服务架构技术栈

我们每一次对数据记录的改动,MySQL都会记录一条日志,我们把它称作undo日志,每一条undo日志对应着也都有一个roll_pointer属性(insert操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表,所以现在的情况就像下图一样:

image.png

对这条记录每次更新后,都会将旧记录放入到undo日志中,就算是该记录的一个历史版本,随着更新次数的一次次增加,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为【版本链】,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务id,这个ID(事务ID)非常重要,后续事务的隔离级别实现原理都是围绕这个ID(事务ID)来的。

ReadView

** 对于使用【读未提交READ_UNCOMMITTED】这种隔离级别的事务来说,直接读取记录的最新版本就好了,对于使用【串行化SERIALIZABLE】隔离级别的事务来说,使用加锁的方式来访问记录。对于使用【读已提交READ COMMITTED】和【可重复读REPRATABLE_READ】隔离级别的事务来说,就需要用到我们上边所说的【版本链】了,核心的问题就是:我们需要判断版本链中的数据,哪个版本是当前事务可见的。所以设计MySQL官方提出了一个ReadView的概念,这个ReadView中主要包含当前MySQL中还有哪些活跃的读写事务,把它们的事务id放到一个列表中,我们把这个列表命名为为m_ids(一个数组)。这样在我们访问某一条记录时,只需要按照下边的步骤判断记录的某个版本是否可见(官方设计规则哦):

  • 如果被访问版本的trx_id属性值小于m_ids列表中最小的事务id,表明生成该版本的事务在生成ReadView前已经提交,所以该版本可以被当前事务访问。

  • 如果被访问版本的trx_id属性值大于m_ids列表中最大的事务id,表明生成该版本的事务在生成ReadView后才生成,所以该版本不可以被当前事务访问。

  • 如果被访问版本的trx_id属性值在m_ids列表中最大的事务id和最小事务id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。

如果某个版本

【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】

开源完整内容戳这里

的数据对当前事务不可见的话,那就顺着版本链继续去找下一个版本的数据记录,依然按照我们上边所说的步骤判断数据是否可见,依此类推,一直到版本链中的最后一个版本数据,如果最后一个版本的数据我也不可见的话,那么也就意味着该条记录对该事务不可见,查询结果就不包含该记录。 在MySQL当中,READ COMMITTED(读已提交)和REPEATABLE READ(可重复读)隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同,我们来具体举例看一下喽。 按照上面我们画的版本链,来具体分析一下,这个版本链是怎么一步步生成的,以及我们查询的时候,MySQL是怎么来通过版本链决定数据我们是否可读(可见)的。 –[1]–【R****EAD COMMITTED — 每次读取数据前都生成一个ReadView】 假设说现在系统里有一个id为100的事务在执行:

Transaction 100

BEGIN;

UPDATE t SET name = ‘小B’ WHERE id = 1;

UPDATE t SET name = ‘小C’ WHERE id = 1;

注意哦:我们这个事务,我并没有提交。没有commit指令哦

复制代码

Transaction 200

BEGIN;

更新了一些别的表的记录

贴心小课堂:事务执行过程中,只有在第一次真正修改记录时(比如使用INSERT、DELETE、UPDATE语句),才会被分配一个单独的事务id,这个事务id是递增的。

此刻,表t中id为1的记录得到的版本链表如下所示:

image.png

千万注意,我上面事务100,还没提交哦,我可没有执行commit指令。 假设现在有一个使用READ COMMITTED(读已提交)隔离级别的事务开始执行:

使用READ COMMITTED隔离级别的事务(读已提交)

BEGIN;

SELECT1:Transaction 100、200未提交

SELECT * FROM t WHERE id = 1; # 得到的列name的值为’小A’

这个SELECT1的执行流程如下:

  • 在执行SELECT语句时会首先生成一个ReadView,ReadView的m_ids数组列表的内容就是[100,200]。

  • 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是’小C’,该版本的trx_id值为100,在m_ids列表内,所以不符合我们的可见性要求,根据roll_pointer跳到下一个版本。

  • 下一个版本的列name的内容是’小B’,该版本的trx_id值也为100,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。

  • 下一个版本的列name的内容是’小A’,该版本的trx_id值为80,小于m_ids列表中最小的事务id100,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name为’小A’的记录。

之后,我们把事务id为100的这个事务提交一下,如下:

Transaction 100

BEGIN;

UPDATE t SET name = ‘小B’ WHERE id = 1;

UPDATE t SET name = ‘小C’ WHERE id = 1;

COMMIT; //提交了哦

然后再到事务id为200的事务中更新一下表t中id为1的记录:

Transaction 200

BEGIN;

更新了一些别的表的记录

UPDATE t SET name = ‘小D’ WHERE id = 1;

UPDATE t SET name = ‘小F’ WHERE id = 1;

此刻,表t中id为1的记录的版本链就长这样:

image.png

然后再到刚才使用READ COMMITTED隔离级别的事务中继续查找这个id为1的记录,如下:

使用READ COMMITTED隔离级别的事务

BEGIN;

SELECT1:Transaction 100、200均未提交的时候执行的查询

SELECT * FROM t WHERE id = 1; # 得到的列name的值为’小A’

SELECT2:Transaction 100提交,Transaction 200未提交的时候执行的查询

SELECT * FROM t WHERE id = 1; # 得到的列name的值为’小C’

这个SELECT2的执行过程如下:

  • 在执行SELECT语句时会先生成一个ReadView,ReadView的m_ids列表的内容就是[200](事务id为100的那个事务已经提交了,所以生成快照时就没有它了)。

  • 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是’小F’,该版本的trx_id值为200,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。

  • 下一个版本的列name的内容是’小D’,该版本的trx_id值为200,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。

  • 下一个版本的列name的内容是’小C’,该版本的trx_id值为100,比m_ids列表中最小的事务id200还要小,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name为’小C’的记录。

以此类推,如果之后事务id为200的记录也提交了,再此在使用READ COMMITTED隔离级别的事务中查询表t中id值为1的记录时,得到的结果就是’小F’了,具体流程我们就不分析了。总结一下就是:使用READ COMMITTED隔离级别的事务在每次查询开始时都会生成一个独立的ReadView。 说完了隔离级别为【读已提交】不知道你理解了没有?如果不理解,烦请联系我,我们一起进行探讨。 接下来我们就来看一下当事务隔离级别为【可重复读】的时候,MVCC是如何控制数据可见性的。 –[2]–****【REPEATABLE READ —在第一次读取数据时生成一个ReadView】 对于使用REPEATABLE READ隔离级别的事务来说,只会在第一次执行查询语句时生成一个ReadView,之后的查询就不会重复生成了。我们还是用例子看一下是什么效果。 比方说现在系统里有两个id分别为100、200的事务在执行:

Transaction 100

BEGIN;

UPDATE t SET name = ‘小B’ WHERE id = 1;

UPDATE t SET name = ‘小C’ WHERE id = 1;

Transaction 200

BEGIN;

更新了一些别的表的记录

此刻,表t中id为1的记录得到的版本链表如下所示:

image.png

假设现在有一个使用REPEATABLE READ隔离级别的事务开始执行:

使用REPEATABLE READ隔离级别的事务

BEGIN;

SELECT1:Transaction 100、200未提交

SELECT * FROM t WHERE id = 1; # 得到的列name的值为’小A’

这个SELECT1的执行过程如下:

  • 在执行SELECT语句时会先生成一个ReadView,ReadView的m_ids列表的内容就是[100, 200]。

  • 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是’小C’,该版本的trx_id值为100,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。

  • 下一个版本的列name的内容是’小B’,该版本的trx_id值也为100,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。

  • 下一个版本的列name的内容是’小A’,该版本的trx_id值为80,小于m_ids列表中最小的事务id100,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name为’小A’的记录。

之后,我们把事务id为100的事务提交一下,就像这样:

Transaction 100

BEGIN;

UPDATE t SET name = ‘小B’ WHERE id = 1;

UPDATE t SET name = ‘小C’ WHERE id = 1;

COMMIT;

然后再到事务id为200的事务中更新一下表t中id为1的记录:

Transaction 200

BEGIN;

更新了一些别的表的记录

UPDATE t SET name = ‘小D’ WHERE id = 1;

UPDATE t SET name = ‘小F’ WHERE id = 1;

此刻,表t中id为1的记录的版本链就长这样:

image.png

然后再到刚才使用REPEATABLE READ隔离级别的事务中继续查找这个id为1的记录,如下:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值