mysql整体架构预览

图片:

image-gjsj.png

客户端

连接工具(Navacat、SQLyog、JDBC、命令行)都是MYSQL客户端,主要用户发送sql语句。

服务端

mysql服务端分为Server层和存储引擎层,Server层包括连接器、查询缓存、分析器、优化器、执行器,涵盖大多数MYSQL的核心服务,以及所有的内置函数,所有夸存储引擎的功能都在这里实现,比如存储过程、触发器、视图等。

存储引擎负责数据存储和检索,插件式,支持InnoDB、MyISAM、Memory等多个存储引擎。InnoDB从MYSQL5.5.5版本开始成为默认存储引擎。

Server层

  • 处理sql语句、解析、优化、缓存
  • 负责权限管理、用户认证等
  • 提供了各种SQL函数和存储过程
  • 提供了复制、备份、回复等高级系统功能
  • Server层有自己的日志系统,binlog,记录所有DDL,DML语句,但不记录查询语句。

存储引擎层

  • 负责数据的存储和检索

连接器

和客户端建立连接,获取权限,维持和管理连接。
输入账号密码后连接成功,等待超时后会自动断开连接。

# 查看数据库的连接状态 
show processlist; 
#查看当前的wait_timeout参数值 
SHOW VARIABLES LIKE 'wait_timeout';

建立连接的过程比较复杂,程序里应该减少创建连接的动作,尽量使用长连接,项目中一般都用连接池。

mysql连接过多可能会占用内存异常,导致服务重启,可能是mysql临时内存在长连接里,要偶尔断开重连,5.7之后可以通过执行 mysql_reset_connection 来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。

查询缓存

MYSQL5.7,查询数据会先查缓存,查这个语句有没有执行过(参数也要一样),命中后就返回给客户端。

MYSQL8.0移除了缓存功能,因为要求太严格了,命中率很低,修改也没必要了,缓存在代码里做更好,还有很多第三方redis等缓存。

分析器

分析器判断语句是否合法。

先做词法分析,从SQL语句中提取关键字,比如:查询的表,字段名,查询条件、常量、运算符等,解析成词法单元(token)。

后做语法分析,根据token解析成抽象语法树(AST),根据语法树验证语法合法性和合理性,为后续查询优化、执行计划生成打下基础。

image-hbPJ.png

优化器

拿到语法树后,基于表的统计信息、索引情况、数据分布等因素,评估多种可能的执行路径,并选择成本最低的执行计划,比如多表关联小表驱动大表。

执行器

执行器拿到优化后的语句还要再做权限验证,确保只有被授权的用户才能访问和操作数据。

有些时候,SQL语句要操作的表不只是SQL字面上那些。SQL执行过程中可能会有触发器这种在运行时才能确定的过程,precheck是不能对这种运行时涉及到的表进行权限校验的,所以需要在执行器阶段进行权限检查。

执行流程图

Mysql执行过程与Innodb底层原理 (1).jpg

  1. 词法语法分析,生成语法树,检查权限
  2. 把数据所在的磁盘页加载到buffer pool里(如果buffer pool中有就不用加载),每个磁盘页16k,连续空间,每个磁盘页对应一个缓存页,每个缓存页附带一个控制块(约800字节),存储页的元数据(表空间、页号、链表节点等)。
  3. 修改数据前,要记录undo log,用于回滚事务,和MVCC多版本并发控制(Multi-Version Concurrency Control)。
  4. 写redo log,记录要变更的数据,redo log写到最后一个文件末尾后,会回到第一个文件循环写。
  5. 更新buffer pool内存数据,标记修改的缓存页为脏页。
  6. redo log刷盘,标记事务为prepare,并通知server层。
  7. server层收到通知,写入binlog(server层数据恢复,集群同步),并通知存储引擎层。
  8. 存储引擎层收到binlog写入成功的通知,标记事务为commit。
  9. 系统空闲时,将buffer pool中的数据写入磁盘。

EXPLAIN

官方文档:https://dev.mysql.com/doc/refman/5.7/en/explain-output.html

