这个是网友++C++在群里问的一个关于MySQL的问题,本篇文章实验测试环境为MySQL 5.6.20,事务隔离级别为REPEATABLE-READ ,在演示问题前,我们先准备测试环境。准备一个测试表test以及一个存储过程循环往test表里面插入记录。

CREATE TABLE test

(

  `id` int(11) primary key not null,

  `name` char(255) 

) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

 

 

delimiter &&

drop procedure if exists prc_insert;

 

create procedure prc_insert(in  cnt int)

begin

declare i int;

set i=1;

while i < cnt do

    insert into test(id, name) select i,  CONCAT('name',i) from dual;

    

    set i = i+1;

 

end while;

end &&

 

delimiter ;

在线程ID为14的会话中,开启事务,然后执行查询test的SQL语句

mysql> select connection_id() from dual;

+-----------------+

| connection_id() |

+-----------------+

|              14 |

+-----------------+

1 row in set (0.00 sec)

 

mysql> start transaction;

Query OK, 0 rows affected (0.00 sec)

 

mysql> select * from test;

然后在线程ID为12的会话中,循环往表test里面插入1000000记录

mysql> select connection_id() from dual;

+-----------------+

| connection_id() |

+-----------------+

|              12 |

+-----------------+

1 row in set (0.00 sec)

 

mysql> call prc_insert(1000000);

Query OK, 1 row affected (8 min 32.11 sec)

在执行循环插入的这段时间里(SQL执行需要几分钟时间),我们在线程ID 为14的会话中反复执行select * from test这个SQL语句,你会发现该SQL的执行时间变长。那么引起SQL语句执行时间变长的原因是什么呢? 如何解释得通呢?

刚开始讨论的时候,以为MySQL会像ORACLE那样会在UNDO的回滚段中产生大量UNDO记录,最后导致SQL语句会像ORACLE那样产生额外的一致性读,产生额外的IO,从而导致执行时间变长。 后面测试发现,其实对于MySQL而言,INSERT操作在事务提交前只对当前事务可见,因此产生的Undo日志可以在事务提交后直接删除,而此处使用是自动提交模式。用“MySQL技术内幕:InnoDB存储引擎”里面提供的脚本py_innodb_page_info.py测试验证。也是确实如此(UNDO日志的大小变化很小,时而增长,时而变小)。其实MySQL里面多版本并发控制(MVCC)的实现机制跟Oracle还是不同的。不能生搬硬套Oracle下的那套理论。

[root@DB-Server kerry]# python py_innodb_page_info.py /data/mysql/ibdata1

Total number of page: 4864:

Insert Buffer Free List: 32

Insert Buffer Bitmap: 1

System Page: 130

Transaction system Page: 1

Freshly Allocated Page: 1326

Undo Log Page: 3224

File Segment inode: 6

B-tree Node: 142

File Space Header: 2

[root@DB-Server kerry]# python py_innodb_page_info.py /data/mysql/ibdata1

Total number of page: 4864:

Insert Buffer Free List: 32

Insert Buffer Bitmap: 1

System Page: 130

Transaction system Page: 1

Freshly Allocated Page: 1326

Undo Log Page: 3223

File Segment inode: 6

B-tree Node: 143

File Space Header: 2

[root@DB-Server kerry]# python py_innodb_page_info.py /data/mysql/ibdata1

Total number of page: 4864:

Insert Buffer Free List: 32

Insert Buffer Bitmap: 1

System Page: 130

Transaction system Page: 1

Freshly Allocated Page: 1326

Undo Log Page: 3213

File Segment inode: 5

B-tree Node: 154

File Space Header: 2

[root@DB-Server kerry]# python py_innodb_page_info.py /data/mysql/ibdata1

Total number of page: 4864:

Insert Buffer Free List: 32

Insert Buffer Bitmap: 1

System Page: 130

Transaction system Page: 1

Freshly Allocated Page: 1326

Undo Log Page: 3205

File Segment inode: 5

B-tree Node: 162

File Space Header: 2

