1. MySQL架构
1.1 逻辑架构图
1.1.1 连接层:
A 连接器(Connectors)
①负责与客户端的通信,是半双工模式,这就意味着某一固定时刻只能由客户端向服务器请求或者服务器向客户端发送数据,而不能同时进行。
②验证请求用户的账户和密码是否正确,如果账户和密码错误,会报错:Access denied for user 'root'@'localhost' (using password: YES)。
③如果用户的账户和密码验证通过,会在mysql自带的权限表中查询当前用户的权限,mysql中存在4个控制权限的表,分别为user表,db表,tables_priv表,columns_priv表。
B 系统管理和控制工具(Management Services & Utilities)
C 连接池(Connector Pool)
1.1.2 SQL Layer(MySQL的业务层)
D SQL接口(SQL Interface):接收SQL、DML、DDL
E 解析器(Parser)
①词法分析:对接收到的SQL进行词法分析,根据空格进行分词生成语法树;比如 select * from t1 会生成如下的一棵语法树。
②语法分析:分析语法树是否符合SQL的语法(SQL92),其中比如limit语法属于MySQL自己的语法,因此limit要最后解析。 比如 aa * from t1 在解析的时候会报语法错误。如果分析到语法错误,会直接给客户端抛出异常:ERROR:You have an error in your SQL syntax。
F 查询优化器(Optimizer)
正确的语法树(语法正确)会进入到优化器模块,进行查询SQL语句的优化,包括索引的使用(使用最优索引)、多表关联的优化(未手动指定左右连接时,自动优化,小表驱动大表,即小表在前)、where从左到右查找过滤程度最大的条件先执行(非绝对,即优化器自动把此条件调整到最前边。Oracle是从右到左查找)等。 另附SQL的执行顺序:
G 查询缓存(Caches & Buffers)
将查询结果进行缓存,将查询SQL语句字符串hash后的值作为key,查询结果作为value。下次同样的SQL进行查询时,直接从查询缓存中取结果。当数据库中的数据发生改变时(增删改),缓存清除。因性能问题,MySQL 8后不再使用此查询缓存功能。
1.1.3 存储引擎层(对接数据)
H 存储引擎(Plugable Storage Engines)
MySQL采用可插拔的存储引擎架构,MySQL5.6开始默认采用InnoDB存储引擎。MySQL以表为单位指定存储引擎,如下:
create table xxx() engine=InnoDB/MyISAM/Memory;
查看支持的存储引擎:
show engines;
各个存储引擎的区别
1.2 简版执行流程图
1.3 详细执行流程图
1.4 MySQL物理结构
MySQL从物理结构上来说,分为日志文件和数据文件。日志文件采用顺序IO(记录速度快,只能追加,但浪费空间,适合日志的存储)方式存储,数据文件采用随机IO(记录速度相对顺序IO慢,但节省空间,适合数据和索引的存储)方式存储。
1.4.1 日志文件
MySQL通过日志文件记录了数据库的操作信息和错误信息。常用的日志文件包括错误日志、二进制日志、通用查询日志、慢查询日志、InnoDB引擎在线Redo日志和中继日志等。相关配置如下图:
①错误日志(err log),默认是开启的,且从版本5.5.7以后无法关闭错误日志,记录了运行过程中遇到的所有的严重的错误信息,以及数据库每次启动和关闭时的详细信息。错误日志的相关信息可以通过log-error和log-warnings来配置,其中log-error是定义是否启用错误日志的功能和错误日志的存储位置(log-error=/usr/local/mysql/log/error.log),log-warnings是定义是否将警告信息也定义至错误日志中。log-error可以直接定义为日志路径也可以是ON/OFF;log-warnings只能使用0/1/2来定义,
- og_warnings的值为0,表示不记录警告信息。
- log_warnings的值为1,表示警告信息一并记录到错误日志中。
-
log_warnings的值大于1,表示"失败的连接"的信息和创建新连接时"拒绝访问"类的错误信息也会被记录到错误日志中。
mysql5.5中log_warnings参数的默认值为1
mysql5.7中log_warnings参数的默认值为2
mysql> show variables like "%log_warnings%";
②二进制日志(bin log),默认是关闭的,需要通过配置log-bin=mysql-bin进行开启(等号后面的mysql-bin只是自定义的bin log日志文件的前缀名称,比如mysql-bin.000001.log,不指明前缀默认采用主机名称作为前缀);bin log记录了数据库所有的DDL和DML语句,但不包括select语句内容,语句以事件的形式进行保存,描述了数据库的变更顺序,bin log还包括了每个更新语句的执行时间信息,bin log主要作用在于恢复数据,因此bin log对于灾难恢复和备份恢复来说至关重要;如果是DDL语句,则直接记录到bin log日志中,而DML语句必须通过事务提交才能记录到bin log中;bin log还用于实现主从复制以及数据恢复。
③通用查询日志(general query log),默认是关闭的。由于通用查询日志会记录用户所有的操作,即包括所有的增删改查日志,在并发量大的情况下会产生大量的日志数据从而导致不必要的磁盘IO,会影响mysql的性能,如果不是为了调试不建议开启通用查询日志。
④慢查询日志(slow query log),默认是关闭的。需要通过配置 slow-query-log=ON 开启。记录执行时间超过long_query_time秒的所有查询,便于收集执行时间比较长的查询语句。
⑤事务日志,InnoDB特有的日志,包括重做日志(Redo log)和回滚日志(Undo log),Redo log的日志文件名为"ib_logfile0"和"ib_logfile1",默认存放在表空间所在的目录。Undo log日志文件默认存储在.ibdata文件里,此目录是全局表空间目录。
⑥中继日志(relay log),是在主从复制环境中产生的日志。主要作用就是为了从机能够从中继日志中获取到从主机同步过来的SQL语句,然后执行到从机中。
⑦查看日志开启情况:
show variables like 'log_%';
1.4.2 数据文件
①InnoDB数据文件,.frm文件主要存放与表相关的数据信息,主要包括表结构的定义信息;.ibd文件使用独享表空间存储表数据和索引信息,一张表对应一个ibd文件;.ibdata文件使用共享表空间存储表数据和索引信息,所有表使用一个或多个ibdata文件。
②MyISAM数据文件:.frm文件主要是存放与表相关的数据信息,主要包括表结构的定义信息;.myd文件主要用来存储表数据;.myi文件主要用来存储表数据文件中任何索引的数据树。
③查看数据文件存放目录:
show variables like '%datadir%';
2. 索引介绍及原理分析
2.1 索引介绍
官方介绍索引是帮助MySQL高效获取数据的数据结构。更通俗的说,数据库索引好比一本字典的目录,可以加快数据库检索数据的速度。
create index idx_name on user(name(10)); ---因name字段是字符串,所以要指定建立索引的字段 长度
create unique index idx_name on user(name(10) ); ---唯一索引
2.1.1 索引的优势
①检索方面,可以提高数据检索的效率,降低数据库的IO成本,类似于树的目录;
②排序方面,通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗。被索引的列会自动进行排序,包括单列索引和组合索引,只是组合索引的排序要复杂一些。如果按照索引列的顺序进行排序,对order by来说,效率会提高很多。where索引列在存储引擎层处理。
2.1.2 索引的劣势
①索引会占用磁盘空间。
②索引虽然会提高查询效率,但是会降低更新数据的效率。比如每次对表进行增删改操作时,MySQL不仅要更新数据文件还要更新相关的索引文件。
2.2 索引分类
①单列索引
主键索引,索引列中的值是唯一的且不能为空值;唯一索引,索引列中的值是唯一的,但可为空值。
②组合索引
在表中的多个字段上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使用,使用组合索引时遵循"最左前缀原则"。
③全文索引
④空间索引
不做了解
⑤位图索引
不做了解
2.3 索引的原理分析
2.3.1 索引的存储结构
①索引是在存储引擎中实现的,也就是说不同的存储引擎会使用不同的索引。
②MyISAM和InnoDB存储引擎:只支持B+ TREE索引,也就是默认使用BTREE,不能够更换。
③MEMORY/HEAP存储引擎:支持HASH和BTREE索引。
2.3.1.1 B树和B+树
外站链接: 数据结构示例网站
①B树是为了磁盘或其他存储设备而设计的一种多叉平衡查找树。
②B树的高度一般都是在2-4这个高度,树的高度直接影响IO读写的次数。
③如果是三层树结构,支撑的数据可以到20G;如果是四层树结构,支持的数据可以到几十T。
④B树和B+树的区别,最大的区别是非叶子节点是否存储数据的问题。B树的叶子节点和非叶子节点都会存储数据行;B+树只有叶子节点才会存储数据行,且存储的数据都是在一行上,而且这些数据都是有指针指向的,也就是有序的。MyISAM和InnoDB存储引擎所使用的索引区别如下:
2.3.2 单列索引
2.3.2.1 非聚集索引(MyISAM)
①主键索引:B+ TREE的每个叶子节点存放的数据除了ID外,还有指向真实数据的指针(指向磁盘地址)。
MyISAM主键索引
②辅助索引(非主键索引):跟主键索引相同,叶子节点存放的也是真实数据的指针(指向磁盘地址)。区别在于辅助索引的节点key可以重复,比如年龄。
辅助索引(MyISAM)
2.3.2.2 聚集索引(InnoDB)
①主键索引:因InnoDB引擎的数据文件和索引文件是存放在一个.ibd文件中的,因此B+ TREE的每个叶子节点存放的数据除了ID外,还有当前ID对应的真实数据。
主键索引(InnoDB)
②辅助索引(非主键索引):每个叶子节点存放的是索引字段值和ID,在通过非主键索引得到结果后,通过所存储的ID去主键索引中找到对应数据。同样,辅助索引的key可以重复,比如姓名。
辅助索引(InnoDB)
③结论:由以上可以得出,InnoDB使用辅助索引进行检索时,要搜索两次索引树(辅助索引树和主键索引树),最终取出结果,此方式称作回表,即先定位主键值,再定位行记录,它的性能较扫一遍索引树更低。另外官网有表示:只需要在一棵索引树上就能获取SQL所需的所有列数据,则无需回表,速度更快。
2.3.2.3 覆盖索引
为了避免回表操作,可以实现覆盖索引来达到目的。常见的方法是:将被查询的字段,建立到联合索引里去。
create table user (
id int primary key,
name varchar(20),
sex varchar(5),
index(name)
)engine=innodb;
①第一个SQL:
select id, name from user where name='shenjian';
能够命中name索引,索引叶子节点存储了主键id,通过name的索引树即可获取id和name,无需回表,符合索引覆盖,效率较高。
②第二个SQL:
select id, name, sex from user where name='shenjian';
能够命中name索引,索引叶子节点存储了主键id,但sex字段必须回表查询才能获取到,不符合索引覆盖,需要再次通过id值扫码聚集索引获取sex字段,效率会降低。
如果把(name)单列索引升级为联合索引(name, sex)就不同了。
create table user (
id int primary key,
name varchar(20),
sex varchar(5),
index(name, sex)
)engine=innodb;
③哪些场景可以利用索引覆盖来优化SQL?
场景1:全表count查询优化;
原表为:user(PK id, name, sex);
直接:select count(name) from user; 不能利用索引覆盖。
添加索引:alter table user add key(name); 就能够利用索引覆盖提高效率。
场景2:列查询回表优化
select id, name, sex where name='shenjian';
这个例子不再赘述,将单列索引(name)升级为联合索引(name, sex),即可避免回表。
场景3:分页查询
select id, name, sex order by name limit 500, 100;
将单列索引(name)升级为联合索引(name, sex),也可以避免回表。
2.3.3 组合索引
由多个字段组成的索引,使用顺序就是创建的顺序。也属于次要索引。
alter table `table_name` add index index_name(col1, col2, col3);
组合索引
可以看到,组合索引的索引树上每个节点包含多个字段。
- 优势:节省空间,容易形成覆盖索引。
- 使用:遵循最左前缀原则,①前缀索引,select * from user where name like '常量%'; 此处的 like '常量%' 会使用到name字段上的索引,如果是 like '%常量' 则不会使用到name字段上的索引;②最左前缀,从左向右匹配直到遇到范围查询(>、<、between)索引失效,比如:
---对于索引idx_a_b_c_d来说
select * from table where a=1 and b=2 and c=4 and d=9 and ...; ---会使用到完整的索引
select * from table where a=1 and b=2 and c=4 and e=0...; ---会使用到索引的前三个字段
select * from table where a=1 and b=2 and e=6...; ---会使用到索引的前两个字段
select * from table where a=1 and e=5...; ---会使用到索引的前一个字段
select * from table where a=1 and b=2 and c > 5 and d=9 and ...; ---因为碰到了C>5范围条件,则索引的使用会中止(c > 5 and d=9这个条件不会走索引),会使用到索引的前两个字段
select * from table where a=1 and c=4 and b=2 and d=9 and ...; ---虽然where后面的字段顺序没有按照索引的字段顺序来,但优化器会自动优化,仍然会使用到完整的索引
2.4 执行计划分析(explain)
查看执行计划,使用explain关键字(注意:explain出来的结果,不同MySQL版本会有不同结果,以下以版本5.6和8.0.19为实验版本),如下:
explain select * from user where name='老王' and age = 27;
explain出来的信息有是个:id、select_type、table、type、possible_keys、key、key_len、ref、rows、Extra。
案例表:
---用户建表
create table tuser(
id int primary key,
loginname varchar(100),
name varchar(100),
age int,
sex char(1),
dep int,
address varchar(100)
);
---部门表
create table tdep(
id int primary key,
name varchar(100)
);
---地址表
create table taddr(
id int primary key,
addr varchar(100)
);
---创建普通索引
alter table tuser add index idx_dep(dep);
---创建唯一索引
alter table tuser add unique index idx_loginname(loginname);
---创建组合索引
alter table tuser add index idx_name_age_sex(name, age, sex);
---创建全文索引,不做重点
alter table taddr add fulltext ft_addr(addr);
- id:①每个select语句都会分配一个id作为唯一标识符;②表示查询中操作表的顺序,有三种情况:a、id相同,执行顺序由上到下;b、id不同,如果是子查询,id号会自增,id越大优先级越高;c、id相同的不同的同时存在;③ id列为null表示这是一个结果集,不需要使用它来查询。
- select_type(重要):查询类型,主要用于区别普通查询、联合查询(union、union all)、子查询等复杂查询。①simple:表示不需要union操作或者不包含子查询的简单select查询。有连接查询时,外层的查询为simple,且只有一个。
mysql> explain select * from tuser;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| 1 | SIMPLE | tuser | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
②primary:一个需要union操作或者含有子查询的select,位于最外层的select的select_type为primary,且只有一个。
mysql> explain select (select name from tuser) from tuser;
+----+-------------+-------+------------+-------+---------------+------------------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+------------------+---------+------+------+----------+-------------+
| 1 | PRIMARY | tuser | NULL | index | NULL | idx_dep | 5 | NULL | 1 | 100.00 | Using index |
| 2 | SUBQUERY | tuser | NULL | index | NULL | idx_name_age_sex | 312 | NULL | 1 | 100.00 | Using index |
+----+-------------+-------+------------+-------+---------------+------------------+---------+------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)
③subquery:除了from子句中包含的子查询外,其他地方出现的子查询都有可能是subquery。
mysql> explain select * from tuser where id = (select max(id) from tuser);
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------+
| 1 | PRIMARY | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | no matching row in const table |
| 2 | SUBQUERY | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | No matching min/max row |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------+
2 rows in set, 1 warning (0.00 sec)
④dependent subquery:与dependent union类似,表示这个subquery要受到外部表查询的影响。
mysql> explain select id,name,(select name from tdep a where a.id=b.dep) from tuser b;
+----+--------------------+-------+------------+--------+---------------+---------+---------+------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------------+-------+------------+--------+---------------+---------+---------+------------+------+----------+-------+
| 1 | PRIMARY | b | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
| 2 | DEPENDENT SUBQUERY | a | NULL | eq_ref | PRIMARY | PRIMARY | 4 | mazh.b.dep | 1 | 100.00 | NULL |
+----+--------------------+-------+------------+--------+---------------+---------+---------+------------+------+----------+-------+
2 rows in set, 2 warnings (0.00 sec)
⑤union:union连接的两个select查询,第一个select_type是primary,除了第一个select_type外,第二个开始的select_type都是union。
mysql> explain select * from tuser where sex='1' union select * from tuser where sex='2';
+----+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| 1 | PRIMARY | tuser | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using where |
| 2 | UNION | tuser | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using where |
| NULL | UNION RESULT | <union1,2> | NULL | ALL | NULL | NULL | NULL | NULL | NULL | NULL | Using temporary |
+----+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
3 rows in set, 1 warning (0.01 sec)
⑥dependent union:与union一样,出现在union或union all语句中,但是这个查询要受到外部查询的影响。
mysql> explain select * from tuser where sex in (select sex from tuser where sex='1' union select sex from tuser where sex='2');
+----+--------------------+------------+------------+-------+------------------+------------------+---------+------+------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------------+------------+------------+-------+------------------+------------------+---------+------+------+----------+--------------------------+
| 1 | PRIMARY | tuser | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using where |
| 2 | DEPENDENT SUBQUERY | tuser | NULL | index | idx_name_age_sex | idx_name_age_sex | 312 | NULL | 1 | 100.00 | Using where; Using index |
| 3 | DEPENDENT UNION | tuser | NULL | index | idx_name_age_sex | idx_name_age_sex | 312 | NULL | 1 | 100.00 | Using where; Using index |
| NULL | UNION RESULT | <union2,3> | NULL | ALL | NULL | NULL | NULL | NULL | NULL | NULL | Using temporary |
+----+--------------------+------------+------------+-------+------------------+------------------+---------+------+------+----------+--------------------------+
4 rows in set, 1 warning (0.00 sec)
⑦union result:包含union的结果集,在union或union all语句中,因为它不参与查询,所以id字段为null。
⑧derived:from子句中出现的子查询,也叫做派生表,其他数据库中可能叫做内联视图或嵌套select。版本5和8不同。
mysql> explain select * from (select * from tuser) a where id=2;
+----+-------------+------------+------+---------------+-------------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+------+---------------+-------------+---------+-------+------+-------+
| 1 | PRIMARY | <derived2> | ref | <auto_key0> | <auto_key0> | 4 | const | 0 | NULL |
| 2 | DERIVED | t | ALL | NULL | NULL | NULL | NULL | 4 | NULL |
+----+-------------+------------+------+---------------+-------------+---------+-------+------+-------+
以上测试的MySQL版本是5,在版本8中测试的结果没有出现derived:
mysql> explain select * from (select * from tuser) a where id=2;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------+
| 1 | SIMPLE | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | no matching row in const table |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------+
1 row in set, 1 warning (0.00 sec)
- table:表名,如果使用了别名,此处显示的是别名;如果不涉及对真实数据表的操作则显示null;如果显示为尖括号括起来的<derived N>则表示这是个临时表(版本8有可能不同),后面的N则是执行计划中的id,表示结果来自于这个select;如果是尖括号括起来的<union M,N>,则与<derived N>类似,也是一个临时表,表示这个结果集来自于union查询的且id为M,N的结果集。
mysql> explain select * from tuser where sex in (select sex from tuser where sex='1' union select sex from tuser where sex='2');
+----+--------------------+------------+------------+-------+------------------+------------------+---------+------+------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------------+------------+------------+-------+------------------+------------------+---------+------+------+----------+--------------------------+
| 1 | PRIMARY | tuser | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using where |
| 2 | DEPENDENT SUBQUERY | tuser | NULL | index | idx_name_age_sex | idx_name_age_sex | 312 | NULL | 1 | 100.00 | Using where; Using index |
| 3 | DEPENDENT UNION | tuser | NULL | index | idx_name_age_sex | idx_name_age_sex | 312 | NULL | 1 | 100.00 | Using where; Using index |
| NULL | UNION RESULT | <union2,3> | NULL | ALL | NULL | NULL | NULL | NULL | NULL | NULL | Using temporary |
+----+--------------------+------------+------------+-------+------------------+------------------+---------+------+------+----------+--------------------------+
4 rows in set, 1 warning (0.00 sec)
- type(重要):type的值依次从好到差为system、const、eq_ref、ref、fulltext、ref_or_null、unique_subquery、index_subquery、range、index_merge、index、all。除了all以外,其他的都代表使用了索引,除了index_merge以外,其他的值代表只可以使用到一个索引(优化器会自动选用最优索引)。使用索引的效果至少要达到type值为range级别。
①system:表中只有一条数据或者是空表。版本5和8不同。
版本8中的结果:
mysql> explain select * from (select * from tuser where id = 1) a;
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| 1 | SIMPLE | tuser | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
②const(重要):使用主键索引或者唯一索引,返回记录是一行记录的等值where条件时,通常type值为const,其他数据库也叫唯一索引扫描。版本5和8相同。
主键索引
mysql> explain select * from tuser where id = 1;
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| 1 | SIMPLE | tuser | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
唯一索引
mysql> explain select * from tuser where loginname = 'admin';
+----+-------------+-------+------------+-------+---------------+---------------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------------+---------+-------+------+----------+-------+
| 1 | SIMPLE | tuser | NULL | const | idx_loginname | idx_loginname | 303 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+-------+---------------+---------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
③eq_ref(重要): 使用主键关联或者唯一索引关联时。此类型主要出现在多表关联join查询,表示对于前表的每一个结果都只能匹配到后表的一行结果,且查询的比较操作符通常是=,效率较高。
mysql> explain select a.id from tuser a left join tdep b on a.dep = b.id;
+----+-------------+-------+------------+--------+---------------+---------+---------+------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+---------------+---------+---------+------------+------+----------+-------------+
| 1 | SIMPLE | a | NULL | index | NULL | idx_dep | 5 | NULL | 1 | 100.00 | Using index |
| 1 | SIMPLE | b | NULL | eq_ref | PRIMARY | PRIMARY | 4 | mazh.a.dep | 1 | 100.00 | Using index |
+----+-------------+-------+------------+--------+---------------+---------+---------+------------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)
如果将tuser表中的dep字段上的索引干掉,则
mysql> drop index idx_dep on tuser;
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> explain select a.id from tuser a left join tdep b on a.dep = b.id;
+----+-------------+-------+------------+--------+---------------+---------+---------+------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+---------------+---------+---------+------------+------+----------+-------------+
| 1 | SIMPLE | a | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
| 1 | SIMPLE | b | NULL | eq_ref | PRIMARY | PRIMARY | 4 | mazh.a.dep | 1 | 100.00 | Using index |
+----+-------------+-------+------------+--------+---------------+---------+---------+------------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)
④ref(重要):针对非唯一索引,即普通索引,使用非主键索引进行等值查询。
mysql> alter table tuser add index idx_dep(dep);
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0
---非唯一索引
mysql> explain select * from tuser where dep = 1;
+----+-------------+-------+------------+------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+---------+---------+-------+------+----------+-------+
| 1 | SIMPLE | tuser | NULL | ref | idx_dep | idx_dep | 5 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+---------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
---非主键索引等值查询,b.name上无索引
mysql> explain select a.id from tuser a left join tdep b on a.name = b.name;
+----+-------------+-------+------------+-------+---------------+------------------+---------+------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+------------------+---------+------+------+----------+----------------------------------------------------+
| 1 | SIMPLE | a | NULL | index | NULL | idx_name_age_sex | 312 | NULL | 1 | 100.00 | Using index |
| 1 | SIMPLE | b | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+-------+------------+-------+---------------+------------------+---------+------+------+----------+----------------------------------------------------+
2 rows in set, 1 warning (0.00 sec)
---非主键索引等值查询,b.name上有索引
mysql> alter table tdep add index idx_name(name);
Query OK, 0 rows affected (0.03 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> explain select a.id from tuser a left join tdep b on a.name = b.name;
+----+-------------+-------+------------+-------+---------------+------------------+---------+-------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+------------------+---------+-------------+------+----------+-------------+
| 1 | SIMPLE | a | NULL | index | NULL | idx_name_age_sex | 312 | NULL | 1 | 100.00 | Using index |
| 1 | SIMPLE | b | NULL | ref | idx_name | idx_name | 303 | mazh.a.name | 1 | 100.00 | Using index |
+----+-------------+-------+------------+-------+---------------+------------------+---------+-------------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)
⑤fulltext: 全文索引检索,要注意,全文检索的优先级很高,若全文索引和普通索同时存在时,MySQL不考虑代价,优先使用全文索引。
mysql> explain select * from taddr where match(addr) against('朝阳');
+----+-------------+-------+------------+----------+---------------+---------+---------+-------+------+----------+-------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+----------+---------------+---------+---------+-------+------+----------+-------------------------------+
| 1 | SIMPLE | taddr | NULL | fulltext | ft_addr | ft_addr | 0 | const | 1 | 100.00 | Using where; Ft_hints: sorted |
+----+-------------+-------+------------+----------+---------------+---------+---------+-------+------+----------+-------------------------------+
1 row in set, 1 warning (0.03 sec)
⑥ref_or_null:与ref类似,只是增加了null值得比较,实际用到的不多。
⑦unique_subquery:用于where中使用到in的子查询,子查询返回不重复值,即唯一值。不多见
⑧index_subquery:用于使用到in的子查询且此子查询使用到了辅助索引或者常数列表,子查询可能返回重复值,可以使用索引将子查询去重。
⑨range(重要):索引范围扫描,常见于使用了>、<、is null、between、in、like等运算符的查询
mysql> explain select * from tuser where id > 1;
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| 1 | SIMPLE | tuser | NULL | range | PRIMARY | PRIMARY | 4 | NULL | 1 | 100.00 | Using where |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
---使用了前缀索引
mysql> explain select * from tuser where name like 'a%';
+----+-------------+-------+------------+-------+------------------+------------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+------------------+------------------+---------+------+------+----------+-----------------------+
| 1 | SIMPLE | tuser | NULL | range | idx_name_age_sex | idx_name_age_sex | 303 | NULL | 1 | 100.00 | Using index condition |
+----+-------------+-------+------------+-------+------------------+------------------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.01 sec)
⑩index_merge:表示查询使用了多个索引,最后取交集或者并集,常见and、or的查询条件使用了不同的索引,官方排序这个在ref_or_null之后,但是实际上要读取多个索引,性能可能大部分时候都不如range。
⑪index(重要):核心是使用到了索引覆盖。查询条件用到的字段属于索引树中的节点所含的字段,可能没有完全匹配节点中的索引字段。
索引全表扫描:把索引树从头到尾扫描一遍,常见于使用索引树就可以得到结果,无需回表;也可能是使用到索引进行排序或者分组的查询。
mysql> explain select loginname from tuser;
+----+-------------+-------+------------+-------+---------------+---------------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------------+---------+------+------+----------+-------------+
| 1 | SIMPLE | tuser | NULL | index | NULL | idx_loginname | 303 | NULL | 1 | 100.00 | Using index |
+----+-------------+-------+------------+-------+---------------+---------------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
由于 loginname和name没有在同一棵索引树上,因此一下这个SQL造成了回表操作:
mysql> explain select loginname,name from tuser;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| 1 | SIMPLE | tuser | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
用到select * 的查询SQL,一般也会造成回表,原因就是*代表了所有字段,极大概率不会对所有字段建立索引:
mysql> explain select * from tuser;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| 1 | SIMPLE | tuser | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
如果非要select * 且想要用到索引快速得到结果集,可以使用order by id子句实现,原因是会直接到主键索引树的叶子节点直接把结果拿到,索引树事先已排好序:
mysql> explain select * from tuser order by id;
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------+
| 1 | SIMPLE | tuser | NULL | index | NULL | PRIMARY | 4 | NULL | 1 | 100.00 | NULL |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
⑫all(重要):全表扫描,然后在server层(SQL Layer层)进行数据过滤进而返回想要的结果集。如果使用到索引的查询,会在存储引擎层直接返回结果,不需要经过server层。
mysql> explain select * from tuser;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| 1 | SIMPLE | tuser | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
- possible_keys:可能使用到的索引列表,最终使用到的索引是用key值指出的那个。
mysql> explain select * from tuser where id = 1 and loginname = 'admin' and name = '管理员';
+----+-------------+-------+------------+-------+----------------------------------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+----------------------------------------+---------+---------+-------+------+----------+-------+
| 1 | SIMPLE | tuser | NULL | const | PRIMARY,idx_loginname,idx_name_age_sex | PRIMARY | 4 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+-------+----------------------------------------+---------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
- key:最终使用到的那个索引,select_type值为index_merge的时候,此处的索引有可能是多个,其他的select_type值时,此处只会出现一个索引。
- key_len:使用到的索引的长度(组成索引的字段值的长度)。①如果是单列索引,则是整个索引长度;②如果是组合索引,那么最终使用到的不一定是所有的索引列,使用几个字段,此处就显示这几个字段值的总长度;③根据这个列的值,就可以算出当前使用了索引的前几个(最左前缀)字段了;④另外key_len只计算where条件使用到的索引长度,而排序和分组即便使用到了索引,也不会计算到key_len中。⑤key_len的主要意义就是在于观察组合索引的使用情况。
- ref:①使用到常量等值查询时,此列会出现const值。②如果是连接查询,被驱动表的执行计划这里会显示驱动表的关联字段。如果查询条件使用到了表达式或者函数,或者条件列发生了内部隐式转换,这里可能显示func。
mysql> explain select a.* from tuser a join tdep b on a.dep = b.id;
+----+-------------+-------+------------+--------+---------------+---------+---------+------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+---------------+---------+---------+------------+------+----------+-------------+
| 1 | SIMPLE | a | NULL | ALL | idx_dep | NULL | NULL | NULL | 1 | 100.00 | Using where |
| 1 | SIMPLE | b | NULL | eq_ref | PRIMARY | PRIMARY | 4 | mazh.a.dep | 1 | 100.00 | Using index |
+----+-------------+-------+------------+--------+---------------+---------+---------+------------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)
- rows:执行计划中估算的扫描行数。不是精确值(InnoDB不是精确值,MyISAM是精确值主要原因是InnoDB使用了MVCC并发机制)。
- Extra(重要):这个列显示不适合在其他列中显示但非常重要的信息。这个列可以显示的信息非常多,有几十种,值为NULL时效率最高。常见的值有:
①Distinct:表示在select查询时使用到了distinct关键字。distinct操作是需要用到一张临时表的,以便进行数据的过滤,一条一条的进行比对过滤。
②no_tables_used:不带from子句的查询或者from dual查询;使用not in形式的子查询或者not exists运算符的子查询,这种叫做反连接。即一般连接查询是先查询内表再查询外表,而反连接是先查询外表再查询内表。
③Using filesort(重要):代表全文检索,效率非常低。排序时无法使用到索引时会出现,常见于order by和group by语句中;代表MySQL会使用到一个外部的索引进行排序,而不是按照索引顺序进行读取;MySQL中无法使用索引完成的排序操作称为文件排序。
mysql> explain select * from tuser order by address \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: tuser
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1
filtered: 100.00
Extra: Using filesort
1 row in set, 1 warning (0.00 sec)
④Using_index(重要):获取结果时不需要回表查询,直接通过索引树就可以得到结果集;表示相应的select查询中使用到了覆盖索引(covering index),避免访问表的数据行,效率高;如果同时出现Using where,说明索引被用来执行过滤索引键值的操作;如果没有同时出现Using where,表明索引被用来读取数据而非执行过滤索引键值的操作。
mysql> explain select name,age,sex from tuser \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: tuser
partitions: NULL
type: index
possible_keys: NULL
key: idx_name_age_sex
key_len: 312
ref: NULL
rows: 1
filtered: 100.00
Extra: Using index
1 row in set, 1 warning (0.01 sec)
⑤Using_temporary:证明使用了临时表存储中间结果。MySQL在对order by和group by操作使用了临时表;临时表可以是内存临时表和磁盘临时表,在执行计划中看不出来,需要查看status变量,used_tmp_table、used_tmp_disk_table才能看出来。
mysql> explain select distinct a.id from tuser a join tdep b on a.dep = b.id \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: a
partitions: NULL
type: index
possible_keys: PRIMARY,idx_loginname,idx_name_age_sex,idx_dep
key: idx_dep
key_len: 5
ref: NULL
rows: 1
filtered: 100.00
Extra: Using where; Using index; Using temporary
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: b
partitions: NULL
type: eq_ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: mazh.a.dep
rows: 1
filtered: 100.00
Extra: Using index; Distinct
2 rows in set, 1 warning (0.00 sec)
⑥Using where(重要):表示存储引擎返回的记录并不是所有的都满足查询条件,需要在server层进行过滤。 根据这个信息可以判断数据是在server层过滤还是存储引擎层过滤。
---查询条件无索引
mysql> explain select * from tuser where address = '' \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: tuser
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1
filtered: 100.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
---索引失效
mysql> explain select * from tuser where age = 11 \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: tuser
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1
filtered: 100.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
-----------------------------------------------------------------------
mysql> explain select * from tuser where id in (1, 2) \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: tuser
partitions: NULL
type: range
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: NULL
rows: 2
filtered: 100.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
查询条件中分为限制条件和检查条件(Where条件),5.6以及之前的版本,存储引擎只能根据限制条件扫描结果并提供给server层,然后server层根据检查条件(Where条件)进行过滤并返回结果集;5.6.x之后的版本,支持ICP特性(索引下推),可以把检查条件也下推到存储引擎层,不符合检查条件和限制条件的数据直接不读取,这样就大大降低了存储引擎层处理的数据量,Extra列显示Using index condition. 简单理解索引下推就是将where条件由server层下推到存储引擎层进行处理,即存储引擎跟数据文件打交道的时候直接就过滤了数据,无需提供给server层进行再次过滤。
mysql> explain select * from tuser where name like 'adm%' \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: tuser
partitions: NULL
type: range
possible_keys: idx_name_age_sex
key: idx_name_age_sex
key_len: 303
ref: NULL
rows: 1
filtered: 100.00
Extra: Using index condition
1 row in set, 1 warning (0.00 sec)
2.4.1 索引失效
- 全值匹配我最爱
- 最左前缀原则
- 不在索引列上做任何操作(计算、函数、自动或手动类型转换),否则会导致索引失效而使用全表扫描。
- 存储引擎不能使用索引中范围查询右边的列。
- 尽量使用覆盖索引(只访问索引树的查询,即索引列包含查询列),尽量减少select * 的操作。
- 在使用不等于操作符的时候,索引失效,会导致全表扫描。
- is null、is not null对主键索引失效,辅助索引有效。5.6版本以前都失效。
- like的匹配值以通配符开头(like '%345')的索引也会失效。解决like '%aaa%'索引失效的方法:使用覆盖索引
mysql> explain select name,age,sex from tuser where name like '%d%' \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: tuser
partitions: NULL
type: index
possible_keys: NULL
key: idx_name_age_sex
key_len: 312
ref: NULL
rows: 1
filtered: 100.00
Extra: Using where; Using index
1 row in set, 1 warning (0.00 sec)
- 字符串不加单引号,索引失效。但不会报错。
- 少用or,用or来连接的时候索引失效。
- 总结:
全值匹配我最爱,最左前缀要遵守。
带头大哥不能死,中间兄弟不能断。
索引列上少计算,范围之后全失效。
like百分写最右,覆盖索引不写星。
主键判空还有or,索引失效要少用。
使用in关键字会用到索引,type为range。
3. MySQL锁
3.1 表级锁
由SQL Layer层来实现(无存储引擎之分),分为表锁、元数据锁(meta data lock,MDL)。另外还有InnoDB自己实现的表级锁,即意向锁,为MySQL内部使用,不需要用户干预。
MySQL表级锁的争用状态变量为:
mysql> show status like 'table%';
+----------------------------+-------+
| Variable_name | Value |
+----------------------------+-------+
| Table_locks_immediate | 1 |
| Table_locks_waited | 0 |
| Table_open_cache_hits | 116 |
| Table_open_cache_misses | 27 |
| Table_open_cache_overflows | 0 |
+----------------------------+-------+
5 rows in set (0.01 sec)
- Table_locks_immediate:产生表级锁定的次数
- Table_locks_waited:出现表级锁争用而发生等待的次数
create table mylock (
id int not null auto_increment,
name varchar(100) default null,
primary key (id)
);
insert into mylock values(1,'a');
insert into mylock values(2,'b');
insert into mylock values(3,'c');
insert into mylock values(4,'d');
表锁的两种表现形式:共享读、排他写。
- 表共享读锁(Table read lock):
在给一张表加了读锁后,不能访问其他的非锁定表,且只能对加了读锁的表进行查询操作。
- 表独占写锁(Table write lock):
在给一张表加了写锁后,不能访问其他的非锁定表,但可对加了写锁的表进行读写操作。
3.2 元数据锁
DML(metaDataLock),属于表锁,元数据其实就是表结构。在MySQL5.5版本中加入了MDL锁。当对一个表进行增删改查操作的时候,自动加MDL读锁;当对表结构做变更时,自动加MDL写锁。即在做增删改查的时候不能改变表结构,变更表结构的时候不能对表数据进行增删改查。
MDL锁只对当前表起作用,不妨碍其他非锁定表的正常访问。
3.3 行级锁
由InnoDB存储引擎实现,按照锁定范围来划分,有三种:
- 记录锁(Record Locks):锁定索引中的一条记录,一般是由主键指定(where id = 1)。
- 间隙锁(Gap Locks):锁定记录前、记录中、记录后的数据行。
- Next-key锁:只出现在RR事务隔离级别中。记录锁+间隙锁。
按照功能来划分,有两种:
- 共享读锁(S):允许一个事务去读数据集,阻止其他事务获取相同数据集的排他写锁(但允许其他事务同时加共享读锁)。需要手动添加,使用 lock in share mode 关键字。
---共享读锁,需要手动添加
select * from table_name where ... lock in share mode;
---无锁
select * from table_name;
- 排他写锁(X):允许当前获得排他写锁的事务更新数据,阻止其他事务获取相同数据集的共享读锁(不是不能读)和排他写锁。
①自动加排他锁:对于update、delete、insert操作,InnoDB会自动给涉及到的数据集加排他锁。
②手动加排他锁:使用 for update 关键字。
---手动加排他锁
select * from table_name where ... for update;
3.4 意向锁
由InnoDB自己实现的表级锁,即意向锁,为MySQL内部使用,不需要用户干预。
- 意向共享读锁(IS):当事务打算给数据集加共享读锁前,必须先获取当前表的IS锁。
- 意向排他写锁(IX):当事务打算给数据集加排他写锁前,必须先获取当前表的IX锁。
意向锁的主要作用是,为了“全表更新数据”时的性能提升,否则在全表更新数据(update不带条件)时,需要先检索相关记录上是否有行锁。
各个锁的兼容性:
3.5 两阶段锁
在一个事务操作中,分为加锁阶段
和解锁阶段
,且所有的加锁操作在解锁操作之前,如下图:
①加锁阶段:当对记录进行更新操作或者 select ... for update、select ... lock in share mode时,会对记录进行加锁。只加锁不解锁。
②解锁阶段: 在一个事务中,只有在进行 commit 或者 roll back 操作时才是解锁阶段。只解锁不加锁。
两阶段加锁最佳实践:下面举个具体的例子,来讲述二段锁对应用性能的影响,我们举个库存扣减的例子:
---方案一:
start transaction;
// 锁定用户账户表
select * from t_accout where acount_id = 234 for update;
// 生成订单
insert into t_trans ... ;
// 减库存
update t_inventory set num = num - 3 where id = 111 and num >= 3;
commit;
-------------------------------------------------------------
---方案二:
start transaction;
// 减库存
update t_inventory set num=num-3 where id = 111 and num >= 3;
// 锁定用户账户表
select * from t_accout where acount_id = 234 for update;
//生成订单
insert into t_trans ... ;
commit;
我们的应用通过JDBC
操作数据库时,底层本质上还是走TCP
进行通信,MySQL协议
是一种停--等式协议
(和http
协议类似,每发送完一个分组就停止发送,等待对方的确认,在收到确认后再发送下一个分组),既然通过网络进行通信,就必然会有延迟,两种方案的网络通信时序图如下:
由于商品库存往往是最致命的热点,是整个服务的热点。如果采用第一种方案的话,TPS
理论上可以提升3rt/rt=3
倍。而这是在一个事务中只有3条SQL的情况,理论上多一条SQL就多一个rt时间。
另外,当更新操作到达数据库的那个点,才算加锁成功。commit
到达数据库的时候才算解锁成功。所以,更新操作的前半个rt
和commit
操作的后半个rt
都不计算在整个锁库存的时间内。
性能优化:从上面的例子可以看出,在一个事务操作中,将对最热点记录的操作放到事务的最后面,这样可以显著地提高服务的吞吐量
。
select ... for update 和 update ... where ...的选择:我们可以将一些简单的判断逻辑写到update操作的谓词里面,这样可以减少加锁的时间,如下:
方案一:
start transaction;
int num = select count from t_inventory where id = 234 for update;
if num >= 3
update t_inventory set num = num - 3 where id = 234;
commit;
else
rollback;
----------------------------------------------------------------------
方案二:
start transaction;
int affectedRows = update t_inventory set num = num - 3 where id = 234 and num >= 3;
if affectedRows > 0
commit;
else
rollback;
延时图如下:
从上图可以看出,加了update谓词以后,一个事务少了1rt的锁记录时间(update谓词和select for update对记录加的都是X锁,所以效果是一样的) 。
加锁SQL都或多或少会遇到死锁这个问题。上面的最佳实践中,笔者建议在一个事务中,对记录的加锁按照记录的热点程度升序排列,对与任何会并发的SQL都必须按照相同的顺序来处理,否则会导致死锁,如下图:
3.6 行锁演示
InnoDB的行锁是通过给索引上的索引项加锁来实现的。因此InnoDB这种行锁的实现特点意味着:只有通过索引条件检索的数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。无索引则不支持行锁。
查看行锁状态
mysql> show status like 'innodb_row_lock%';
+-------------------------------+-------+
| Variable_name | Value |
+-------------------------------+-------+
| Innodb_row_lock_current_waits | 0 |
| Innodb_row_lock_time | 0 |
| Innodb_row_lock_time_avg | 0 |
| Innodb_row_lock_time_max | 0 |
| Innodb_row_lock_waits | 0 |
+-------------------------------+-------+
5 rows in set (0.01 sec)
Innodb_row_lock_current_wait:等待锁的数量
Innodb_row_lock_time:从系统启动到现在锁定的总时长
Innodb_row_lock_time_avg:每次锁定所花的平均时长
Innodb_row_lock_time_max:从系统启动到目前为止,等待时间最长的一次的时间值
Innodb_row_lock_waits:从系统启动到目前为止,总等待次数
①行读锁:
行读锁升级为表锁:
因为name字段上无索引,所以where name = 'c'会给整张表上共享读锁。
②行写锁:
以上代码中的第2、3步只所以能执行成功,是因为单纯的select不会对数据加锁,因此即便在记录上已有排他写锁后,其他事务仍可以进行查询操作(此处涉及到MVCC);第4步执行不成功的原因是由于共享读锁和排他写锁是互斥的,已经加了排他写锁的记录,不再允许其他事务加读锁和写锁。
3.7 间隙锁
表中的数据如下:
create table news(
id int,
number int,
primary key (id)
);
insert into news value(1, 2);
insert into news value(3, 4);
insert into news value(6, 5);
insert into news value(8, 5);
insert into news value(10, 5);
insert into news value(13, 11);
alter table news add index idx_num(number); ---加非唯一索引
mysql> select * from news;
+----+--------+
| id | number |
+----+--------+
| 1 | 2 |
| 3 | 4 |
| 6 | 5 |
| 8 | 5 |
| 10 | 5 |
| 13 | 11 |
+----+--------+
6 rows in set (0.00 sec)
---session 1 :
start transaction;
select * from news where number = 4 for update;
---session 2 :
start transaction;
insert into news value(2, 4); --阻塞
insert into news value(2, 2);--阻塞
insert into news value(4, 4);--阻塞
insert into news value(4, 5);--阻塞
insert into news value(7, 5);--成功
insert into news value(7, 2);--阻塞
insert into news value(7, 3);--阻塞
insert into news value(7, 4);--阻塞
insert into news value(9, 5);--成功
insert into news value(11, 5);--成功
示例中,select * from news where number = 4 for update; 锁定的是 number区间[2, 5]即ID区间[1, 6]之间的数据。按照插入顺序进行排序。insert的语句所要插入的数值,经过测试发现:
①ID在区间[1, 6]内时,只要number也同时落入区间[2, 5](全闭合)则不会插入成功,落入区间[2, 5]外则成功;
②当ID不在区间[1, 6]内时,number如果落入区间[2, 5)(前闭后开即不含5)内不会插入成功,落入区间[2, 5)外则插入成功。比如以上示例中的:
主键产生间隙锁:
select * from news where id > 1 and id < 4 for update; ---锁定ID范围[1, 4]
使用主键范围作为检索条件时,也会产生间隙锁。
非唯一索引作为条件没有匹配记录时:
select * from news where number = 13 for update; ---number=13不存在且是最后一条记录
检索条件number=13,向左取得最靠近的(id=13,number=11)这条记录作为左区间临界记录,向右由于没有数据,则取无穷大作为右区间。
①ID在区间(13, ∞)内时,只要number也同时落入区间[11, ∞)则不会插入成功,落入此区间外则成功;
②当ID不在区间(13, ∞)内时,number如果落入区间(11, ∞)内不会插入成功,落入此区间外则插入成功。
3.8 死锁
两个事务(或者session)相互等待对方释放资源才能继续。行死锁示例:
4 事务
4.1 事务简介
MySQL中的事务是由存储引擎实现的,用来维护数据的完整性。而且支持事务的存储引擎不多,常用的就是InnoDB。
事务四大特性(ACID):
- Atomicity(原子性):构成事务的所有操作必须是一个逻辑单元。要么全执行要么全部不执行
- Consistency(一致性):数据库数据在事务执行的前后状态都必须是稳定的或者一致的
- Isolation(隔离性):事务之间互相不会影响,各自独立,由锁机制和MVCC(多版本并发控制,优化读写性能,读不加锁,读写不冲突)机制来实现
- Durability(持久性):事务执行成功后必须全部写入磁盘
事务操作:
- BEGIN或者START TRANSACTION,显式的开启一个事务
- COMMIT或者COMMIT WORK进行提交事务,二者等价,并将当前事务对数据库的所有更改持久化
- ROLLBACK或者ROLLBACK WORK进行回滚事务,二者等价,回滚会结束当前事务,并撤销该事务所做的所有更改
4.2 InnoDB架构图
InnoDB存储引擎由内存池、后台线程和磁盘文件三大部分构成。如下图:
4.2.1 InnoDB的内存结构:
- Buffer pool(缓冲池)
当一条更新数据的SQL命令过来后,数据不会直接写入磁盘,即不会由一个线程完成,而是由线程A先写入缓冲池,然后由线程B在其他时间写入磁盘落地。即一个线程负责向缓冲池放数据,另外一个线程负责从缓冲池取出数据。
①数据页和索引页:Page是InnoDB存储的最基本结构,也是InnoDB磁盘管理的最小单位。当做增删改操作时,缓存里面的数据页和磁盘上的数据页会不一致,此时,缓存内的该数据页称作脏页。
②插入缓冲(Insert Buffer):插入操作的处理较为复杂,过程包括主键排序、索引更新、插入算法的涉及等。因此插入操作会有单独的缓冲区,即插入缓冲区。删除和修改是操作另一缓冲区。
③自适应哈希索引(Adaptive Hash Index):使用的并非B Tree结构,而是Hash结构(key-value)。InnoDB会根据访问的频率和模式,为热点页建立哈希索引来提高查询效率。即通过索引树获取热点数据后,接下来生成Hash结构的数据缓存。
④锁信息(Lock Info):行锁、表锁等锁的信息。
⑤数据字典信息:元数据信息,即表结构数据,包括表结构、数据库名和表名、字段的数据类型、视图、索引、表字段信息、存储过程、触发器等等。
- Redo Log Buffer(重做日志缓冲)
①重做日志(Redo Log):如果要存储数据则先将数据存储到日志,一旦内存崩坏则可以从日志恢复数据。重做日志保证了数据的可靠性。InnoDB采用了Write Ahead Log(预写日志)策略,即当事务提交时,先写重做日志,然后再择时将脏页写入磁盘。如果脏页完全写入磁盘前出现宕机的情况导致数据丢失,就通过重做日志进行数据恢复。物理文件名称:ib_logfile0、ib_logfile1,默认8M,可通过配置参数innodb_log_buffer_size来配置大小。
比如当insert命令提交后,Buffer Pool中将产生脏页,接着将脏页数据写入到Redo Log 缓冲区,然后按时或者当事务提交时写入Redo Log File磁盘文件中。Redo Log File磁盘文件写入成功,则代表事务提交成功。
Force Log at commit 机制:此机制用来实现事务的持久性,即当事务提交时,必须先将该事务的所有日志写入到重做日志文件中进行持久化,然后才代表着事务的提交完成。重做日志的写入顺序为:buffer pool --> redo log buffer --> OS buffer --> ib_logfile0 。为了确保每次都能成功写入重做日志文件,在每次将重做日志缓冲写入重做日志文件前,必须调用一次 fsync(操作系统的函数)操作,将缓存数据从操作系统缓存真正写入到磁盘文件。
fsync函数的作用便是将操作系统缓存中的数据写入到物理磁盘。任何数据要写入到物理磁盘中都要经过操作系统缓存。
重做日志的落盘机制(即调用fsync函数的时机)通过参数 innodb_flush_log_at_trx_commit 来控制,该参数默认值1,有0、1、2三个值。
- Buffer Log 缓冲池落盘:采用双写机制(Double Write),双写机制保证了InnoDB存储引擎数据页的可靠性。
Double Write 由两部分组成,一部分是内存中的double write buffer,大小为2MB;另一部分是物理磁盘上共享表空间连续的128个页,总大小也为2MB。在对缓冲池中的脏页进行刷新时,并不直接写磁盘,而是通过 memcpy 函数将脏页先复制到内存中的double write buffer区域,之后通过对double write buffer内的数据分两次且每次1MB顺序地写入2MB共享表空间的物理磁盘上,然后马上调用 fsync 函数同步磁盘,避免操作系统缓冲写带来的问题。在完成double write页的写入后,再将double write buffer中的页写入各个表空间文件中(所谓的双写,即此处的写入各个表空间文件以及2MB共享表空间)。如果操作系统在将页写入磁盘的过程中发生了崩溃,在恢复过程中,InnoDB存储引擎可以从共享表空间中的double write中找到该页的一个副本,将其复制到表空间文件中,再应用重做日志。
checkpoint(检查点): 检查点,表示脏页写入到磁盘的时机,所以检查点也就意味着脏数据的写入。
检查点目的:
①缩短数据库的恢复时间;
②buffer pool 中空间不够用时,将脏页刷新到磁盘;
③Redo Log 不可用时刷新脏页。
检查点的分类:
①sharp checkpoint:完全检查点,即数据库正常关闭时,会将所有的脏页都写入到磁盘上。
②fuzzy checkpoint:模糊检查点,即部分页写入到磁盘。包括以下几种
master thread checkpoint:以每秒或者每十秒的速度从缓冲池的脏页列表中刷新一定比例的脏页回磁盘。这个过程是异步的。
flush_lru_list checkpoint:读取lru(Least Recently Used)list,找到脏页,写入磁盘。即最近最少使用的脏页优先写入写入到磁盘中。
async/sync flush checkpoint:Redo Log File 快满了的时候,会批量的触发数据页回写,这个事件触发的方式又分为同步和异步,不可被覆盖的redolog占redo log file的比值为 75%时异步,90%时同步。
dirty page too much checkpoint:默认是脏页占比75%的时候,会触发落盘,将脏页写入磁盘。
Buffer Pool 中的脏页数据一落盘成功,则 Redo Log File 中的脏页数据立马清空。
- InnoDB的磁盘文件
包括:系统表空间(即共享表空间)和用户表空间(独立表空间)
①系统表空间
1、数据字典(data dictionary):记录数据库相关信息;
2、double write buffer:解决部分写失败(页断裂);
3、insert buffer:内存insert buffer数据。周期写入系统表空间,防止以外宕机;
4、回滚段(rollback segments)
5、undo空间:undo log 页。5.6版本后通过配置可以存入独立表空间。
②独立表空间
1、每个表的数据和索引
2、每个表的表结构
3、undo空间:undo log 页。5.6版本后通过配置可以存入独立表空间。
③重做日志文件
在日志组中,每个重做日志文件(ib_logfile0和ib_logfile1)的大小一致,并以【循环写入】的方式运行。InnoDB存储引擎先写入重做日志文件1,当文件被写满时,会切换到重做日志文件2,再当重做日志文件2写满时,再切换到重做日志文件1进行写入。
4.3 InnoDB事务原理分析
4.3.0 ACID四大特性
- 原子性(atomicity):事务最小工作单位,要么全成功,要么全失败。
- 一致性(consistency):事务开始前和结束后,数据库的完整性不会被破坏。
- 隔离性(isolation):不同事务之间互不影响,四种隔离级别为RU(读未提交)、RC(读已提交)、RR(可重复读)、serializable(串行化)。
- 持久性(durability):事务提交后,对数据库的修改是永久性的,即使系统发生故障也不会丢失。
总结来说,事务的隔离性由多版本控制机制和锁来实现,而原子性、一致性和持久性是通过InnoDB 的 redo log、undo log 和 Force Log at Commit 机制来实现的。
4.3.1 原子性、持久性和一致性
原子性、持久性和一致性是通过 redo log、undo log 和 Force Log at Commit 机制来实现的。redo log 负责在系统崩溃时恢复数据;undo log 用于撤销尚未提交的事务操作,也可以用于多版本控制。而 Force Log at Commit 机制保证事务提交后 redo log 日志都已持久化。
其实,原子性和持久性保证了一致性。
4.3.2 Redo Log
redo log 写入磁盘时,必须进行一次操作系统的 fsync(将操作系统缓存中的数据刷新到磁盘) 操作,防止 redo log 只是写入了操作系统的磁盘缓冲中。参数 innodb_flush_log_trx_commit 参数可以配置 redo log 日志刷新到磁盘中的策略。
4.3.3 Undo Log
数据库崩溃重启后需要从 redo log 日志中将未落盘的脏页数据恢复出来,重新写入到磁盘,保证用户的数据不丢失。同时,在崩溃恢复的过程中还需要回滚尚未提交的事务,由于回滚操作需要 undo log 日志的支持,undo log 日志的完整性和可靠性需要 redo log 日志来保证,所以崩溃后恢复操作是先做 redo log 恢复数据,然后通过 undo log 日志进行回滚。
在事务执行的过程中,除了记录 redo log ,还会记录一定量的 undo log 。undo log 记录了数据在每个操作前的状态,如果事务执行过程中需要回滚,就可以根据 undo log 进行回滚操作。
undo log 不同于 redo log ,它存放在数据库内部的一个特殊的段(segment)中, 这个段称为回滚段。回滚段位于共享表空间中。undo 段中以 undo page 为更小的组织单位。undo page 和存储数据库数据和索引的页类似,因为 redo log 是物理日志,记录的是数据库页的物理修改操作。所以 undo log(也看成数据库数据) 的写入也会产生 redo log 日志,也就是 undo log 的产生会伴随 redo log 的产生,这是因为 undo log 也需要持久性的保护。
如上图所示,表空间中有回滚段、叶子节点段和非叶子节点段,而三者都有对应的页结构。
回滚的流程,以下的数据行都是记录在回滚段中的记录:
数据库事务的整个流程,如下图:
事务进行过程中,每次修改数据的sql语句执行都会记录 undo log 和 redo log,接着更新数据形成脏页。
redo log 按照时间或者空间要求等条件进行落盘,undo log 和脏页按照 checkpoint 进行落盘,落盘后相应的 redo log 就可以清除了。此过程中,如果事务尚未真正提交,系统发生崩溃,则首先检查 checkpoint 记录,使用相应的 redo log 进行数据和 undo log 的恢复,接下来查看 undo log 的状态发现事务尚未提交,就使用 undo log 进行事务回滚。
事务执行 commit 操作时,会将所有相关的 redo log 进行落盘,只有所有相关的 redo log 落盘成功,才算 commit 成功。然后内存中的脏页按照 checkpoint 继续落盘,如果此时发生系统崩溃,则只使用 redo log 恢复数据。
事务 commit 之前系统崩溃的话,从 undo log 中恢复数据,即回滚;如果是事务 commit 之后系统崩溃且 buffer pool 中的脏页尚未落盘,则需要从 redo log 中恢复数据。
疑难解析:上图中的虚线处有可能存在疑问,即
虚线之前,我发现 undo log 和 redo log 都没有落盘,所以“在这之前崩溃,则恢复后会进行回滚”这句话不理解,系统崩溃,缓存中的数据都会消失,何来恢复一说? 原因是“InnoDB每秒也会触发 redo log 刷盘”,且 undo log 相关的 redo log 中有 undo log 的信息,即崩溃的时候 redo log 是有已落盘可能性的,同时也会有 undo log 的相关信息落盘,恢复数据时先从 redo log 中恢复 undo log 信息,然后进行回滚,相关 redo log 清除。
4.3.4 隔离性
①事务并发问题
在事务的并发操作中可能会出现一些问题:
- 丢失更新:两个事务针对同一个数据进行更新操作时,可能会存在丢失数据的问题;
- 脏读:一个事务读取到另一个事务尚未提交的数据;
- 不可重复读:一个事务因读取到另一个事务 update 或者 delete 的数据,导致对同一数据读取多次而得到不同结果;
- 幻读:一个事务因读取到另一个事务已提交的 insert 数据,导致对同一张表读取两次以上结果不一致;
②事务隔离级别
四种隔离级别(SQL92标准),由低到高:
a、Read uncommited(读未提交):最低级别,任何问题都无法解决;
b、Read commited(RC,读已提交):可避免脏读的发生;
c、Repeatable read(RR,可重复读):数据库默认。可避免脏读、不可重复读的发生(注意事项:InnoDB的RR还可以解决幻读,主要原因是Next-key锁,只有RR才能使用Next-key锁);
多次读数据一致,即便另一个事务提交了更改也不会读到这个最新值。即可重复读。
d、Serializable(串行化):可避免脏读、不可重复读、幻读的发生;(由MVCC降级为 Locking-Base CC);当设置了隔离级为串行化时,相关的所有的sql包括查询,都会上锁,有索引的基于索引加行锁,没有索引的锁表。
设置数据库隔离级别:
---设置读未提交,只在当前session生效
set session transaction isolation level read uncommitted;
---查看隔离级
select @@tx_isolation;
想要永久生效,需要更改配置文件。
③InnoDB的MVCC实现
MVCC在mysql中的实现,依赖的是 undo log 和 read view,并且只在 RC 和 RR 隔离级下。
当前读和快照读:
在MVCC并发控制中,读可以分为两类:快照读(snapshot read)和当前读(current read)。
- 快照读:读取的是记录的可见版本(有可能是历史版本),不用加锁。
- 当前读:读取的是记录的最新版本,并且当前读返回的记录都会加锁,保证其他事务不会并发修改该记录。
在一个支持MVCC并发控制的系统中,哪些读操作是快照读,哪些操作优势当前读呢?以 MySQL InnoDB 为例:
- 快照读:简单的 select 操作(不加共享读锁的),属于快照读,且读历史版本(undo log中)。不加锁。当前也会有例外,下面会解释。
- 当前读:特殊的读操作,插入、更新、删除操作,都属于当前读,且读得是当前版本。需要加锁。加行写锁。增删改之前,都需要读取已有数据,因此也是读。
MVCC中的一致性非锁定读:
MVCC带来的最大的好处便是一致性非锁定读。
一致性非锁定读(consistent nonlocking read)是指InnoDB通过多版本并发控制(MVCC)读取当前数据库中行数据的方式。如果读取的行正在进行 delete 或者 update 操作,这时读取操作不会等待行锁的释放,而是直接读取该行的一个最新的可见快照(快照读在 undo log 中找)。
如下图所示,当会话B提交事务后,会话A再次进行 select 操作,在两个事务隔离级别下得到的结果会不一样。
读已提交(RC)得到的是修改后的新值;可重复读(RR)得到的是修改前的旧值;
Undo Log:
InnoDB 中的行记录有三个隐藏字段:分别对应该行的 rowid、事务id号 db_trx_id 和回滚指针db_roll_ptr,其中 db_trx_id 表示最近的事务id,db_row_ptr 指向回滚段中的 undo log 。
根据行为的不同,undo log 又分为:insert undo log 和 update undo log 。
- insert undo log:是在 insert 操作中产生的 undo log ,因为 insert 操作的记录只对当前事务可见, 所以一旦 roll back ,此条 undo log 将被直接删除,不需要进行 purge 操作
- update undo log:是在 update 和 delete 操作时候产生的 undo log ,因为会对已存在的记录产生影响,所以一旦 roll back , MVCC机制会找到此条记录的历史版本进行恢复。为了提供MVCC机制,因此 update undo log 不能在事务提交时就进行删除,而是在事务提交时将此 update undo log 放入到 history list(即存放历史快照) 上,等待 purge 线程进行最后的删除操作。
如下图所示(初始状态):
当事务2使用 update 对该行记录进行修改时,会首先使用排他锁对改行进行锁定,将该行的当前值记录到 undo log 中,然后再真正的对该行进行修改,最后填写事务id,使用回滚指针指向 undo log 中修改前的行。
事务链表:
MySQL中的事务在开始到提交这个过程中,都会被保存到一个叫 trx_ids 的事务链表中,这是一个基本的链表结构:
事务链表中保存的事务都是尚未提交的事务,事务一旦被提交,就会从事务链表中删除。
在RR隔离级下,在每个事务开始的时候,都会将当前系统中所有的活跃事务拷贝到一个列表中(read view)。
在RC隔离级下,在每个语句开始的时候,都会将当前系统中所有的活跃事务拷贝到一个列表中(read view)。
⑧Read View
当前事务(读)能读到哪个历史版本?
Read View 是事务开启时,当前系统中所有事务的一个集合,这个对象中存储了当前 Read View 中最大事务ID和最小事务ID。
如下就是当前活跃的事务列表:
ct-trx 代表当前事务的ID,对应上面的 Read View 数据结构如下:
low_limit_id 是高水位,即当时活跃的事务的最大ID。意思是事务ID没有比他更高的了,所以叫做高水位,如果读到 row 的 db_trx_id >= low_limit_id,说明在此之前的事务都没有提交,即这些数据都不可见。
up_limit_id 是 低水位,即当前活跃的事务的的最小ID; 意思是事务ID没有比它更低的了,所以叫低水位。如果读到 row 的 db_trx_id < up_limit_id,说明这些数据在事务开启时都已经提交,即这些数据都可见。
row 的 db_trx_id 在 low_limit_id 和 up_limit_id 之间,则查找该记录的 db_trx_id 是否在自己事务的 Read View -> trx_ids 列表中,如果在则该记录的当前版本不可见,否则该记录的当前版本可见。
不同隔离级别 Read View 实现方式:
a、read commited:
即,在RC隔离级下,在事务中每个语句在执行的时候,都会关闭 read_view 重新在row_search_for_mysql 函数中创建当前的一份 read_view,这样就会产生不可重复读(读到最新的已提交的)现象的发生。
b、repeatable read:在RR的隔离级下,创建事务 trx 结构的时候,就生成了当前事务的global_read_view(全局的 read_view,事务内共用)。使用 trx_assign_read_view 函数创建,一直持续到事务结束。在事务执行这段时间内,每一次查询都不会重新创建 read_view,从而实现了可重复读。
————————————————