explain 两个变种

  1. 1)explain extended:会在 explain 的基础上额外提供一些查询优化的信息。紧随其后通过 show warnings 命令可以得到优化后的查询语句,从而看出优化器优化了什么。额外还有 filtered 列,是一个百分比的值,rows * filtered/100 可以估算出将要和 explain 中前一个表进行连接的行数(前一个表指 explain 中的id值比当前表id值小的表)。
    image-snfQ.png
  2. explain partitions:相比 explain 多了个 partitions 字段,如果查询是基于分区表的话,会显示查询将访问的分区。

explain中的列

示例表:

示例表:
DROP TABLE IF EXISTS `actor`; 
CREATE TABLE `actor` (
  `id` int(11) NOT NULL,
  `name` varchar(45) DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `actor` (`id`, `name`, `update_time`) VALUES (1,'a','2017-12-22 15:27:18'), (2,'b','2017-12-22 15:27:18'), (3,'c','2017-12-22 15:27:18');

DROP TABLE IF EXISTS `film`;
CREATE TABLE `film` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `film` (`id`, `name`) VALUES (3,'film0'),(1,'film1'),(2,'film2');

DROP TABLE IF EXISTS `film_actor`;
CREATE TABLE `film_actor` (
  `id` int(11) NOT NULL,
  `film_id` int(11) NOT NULL,
  `actor_id` int(11) NOT NULL,
  `remark` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_film_actor_id` (`film_id`,`actor_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `film_actor` (`id`, `film_id`, `actor_id`) VALUES (1,1,1),(2,1,2),(3,2,1);
  1. id列

id列的编号是 select 的序列号,有几个 select 就有几个id,并且id的顺序是按 select 出现的顺序增长的。

id列越大执行优先级越高,id相同则从上往下执行,id为NULL最后执行。

  1. select_type列
    select_type 表示对应行是简单还是复杂的查询。

    1)simple:简单查询。查询不包含子查询和union
    image-bHNP.png
    2)primary:复杂查询中最外层的 select
    3)subquery:包含在 select 中的子查询(不在 from 子句中)
    4)derived:包含在 from 子句中的子查询。MySQL会将结果存放在一个临时表中,也称为派生表(derived的英文含义)
    用这个例子来了解 primary、subquery 和 derived 类型
    image-DkbE.png
    5)union:在 union 中的第二个和随后的 select
    image-kzpd.png

  2. table列
    这一列表示 explain 的一行正在访问哪个表。
    当 from 子句中有子查询时,table列是 格式,表示当前查询依赖 id=N 的查询,于是先执行 id=N 的查询。

    当有 union 时,UNION RESULT 的 table 列的值为,1和2表示参与 union 的 select 行id。

  3. partitions列
    如果查询是基于分区表的话,partitions 字段会显示查询将访问的分区。

  4. type列
    这一列表示关联类型或访问类型,即MySQL决定如何查找表中的行,查找数据行记录的大概范围。
    依次从最优到最差分别为:system > const > eq_ref > ref > range > index > ALL

    一般来说,得保证查询达到range级别,最好达到ref

    NULL:mysql能够在优化阶段分解查询语句,在执行阶段用不着再访问表或索引。例如:在索引列中选取最小值,可以单独查找索引来完成,不需要在执行时访问表
    image-syxY.png

    const, system:mysql能对查询的某部分进行优化并将其转化成一个常量(可以看show warnings 的结果)。用于 primary key 或 unique key 的所有列与常数比较时,所以表最多有一个匹配行,读表1次,速度比较快。system是const的特例,表里只有一条元组匹配时为system
    image-ZVQd.png
    image-pWIt.png
    eq_ref:primary key 或 unique key 索引的所有部分被连接使用 ,最多只会返回一条符合条件的记录。这可能是在 const 之外最好的联接类型了,简单的 select 查询不会出现这种 type。
    image-Gvvy.png

    ref:相比 eq_ref,不使用唯一索引,而是使用普通索引或者唯一性索引的部分前缀,索引要和某个值相比较,可能会找到多个符合条件的行。

    1)简单 select 查询,name是普通索引(非唯一索引)
    image-aCSw.png
    2)关联表查询,idx_film_actor_id是film_id和actor_id的联合索引,这里使用到了film_actor的左边前缀film_id部分。
    image-tXgd.png
    range:范围扫描通常出现在 in(), between ,> ,<, >= 等操作中。使用一个索引来检索给定范围的行。
    image-FFZc.png
    index:扫描全索引就能拿到结果,一般是扫描某个二级索引,这种扫描不会从索引树根节点开始快速查找,而是直接对二级索引的叶子节点遍历和扫描,速度还是比较慢的,这种查询一般为使用覆盖索引,二级索引一般比较小,所以这种通常比ALL快一些。
    image-RATN.png
    ALL:即全表扫描,扫描你的聚簇索引的所有叶子节点。通常情况下这需要增加索引来进行优化了。

  5. possible_keys列
    这一列显示查询可能使用哪些索引来查找,也叫候选索引
    情况一:possible_keys 有值,key 为 NULL
    表示有候选索引可用,但 MySQL 优化器最终没有选择使用索引。可能的原因包括:表中数据量较少,优化器认为全表扫描比走索引更快(因为索引需要回表,小表全表扫描 I/O 更少);或者索引的区分度不高,优化器判断走索引效率不如全表扫描;又或者查询条件无法有效利用索引,例如对索引列使用了函数、类型转换等操作。
    情况二:possible_keys 为 NULL,key 有值
    这种情况通常发生在查询使用了覆盖索引(Covering Index)时。覆盖索引是指查询所需的所有字段都包含在某个索引中,MySQL 可以直接从索引中获取数据,不需要回表读取数据行,效率更高。另一个场景是索引被用于排序或分组,而非过滤条件。由于 possible_keys 主要记录用于查找或过滤的候选索引,当索引仅用于排序、分组或覆盖扫描时,可能出现 possible_keys 为 NULL 但 key 显示具体索引名的情况。
    情况三:possible_keys 有值,key 有值
    这是正常情况,表示 MySQL 从候选索引中选择了一个最优索引来执行查询。具体选择了哪个索引可以通过 key 列查看。
    情况四:possible_keys 为 NULL,key 为 NULL
    表示没有可用的索引,MySQL 只能使用全表扫描来执行查询。此时应该检查 WHERE 子句、JOIN 条件、ORDER BY 子句或 GROUP BY 子句,考虑是否可以创建合适的索引来提升查询性能。创建索引后,可再次使用 EXPLAIN 验证效果。

  6. key列
    这一列显示mysql实际采用哪个索引来优化对该表的访问。
    如果没有使用索引,则该列是 NULL。如果想强制mysql使用或忽视possible_keys列中的索引,在查询中使用 force index、ignore index。

  7. key_len列
    这一列显示了mysql在索引里使用的字节数,通过这个值可以算出具体使用了索引中的哪些列。
    举例来说,film_actor的联合索引 idx_film_actor_id 由 film_id 和 actor_id 两个int列组成,并且每个int是4字节。通过结果中的key_len=4可推断出查询使用了第一个列:film_id列来执行索引查找。
    image-xGfJ.png
    key_len计算规则如下:

    • 字符串,char(n)和varchar(n),5.0.3以后版本中,n均代表字符数,而不是字节数,如果是utf-8,一个数字或字母占1个字节,一个汉字占3个字节

      • char(n):如果存汉字长度就是 3n 字节
      • varchar(n):如果存汉字则长度是 3n + 2 字节,加的2字节用来存储字符串长度,因为varchar是变长字符串
    • 数值类型

      • tinyint:1字节
      • 2字节
      • int:4字节
      • bigint:8字节
    • 时间类型

      • date:3字节
      • timestamp:4字节
      • datetime:8字节
    • 如果字段允许为 NULL,需要1字节记录是否为 NULL

    索引最大长度是768字节,当字符串过长时,mysql会做一个类似左前缀索引的处理,将前半部分的字符提取出来做索引。

  8. ref列
    这一列显示了在key列记录的索引中,表查找值所用到的列或常量,常见的有:const(常量),字段名(例:film.id)

  9. rows列
    这一列是mysql估计要读取并检测的行数,这个不是结果集里的行数。

  10. filtered 列
    该列是一个百分比的值,rows * filtered/100 可以估算出将要和 explain 中前一个表进行连接的行数(前一个表指 explain 中的id值比当前表id值小的表)。

  11. Extra列
    这一列展示的是额外信息。常见的重要值如下:
    1)Using index:使用覆盖索引
    覆盖索引:是指查询所需的所有字段都包含在索引中,使得数据库引擎可以直接从索引结构中获取数据,而无需访问数据表(即避免“回表”操作)。这种索引设计能显著提升查询性能,因为只需扫描索引页,减少 I/O 操作。
    image-fuxR.png
    回表:‌MySQL中的“回表”是指在使用二级索引(非聚簇索引)查询时,数据库先通过二级索引找到主键值,再根据主键值回到聚簇索引中查找完整数据行的过程。
    2)Using where:使用 where 语句来处理结果,并且查询的列未被索引覆盖
    image-upEC.png
    3)Using index condition:查询的列不完全被索引覆盖,where条件中是一个前导列的范围;
    image-bTOq.png
    4)Using temporary:mysql需要创建一张临时表来处理查询。出现这种情况一般是要进行优化的,首先是想到用索引来优化。
    image-Xmfy.png
    5)Using filesort:将用外部排序而不是索引排序,数据较小时从内存排序,否则需要在磁盘完成排序。这种情况下一般也是要考虑使用索引来优化的。
    image-QuaY.png
    6)Select tables optimized away:不需要优化,使用某些聚合函数(比如 max、min)来访问存在索引的某个字段
    image-BWln.png