[root@DB-Server kerry]# python py_innodb_page_info.py /data/mysql/ibdata1

Total number of page: 4864:

Insert Buffer Free List: 32

Insert Buffer Bitmap: 1

System Page: 130

Transaction system Page: 2

Freshly Allocated Page: 1326

Undo Log Page: 3240

File Segment inode: 5

B-tree Node: 127

File Space Header: 1

其实InnoDB的多版本并发控制(MVCC),“高性能MySQL”这本书中有这么一段描述:

InnoDB的MVCC,是通过每行记录后面保存的两个隐藏的列来实现的。 这两个列一个保存了行的创建时间,一个保存行的过期时间(或删除时间),当然存储的并不是实际的时间值,而是系统版本号(System version number),每开始一个新的事务,系统版本号都会自动递增,事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。下面看一下在REPEATABLE READ隔离级别下, MVCC是如何具体操作的。

SELECT

InnoDB会根据以下两个条件检查每行的记录:

a.  InnoDB只查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或修改过的。

b.  行的删除要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。

只有符合上述两个条件的记录,才能返回作为查询结果。

..............................................

其实至少从MySQL 5.5之后, 每一行开始额外包含三个隐藏字段,而不是二个字段(没有查证高性能MySQL主要讲述哪个MySQL版本)。

·         6字节的事务ID(DB_TRX_ID)字段: 用来标识最近一次对本行记录做修改(insert|update)的事务的标识符, 即最后一次修改(insert|update)本行记录的事务ID。

至于delete操作,在InnoDB看来也不过是一次update操作,更新行中的一个特殊位将行表示为deleted, 并非真正删除。

·         7字节的回滚指针(DB_ROLL_PTR)字段: 指写入回滚段(rollback segment)的 undo log record (撤销日志记录记录)。

如果一行记录被更新, 则 undo log record 包含 '重建该行记录被更新之前内容' 所必须的信息。

·         6字节的DB_ROW_ID字段: 包含一个随着新行插入而单调递增的行ID, 当由innodb自动产生聚集索引时,聚集索引会包括这个行ID的值,否则这个行ID不会出现在任何索引中。

InnoDB表数据的组织方式为主键聚簇索引。由于采用索引组织表结构,记录的ROWID是可变的(索引页分裂的时候,Structure Modification Operation,SMO),因此二级索引中采用的是(索引键值, 主键键值)的组合来唯一确定一条记录。

无论是聚簇索引,还是二级索引,其每条记录都包含了一个DELETED BIT位,用于标识该记录是否是删除记录。除此之外,聚簇索引记录还有两个系统列:DATA_TRX_ID,DATA_ROLL_PTR。DATA _TRX_ID表示产生当前记录项的事务ID;DATA _ROLL_PTR指向当前记录项的undo信息

在InnoDB中(默认Repeatable Read级别), 事务在begin/start transaction之后的第一条select读操作后, 会创建一个Read View, 将当前系统中活跃的其他事务记录记录起来; 关于Read View这个概念,参考何登成的博客“InnoDB多版本(MVCC)实现简要分析”,下面摘抄部分内容如下:

InnoDB默认的隔离级别为Repeatable Read (RR),可重复读。InnoDB在开始一个RR读之前,会创建一个Read View。Read View用于判断一条记录的可见性。Read View定义在read0read.h文件中,其中最主要的与可见性相关的属性如下:

dulint    low_limit_id;    /* 事务号 >= low_limit_id的记录,对于当前Read View都是不可见的 */

dulint    up_limit_id;     /* 事务号 < up_limit_id ,对于当前Read View都是可见的 */

ulint     n_trx_ids;       /* Number of cells in the trx_ids array */

dulint*    trx_ids;        /* Additional trx ids which the read should

                    not see: typically, these are the active

                    transactions at the time when the read is

                    serialized, except the reading transaction

                    itself; the trx ids in this array are in a

                    descending order */

dulint    creator_trx_id;    /* trx id of creating transaction, or

                    (0, 0) used in purge */