索引优化

最左前缀

在联合索引中,要求查询条件必须从索引的最左列开始,并连续匹配,才能有效利用索引提升查询性能 。

如果创建了一个联合索引 (A, B, C),数据库会先按 A 排序,A 相同再按 B 排序,B 相同再按 C 排序。因此,查询时必须“从左开始、不能断开”,否则后面的列无法使用索引 。
索引下推‌(Index Condition Pushdown,简称ICP)是MySQL 5.6版本引入的一项查询优化技术,它的核心作用是‌在索引遍历过程中,直接在存储引擎层对索引中包含的字段进行条件过滤,从而减少回表次数,提升查询效率‌。
但索引下推只适用二级索引,一级索引(主键聚簇索引)树叶子节点上保存全行数据,这时候索引下推不会起到减少全行数据查询的效果

为什么范围查找Mysql没有用索引下推优化?

估计应该是Mysql认为范围查找过滤的结果集过大,like KK% 在绝大多数情况来看,过滤后的结果集比较小,所以这里Mysql选择给 like KK% 用了索引下推优化,当然这也不是绝对的,有时like KK% 也不一定就会走索引下推。

覆盖索引

在InnoDB存储引擎中,索引分为‌**聚簇索引(主键索引)‌和‌二级索引(辅助索引)**‌。二级索引的叶子节点中存储的是索引列的值以及对应的主键值。如果查询所需的字段都包含在该索引中,MySQL就可以直接从索引中读取数据,而无需再通过主键去查找完整数据行,从而避免了“回表”操作。

filesort

单路排序也被称为“全字段排序”,其核心思想是:一次性将查询所需的所有字段数据读取到内存中的排序缓冲区(sort buffer),然后在内存中完成排序操作。这种方式适用于查询字段总长度小于系统变量 max_length_for_sort_data(默认为 1024 字节)的情况。

双路排序又称为“回表排序”,它首先根据排序字段和主键(或唯一索引)读取部分数据到内存中进行排序,之后再根据主键回表获取其他字段的数据。这种方式适用于查询字段总长度大于 max_length_for_sort_data 的情况。

max_length_for_sort_data默认为1024

这个操作主要为了节约内存,所有列都读出来在内存里浪费空间,双路排序就是时间换空间。

字段基数

MySQL字段基数‌(Cardinality)指的是一个字段中‌不同值的数量‌,它是影响索引效率的关键因素之一。基数越高,说明该字段的唯一性越强,索引的筛选能力就越出色。

性别、状态等不适合添加索引,浪费空间,查询大概率也用不到。

分页优化

SELECT * FROM table LIMIT 1000000, 10;
MySQL执行过程:

  1. 读取前 1,000,010 条记录
  2. 抛弃前 1,000,000 条记录
  3. 返回最后 10 条记录

offset关键字性能问题:

  • 需要扫描并丢弃大量数据
  • 偏移量越大,性能越差,时间复杂度 O(offset + size)
  • 内存消耗巨大,可能触发临时表和文件排序