简单来说,Read View记录读开始时,所有的活动事务,这些事务所做的修改对于Read View是不可见的。除此之外,所有其他的小于创建Read View的事务号的所有记录均可见。可见包括两层含义:

记录可见,且Deleted bit = 0;当前记录是可见的有效记录。

记录可见,且Deleted bit = 1;当前记录是可见的删除记录。此记录在本事务开始之前,已经删除

故此,个人猜测当循环插入的记录越来越多时,故SELECT查询时,由于InnoDB引擎会由于Read View的可见性比较,从而扫描聚簇索引,然后对比聚簇索引中DATA_TRX_ID与当前事务ID,当记录越来越多时,这个对比算法的开销也会越来越大,从而导致SELECT查询时间变长。以上部分是个人结合相关参考资料而作的一个推测和解释,如有错误或不对的地方,敬请指正。关于Read View的可见性对比算法,参考MySQL-InnoDB-MVCC多版本并发控制 这篇博文,摘抄如下:

可见性比较算法(这里每个比较算法后面的描述是建立在rr级别下,rc级别也是使用该比较算法,此处未做描述)

设要读取的行的最后提交事务id(即当前数据行的稳定事务id)为 trx_id_current

当前新开事务id为 new_id

当前新开事务创建的快照read view 中最早的事务id为up_limit_id, 最迟的事务id为low_limit_id(注意这个low_limit_id=未开启的事务id=当前最大事务id+1)

比较:

·         1.trx_id_current < up_limit_id, 这种情况比较好理解, 表示, 新事务在读取该行记录时, 该行记录的稳定事务ID是小于, 系统当前所有活跃的事务, 所以当前行稳定数据对新事务可见, 跳到步骤5.

·         2.trx_id_current >= trx_id_last, 这种情况也比较好理解, 表示, 该行记录的稳定事务id是在本次新事务创建之后才开启的, 但是却在本次新事务执行第二个select前就commit了,所以该行记录的当前值不可见, 跳到步骤4。

·         3.trx_id_current <= trx_id_current <= trx_id_last, 表示: 该行记录所在事务在本次新事务创建的时候处于活动状态,从up_limit_id到low_limit_id进行遍历,如果trx_id_current等于他们之中的某个事务id的话,那么不可见, 调到步骤4,否则表示可见。

·         4.从该行记录的 DB_ROLL_PTR 指针所指向的回滚段中取出最新的undo-log的版本号, 将它赋值该 trx_id_current,然后跳到步骤1重新开始判断。

·         5.将该可见行的值返回。

参考资料

https://segmentfault.com/a/1190000012650596

https://blog.csdn.net/shaochenshuo/article/details/76137652