覆盖索引延迟关联
尽量想办法通过覆盖索引查出目标的主键id,再根据id查完整行数据,这个优化虽然快一些,但还是在覆盖索引树上用了offset,还是遍历扫描。

SELECT t1.* FROM orders t1 JOIN (SELECT id FROM orders ORDER BY create_time LIMIT 4500000, 10) t2 ON t1.id = t2.id;

游标分页
比如项目中默认常用软删除字段过滤列表,还要根据修改时间倒排,就可以利用游标分页,直接定位。
先设置索引:

CREATE INDEX idx_status_gmt_modified_id ON your_table (is_deleted, gmt_modified DESC, id DESC);

第一页查询:

SELECT * FROM your_table
  WHERE is_deleted = 0
  ORDER BY gmt_modified DESC, id DESC

后续页:

SELECT * FROM your_table
  WHERE is_deleted = 0
    AND (gmt_modified < :last_gmt_modified
         OR (gmt_modified = :last_gmt_modified AND id < :last_id))
  ORDER BY gmt_modified DESC, id DESC
  LIMIT 20;

这样就达到彻底抛弃offset了,不会有遍历的问题,O(log n)就能找到,遍历的话需要O(n),分页可能遗漏或者重复。

JOIN关联查询优化

MySQL中JOIN的执行算法主要有两种:嵌套循环链接(Nested Loop Join,NLJ)和基于块的嵌套循环链接(Block Nested Loop Join,BNL)

NLJ

当被驱动表关联字段有索引时,用基于索引的嵌套循环链接,驱动表做一次全表扫描读M行,被驱动表每行走索引查找约2-3次树搜索,总共M × log N次,整体复杂度O(M + M × log N),这是最理想的JOIN方式,被驱动表的索引是关键。

如果被驱动表无索引
驱动表扫M行,被驱动表每次全表扫N行,总扫描行数M × N,效率极差,MySQL基本不会真正使用这种方式,而是用BNL替代。

BNL

被驱动表关联字段没有索引时,MySQL实际使用的是BNL算法。

执行过程:

  1. 把驱动表的数据读入join_buffer(一块内存缓冲区)
  2. 扫描被驱动表,把被驱动表的每一行和join_buffer中的所有行做匹配
  3. 匹配成功的作为结果返回

驱动表读一次,被驱动表也只扫一次,总扫描行数M + N,相比SNLJ的M × N大幅减少。但判断次数仍然是M × N,只是从磁盘I/O变成了内存比较,速度快很多。

查看 join_buffer 大小(默认256KB)

SHOW VARIABLES LIKE 'join_buffer_size';

被驱动表没有索引时,EXPLAIN中Extra列会显示Using join buffer (Block Nested Loop)

join_buffer不够用怎么办?

如果驱动表数据量大,join_buffer放不下,MySQL会分段处理:先把驱动表的前N行放入join_buffer,扫描被驱动表做匹配;清空join_buffer,再放入下N行,再次扫描被驱动表;重复直到驱动表所有行处理完。每多一段被驱动
表就要多扫一次,所以join_buffer越大,段数越少,被驱动表扫描次数越少。

INLJ和BNL的核心区别

INLJ被驱动表走索引,判断次数M × log N;BNL被驱动表没有索引,判断次数M × N,虽然被驱动表只扫一次(分段情况下多次),但每次扫描都是在内存中做全量比较。所以被驱动表的关联字段加索引是JOIN优化最关键的一步。

BNL还有一个隐藏问题:被驱动表的行被多次访问不会缓存在内存中,如果被驱动表很大,数据不在Buffer Pool中,每次扫描都会产生大量磁盘I/O,这种情况下性能会非常差。

实际inner join时,要用小表驱动大表

in和exists优化

原则:小表驱动大表
in:当B表的数据集小于A表的数据集时,in优于exists
select \* from A where id in (select id from B)
exists:当A表的数据集小于B表的数据集时,exists优于in
select \* from A where exists (select 1 from B where B.id = A.id)

count(*)的优化

-- 临时关闭mysql查询缓存,为了查看sql多次执行的真实时间
mysql> set global query_cache_size=0;
mysql> set global query_cache_type=0;

mysql> EXPLAIN select count(1) from employees;
mysql> EXPLAIN select count(id) from employees;
mysql> EXPLAIN select count(name) from employees;
mysql> EXPLAIN select count(*) from employees;

四个sql的执行计划一样,说明这四个sql执行效率应该差不多

字段有索引:count(*)≈count(1)>count(字段)>count(主键 id) //字段有索引,count(字段)统计走二级索引,二级索引存储数据比主键索引少,所以count(字段)>count(主键 id)

字段无索引:count(*)≈count(1)>count(主键 id)>count(字段) //字段没有索引count(字段)统计走不了索引,count(主键 id)还可以走主键索引,所以count(主键 id)>count(字段)

count(1)跟count(字段)执行过程类似,不过count(1)不需要取出字段统计,就用常量1做统计,count(字段)还需要取出字段,所以理论上count(1)比count(字段)会快一点。

count() 是例外,mysql并不会把全部字段取出来,而是专门做了优化,不取值,按行累加,效率很高,所以不需要用count(列名)或count(常量)来替代 count()。

为什么对于count(id),mysql最终选择辅助索引而不是主键聚集索引?因为二级索引相对主键索引存储数据更少,检索性能应该更高,mysql内部做了点优化(应该是在5.7版本才优化)。

事务

ACID特性

Atomicity(原子性):事务是不可分割的工作单位,要么全部完成,要么全部失败Consistency(一致性):事务必须使数据库从一个一致性状态到另外一个一致性状态 Isolation(隔离性):多个事务并发执行时,一个事务的执行不会影响其他事务的执行
Durability(持久性):一个事务一旦提交,他对数据库的改变就是持久性的

事务控制语句

START TRANSACTION;  -- 开始事务
BEGIN;              -- 同上,START TRANSACTION 的别名
COMMIT;             -- 提交事务
ROLLBACK;           -- 回滚事务
SAVEPOINT savepoint_name;           -- 创建保存点
ROLLBACK TO SAVEPOINT savepoint_name; -- 回滚到保存点
RELEASE SAVEPOINT savepoint_name;   -- 删除保存点

事务隔离级别

四种隔离级别

读未提交(READ UNCOMMITTED):一个事务可以读到另一个事务未提交的数据
读已提交(READ COMMITTED):一个事务可以读到另一个事务已经提交的数据
可重复读(REPEATABLE READ):同一个事务内多次读取结果一致
串行化(SERIALIZABLE):最高隔离级别,完全串行执行

并发问题详解

更新丢失
当多个事务更新同一条数据时,最后的更新会覆盖其他事务的更新
脏读
一个事务读取到了其他事务未提交的数据
不可重复读
一行数据多次读取数据不一样
幻读
按照同一条件查询的数据,多次查询会发现有新增/删除的行,返回行数有变化

MVCC(多版本并发控制)

1.Undolog存储历史版本

  • 每次修改数据时,将旧版本写入 undo log
  • 通过 DB_ROLL_PTR 形成版本链:最新数据 → 旧版本 → 更旧版本 → ...
  • 支持事务回滚和 MVCC 读操作

2.隐式字段,记录版本信息