MySQL InnoDB下关于MVCC的一个问题的分析的更多相关文章

  1. Mysql InnoDB下的两种行锁

    今天例举2种常见的Mysql InnoDB下的行锁 现有表dr_test(id pk, name) 数据是 1 zhangsan2 lisi3 wangwu 例子1 事务1 update dr_tes ...

  2. MySQL InnoDB索引介绍以及在线添加索引实例分析

    引言:MySQL之所以能成为经典,不是没有道理的,B+树足矣! 一.索引概念 InnoDB引擎支持三种常见的索引:B+树索引,全文索引和(自适应)哈希索引.B+树索引是传统意义上的索引,构造类似二叉树 ...

  3. 从一个死锁看mysql innodb的锁机制

    背景及现象 线上生产环境在某些时候经常性的出现数据库操作死锁,导致业务人员无法进行操作.经过DBA的分析,是某一张表的insert操 作和delete操作发生了死锁.简单介绍下数据库的情况(因为涉及到 ...

  4. MySQL InnoDB存储引擎中的锁机制

    1.隔离级别 Read Uncommited(RU):这种隔离级别下,事务间完全不隔离,会产生脏读,可以读取未提交的记录,实际情况下不会使用. Read Committed (RC):仅能读取到已提交 ...

  5. Mysql Innodb cluster集群搭建

    之前搭建过一个Mysql Ndb cluster集群,但是mysql版本是5.7的,看到官网上mysql8的还是开发者版本,所以尝试搭建下mysql Innodb cluster集群. MySQL的高 ...

  6. 什么是mysql innodb cluster?

    目录 一.简介 二.特性 一.简介 MySQL InnoDB集群提供了一个集成的,本地的,HA解决方案.MySQL InnoDB集群由以下几部分组成: MySQL Servers with Group ...

  7. MySQL InnoDB MVCC深度分析

    关于MySQL的InnoDB的MVCC原理,很多朋友都能说个大概: 每行记录都含有两个隐藏列,分别是记录的创建时间与删除时间 每次开启事务都会产生一个全局自增ID 在RR隔离级别下 INSERT -& ...

  8. 纯干货,Mysql innodb的ACID特性是怎么实现的?以及高并发情况下会出现的问题

    首先说说什么是ACID: 它们分别是Atomicity(原子性),Consistency(一致性),Isolation(隔离性),Transaction(持久性) 原子性: 意为单个事务里的多个操作要 ...

  9. MySQL InnoDB MVCC

    MySQL 原理篇 MySQL 索引机制 MySQL 体系结构及存储引擎 MySQL 语句执行过程详解 MySQL 执行计划详解 MySQL InnoDB 缓冲池 MySQL InnoDB 事务 My ...

随机推荐

  1. [Swift]LeetCode825. 适龄的朋友 | Friends Of Appropriate Ages

    Some people will make friend requests. The list of their ages is given and ages[i] is the age of the ...

  2. php设计模式2

    代理模式 <?php /** * 代理模式:为其他对象提供一个代理以控制这个对象的访问 它是给某一个对象提供一个替代者,使之在client对象和subject对象之间编码更有效率. 代理可以提供 ...

  3. 【Spark篇】---Spark中Transformations转换算子

    一.前述 Spark中默认有两大类算子,Transformation(转换算子),懒执行.action算子,立即执行,有一个action算子 ,就有一个job. 通俗些来说由RDD变成RDD就是Tra ...

  4. Spring Boot 2.0 教程 | 配置 Undertow 容器

    欢迎关注个人微信公众号: 小哈学Java, 文末分享阿里 P8 资深架构师吐血总结的 <Java 核心知识整理&面试.pdf>资源链接!! 文章首发于个人网站 https://ww ...

  5. Java的内部类真的那么难以理解?

    01 前言 昨天晚上,我把车停好以后就回家了.回家后才发现手机落在车里面了,但外面太冷,冷到骨头都能感受到寒意——实在是不想返回一趟去取了(小区的安保还不错,不用担心被砸车玻璃),于是打定主意过几个小 ...

  6. Java导出CSV文件

    以前导出总是用POI导出为Excel文件,后来当我了解到CSV以后,我发现速度飞快. 如果导出的数据不要求格式.样式.公式等等,建议最好导成CSV文件,因为真的很快. 虽然我们可以用Java再带的文件 ...

  7. Android 修改字体,跳不过的 Typeface

    序 在 Android 下使用自定义字体已经是一个比较常见的需求了,最近也做了个比较深入的研究. 那么按照惯例我又要出个一篇有关 Android 修改字体相关的文章,但是写下来发现内容还挺多的,所以我 ...

  8. Android--MP3播放器MediaPlayer

    前言 Android提供了常见的音频.视频的编码.解码机制.借助于多媒体类MediaPlayer的支持,开发人员可以很方便在在应用中播放音频.视频.本篇博客主要讲解在Android平台下如何播放一个音 ...

  9. 【ASP.NET Core快速入门】(九) RoutingMiddleware介绍以及MVC引入

    前言 前面我们介绍了使用app.Map来配置路由,但是对于一般不是特别大的项目来说,我们不使用Map来进行路由配置. 配置路由 我们首先需要在Startup.cs文件中的ConfigureServic ...

  10. 使用 curl 进行 ssl 认证

    目录 SSL 认证 认证实现 问题解决 curl不支持 https SSL certificate problem, verify that the CA cert is OK curl: (60) ...