问题:如何知道每行数据是被哪个事务修改的?
解决方案:InnoDB为每行数据自动添加隐藏列
DB_TRX_ID:最后一次插入或更新该行的事务 ID(6字节)
DB_ROLL_PTR:回滚指针,指向 undo log 中该行的上一个版本(7字节)
DB_ROW_ID:行 ID,用于索引(如果表没有主键则自动创建,6字节)

3.Read View:决定可见性
在InnoDB中,读已提交和可重复读中查询语句会触发新建Read View(快照读)

字段 说明
m_ids 生成 Read View 时活跃的事务 ID 列表(未提交)
min_trx_id m_ids 中最小的事务 ID
max_trx_id 系统应分配给下一个事务的 ID(当前最大值+1)
creator_trx_id 创建该 Read View 的事务 ID

可见性判断规则
对于表中某行数据的DB_TRX_ID,判断当前事务是否可见

条件 判断 结果
DB_TRX_ID == creator_trx_id 是自己修改的 ✅ 可见
DB_TRX_ID < min_trx_id 事务在 Read View 创建前已提交 ✅ 可见
DB_TRX_ID >= max_trx_id 事务在 Read View 创建后才开启 ❌ 不可见
DB_TRX_ID 在 m_ids 中 事务未提交 ❌ 不可见
DB_TRX_ID 不在 m_ids 中 事务已提交 ✅ 可见

如果不可见,则通过 DB_ROLL_PTR 找到 undo log 中的旧版本,递归判断,直到找到可见版本。

顺序说明:

1.行数据的事务id等于创建该read view的事务id,先确定不是自己事务内的,保证正确性,否则会被后续的规则搞成不可见,可见。
2.行数据的事务id小于最小活跃事务id,放在前面过滤早就已经提交了的行,这种行命中率高,O(1),所以放在第二位,可见。
3.行数据的事务id大于等于最大的活跃事务id,说明在Read View创建后开启的事务,这种命中率不高,但效率也比遍历m_ids效率高,O(1),不可见
4.行数据的事务id在m_ids(n个)列表中,说明事务未提交,效率较低O(n),不可见。
5.行数据id不在m_ids列表中,说明事务是在最小事务id后创建的,但是最小的事务id运行时间比较长,没有提交,但是该事务很快提交了,提交后,creator_trx_id的所有者才创建了read view视图,所以最大事务id和最小事务id的范围内,无该活跃事务,零成本,兜底,可见。

RC(读已提交)与RR(可重复读)的区别
前面写RC和RR中查询语句会触发新建Read View,但是RR是一个事务内多次查询用同一个Read View,这就保证了重复读同一个快照,实现了可重复读。

总结:undo log维护一行数据的历史版本,innodb通过readview决定该行数据是否可见,不可见就遍历undo log找到可见的。

MVCC解决了读写冲突,读操作不用加锁,但是不能解决写写冲突,也不能解决幻读问题,但是隔离级别不能设置成串行化(性能太差),所以写写操作需要锁。

锁分类

  • 从性能上分为乐观锁和悲观锁。

  • 从对数据库操作的粒度分为行锁和表锁。

  • 从对数据库操作的类型分为读锁和写锁,还有意向锁。
    读锁:读锁就是共享锁(Shared,S锁),select * from T where id=1 lock in share mode,允许多线程同时读。
    写锁:写锁就是排它锁(eXclusive,X锁),当写操作没有完成前,它会阻断其它读锁和写锁的获取,查询时也可以通过 for update加写锁,select * from T where id=1 for update
    意向锁:加表锁的时候,要扫描行看有没有行锁,效率低下。分为意向共享锁(IS)和意向排它锁(IX)。
    事务在获取行锁之前,先在表级别获取意向锁:

    • 要获取行级 S 锁 → 先获取表级 IS 锁
    • 要获取行级 X 锁 → 先获取表级 IX 锁
    锁类型 说明
    IS(意向共享锁) 事务打算给某些行加 S 锁
    IX(意向排他锁) 事务打算给某些行加 X 锁

    兼容性矩阵

    IS IX S(表锁) X(表锁)
    IS
    IX
    S
    X