[TOC]mysql
本篇内容主要来自极客时间《MySQL实战45讲》中的:算法
跳表sql
Redis 的有序集合使用的就是这个结构
优势: 查找很快, 支持范围查询数据库
缺点: 插入代价高, 必须逻辑上移动后续的全部记录segmentfault
搜索效率最高, 但实际并不采用, 由于索引是存在磁盘的.数组
假设一棵树高为20, 一次搜索就须要20个数据块, 对应磁盘就是20次随机查找. 对于普通硬盘来讲, 一次寻址约 10ms, 则一次搜索就须要 20x10ms = 200ms.若是要让一个查询尽可能少读磁盘, 那就必须尽可能少地查询数据块, 应该使用下面的多叉树.缓存
多叉树session
为了减小磁盘访问次数, 可使用 "N叉"树, 这里的 N 取决于数据块的大小.并发
以 InnoDB 中 一个整数字段为例, 这个N差很少是1200.函数
计算方法: 补充!!
若是树高为4, 则能够存储 1200^3 个值, 考虑树根数据块基本都在内存中, 所以一次搜索只须要3次磁盘查找, 考虑到第2层数据块也有很大几率在内存中, 那么访问磁盘次数就更少了.
N = 页page的大小 / 每一个索引项大小
N叉树中非叶子节点存放的是索引信息, 每一个索引项包含Key和Point指针及其余辅助数据, 其中Point指针固定大小6字节, 默认索引页的大小是16KB. 所以主键为int时, int占用4个字节, 加上辅助数据差很少每一个索引项占用13字节, 所以非叶子节点大约能够存储 16k/13 ≈ 1260 个左右.
N的大小大体是根据上述式子决定的, 所以若要调整N, 则有2个防线:
MySQL 5.6之后能够修改 page 大小, 参数: innodb_page_size
未测试
优势: 新增和查找都很快
缺点: 没法进行范围遍历, 必须一个一个查找.
索引组织表: 按照主键顺序, 以索引形式存放的表.
InnoDB 使用了 B+树 做为索引的存储结构.
InnoDB 中的索引, 按照叶子节点内容来区分, 分为两类:
InnoDB 中 B+ 树的叶子节点存放的是 页, 一页里面能够存多个行.
这里的页指的是 InnoDB 的页, 而非磁盘页, 默认大小是 16KB.
索引的维护涉及 插入 及 删除, 这两个操做可能致使 页分裂 及 页合并 的问题.
索引可能由于删除或页分裂的缘由致使数据页有空洞, 而重建索引的过程会建立一个新的索引, 并将数据顺序插入, 使得索引更紧凑, 空间利用率更高.
Q. 为何表删除了一半数据, 文件大小却没变?
A. 简单回答一下.
删除时仅仅是将数据从所在的数据页上标记删除, 遗留的空位还会保留着, 供后续插入新记录时直接存放.
这种状况能够考虑重建索引以减小磁盘空间占用
optimize table 表名; -- 或 alter table 表名 engine=InnoDB;
注意 alter table 表名 = engine=InnoDB;
会加 MDL 读锁.
若是是 MySQL 5.7, 则会使用 OnlineDDL, 避免长时间的 MDL 锁致使业务不可用.
Q. 主键索引和非主键索引的区别
A. 主要区别在于:
Q. 非主键索引中字段值相同的索引项是如何存储的?
A. 结论: 独立存储.
以索引c为例, id是主键, 假设有两个记录 (c=10, id=1), (c=10, id=2), 这其实在索引c上是两条不一样的索引项, 它的存放顺序是先按照c递增, c等值状况下再按照id递增, 所以能够理解为索引c 是 (c, id)
Q. 若不给表设置主键会怎样?
A. InnoDB 会为每一行隐式分配一个 RowId 做为主键. 因此其实仍是有主键索引的
Q. 联合索引的存储结构是怎样的?
A. 《高性能MySQL 第三版》P144,关于索引类型的插图,说明了联合索引是N个字段组合成一个索引的。
Q. 在联合索引中多个字段顺序是怎样的?
A. 以 (a,b) 为例, id 是主键. 则在该索引上, 是先按照 a 递增, 再根据 b 递增, 最后根据 id 递增的顺序排序.
能够和下面写到的 最左前缀 一块儿理解.
Q. 若是表用到了联合主键, 那么在二级索引中是如何存储的?
A. 假设联合主键是 (a,b), 此时表中还有个字段 c, 能够分3种状况考虑:
主键尽可能使用自增主键, 缘由:
int not null primary key auto_increment
或 bigint not null primary key auto_increment
, 使用整形作主键只须要4个字节, 使用长整型则是8个字节.所以从性能和存储空间看, 自增主键一般是最好的选择.
那么何时能够考虑用业务字段做为主键:
↑ 这就是典型的 KV 场景了, 考虑到查询时尽可能用主键索引, 避免回表, 此时就能够将这个索引设置为主键.
当查询语句中涉及的全部字段都在同一个索引中, 此时因为只须要在该索引树上查找而不须要回表, 这成为覆盖索引.
覆盖索引能够减小树的搜索次数, 显著提高性能, 所以是经常使用的优化手段.
注意: 索引的维护是有代价的, 所以是否新增冗余索引来支持覆盖索引时须要权衡考量.
以索引 (code, name) 为例, 当使用以下语句时是能够用到覆盖索引, 避免回表的:
select name from 表 where code = "xxx"; -- 或 select id from 表 where code = "xxx";
Q. 是否有必要为了覆盖索引而设立联合索引?
A. 分状况:
最左前缀指的是联合索引的前几个字段, 以及字符串索引的前几个字符.
因为索引是以B+树结构存储的, 而B+树这种索引结构是能够利用索引的最左前缀来定位记录的.
以 (name, age) 这个联合索引为例, 它的大体示意图以下:
能够看出索引项的顺序是按照索引定义的字段顺序来排序的.
如下语句会用到上面的这个索引的最左前缀:
-- 联合索引上的最左N个字段 select * from 表 where name = "xx"; -- 字符串的最左N个字符 select * from 表 where name like '张%';
Q. 联合索引上的字段顺序如何肯定?
A. 优先考虑复用能力, 其次考虑存储空间.
原则1: 若是经过调整顺序能够少建立一个索引, 那么一般就会优先考虑调整后的这个顺序了.
原则2: 优先考虑原则1, 其次应考虑空间占用.
以联合索引 (a,b) 为例, 因为最左前缀优化的缘由, 在该表上就不须要单独再创建索引 (a) 了, 所以这种状况只须要创建一个联合索引 (a,b) 便可.
可是, 若是此时一样须要用到索引 (b), 那么这时候有两个选择:
此时若字段a比较大, 则应考虑方案1, 不然应考虑方案2.
对于联合索引, 对于不知足最左前缀的部分, 在某些状况下是能够用到 索引下推 的.
索引下推: 在索引遍历过程当中, 利用索引中已有的字段过滤不知足条件的记录, 避免每次判断都回表.
先明确:
在 explain 的时候能够在 Extra 看到 Using index condition
, 说明能够用到索引下推
"能够"用, 但不必定用/没有.这个地方还不大明确
以索引 (name, age) 为例, 查看一下SQL语句:
select * from 表 where name like '张%' and age > 20;
此时会先利用索引, 快速找到 name以"张"开头的记录, 而后依次向右遍历:
前提: 业务能保证记录是惟一的状况下, 才须要考虑.
理解这部份内容的 意义:在遇到大量插入数据慢, 内存命中率低的状况下, 多一个排查思路.
相关配置:
## 最大占用 innodb_buffer_poll 内存空间的百分比 innodb_change_buffer_max_size=50
Change Buffer
Merge 发生的状况:
想象一下, 一张表有4,5个普通二级索引, 这些索引的使用率并不高.同时该表会频繁更新数据, 若没有Change Buffer, 那么每次更新操做维护二级索引时都须要从磁盘读入索引对应的数据页, 而有了Change Buffer后只需将这些操做保存在该Buffer中, 极大减小了磁盘随机读次数, 最后统一Merge便可.
查询过程的区别:
从索引树根目录, 逐层查找对应记录所在数据页.
若不在内存中, 则须要先从磁盘上读入内存.
若数据所在页已经在内存中, 则读取该记录, 并向右遍历直到不符合条件. 因为数据的读取是以数据页为单位(默认16KB), 所以这个过程是在内存中, 对性能影响极小, 除非是记录恰好在数据页的最后一条.(考虑到几率, 能够忽略)
相似普通索引, 只是在找到对应一条记录后就中止了.
结论: 在查询过程当中性能区别不大.
更新过程的区别:
若数据都在内存中则没有什么区别, 所以如下只讨论不在内存中的状况.
将更新语句记录在 Change Buffer 中, 更新结束.
因为更新操做须要判断是否违反数据一致性约束, 所以没法使用 Change Buffer, 须要先将数据页从磁盘读取到内存, 进行判断, 再作更新操做.
Q. Change Buffer 何时会成为负优化?
A. 在下述普通索引场景:
当每次更新操做后立刻读取, 因为更新操做会缓存在Change Buffer中, 下一次立刻读取时须要当即 Merge.
此时反而多了维护 Change Buffer的代价, 同时随机访问IO不会减小.
Q. Change Buffer 适合什么场景?
A. 写多读少业务
Change Buffer 会将更新的操做缓存起来, 缓存得越多, 则在 Merge 操做的时候收益就越大.
常见业务模型: 帐单类, 日志类系统.
当须要建立联合索引的状况下, 在 不考虑索引复用 前提, 且 字段顺序不影响索引完整使用 前提下, 如何肯定联合索引中的字段顺序?
!!! 注意这里的前提:
字段顺序不影响索引完整使用
若是是
where a = xx order by b
这类语句, 那么直接就是联合索引 (a, b) 了.
此时应该按照字段的区分度, 区分度高的在前.
以索引 (status, product_id) 为例, 分别查看其区分度:
SELECT COUNT(DISTINCT status)/COUNT(*) as status_disc, COUNT(DISTINCT product_id)/COUNT(*) as product_id_disc FROM 表名;
当前这个例子很清楚, status 就几种取值, 基数很小, 区分度不好, 所以应该创建联合索引 (product_id, status)
通常有如下几种选择:
最耗费空间
适用: 前N个字符具备足够区分度的状况.
缺点: 增长额外扫描行数, 同时没法使用覆盖索引.
适用: 字符串前N个字符区分度不够的状况下, 且后N个字符有足够区分度
存储: 存储的时候直接存储倒序的字符串
使用: update 表 set s = reverse("123456");
缺点: 除了前缀索引的缺点外, 每次更新/查找都须要额外的 reverse 函数调用消耗, 同时没法利用索引进行范围查找.
存储: 新增额外字段存储字符串对应的hash值, 若使用 crc32 函数, 则额外占用4个字节
优势: 查找性能稳定, 基本在 O(1)
使用: 因为hash值会冲突, 所以查找时除了hash字段判断外, 还要判断原始字符串是否一致. select * from 表 where s_hash = crc32("123456") and s = "123456";
缺点: 占用额外的存储空间, 没法利用索引进行范围查找
CREATE TABLE 时建立
CREATE TABLE IF NOT EXISTS `users` ( -- 省略字段定义 PRIMARY KEY (`id`), UNIQUE KEY `users_phone` (`phone`), KEY `users_name` (`name`), ) Engine=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
ALTER TABLE用来建立普通索引、UNIQUE索引或PRIMARY KEY索引。
ALTER TABLE table_name ADD INDEX index_name (column_list) -- 可忽略索引名 -- ALTER TABLE table_name ADD INDEX (column_list) ALTER TABLE table_name ADD UNIQUE (column_list) ALTER TABLE table_name ADD PRIMARY KEY (column_list) -- 一个语句建多个索引 ALTER TABLE HeadOfState ADD PRIMARY KEY (ID), ADD INDEX (LastName,FirstName);
其中table_name是要增长索引的表名,column_list指出对哪些列进行索引,多列时各列之间用逗号分隔。索引名index_name可选,缺省时,MySQL将根据第一个索引列赋一个名称。另外,ALTER TABLE容许在单个语句中更改多个表,所以能够在同时建立多个索引。
CREATE INDEX可对表增长普通索引或UNIQUE索引。
CREATE INDEX index_name ON table_name (column_list) CREATE UNIQUE INDEX index_name ON table_name (column_list)
索引失效的状况我的认为主要是如下状况:
对索引字段使用函数、进行计算、类型转换
WHERE a + 1 = 2
这种语句也会致使索引 a 失效, 此时应该改写 SQL 语句为:WHERE a = 1
包括显式转换及隐式转换
若是字段 phone 是 char 类型, 那么WHERE phone = 18612345678
一样可能会致使索引失效, 应该改写成WHERE phone = '18612345678
'
包括联合索引和字符串最左前缀
索引列存在NULL且查询条件是 is not null
, 若索引没有覆盖查询字段和查询条件时, 此时会符合如下的<u>状况6</u>, 致使全表扫描.
如下是我的测试
-- UserName 是 varchar, nullable explain select Uid from new_light_user where UserName is null; -- SIMPLE new_light_user ref UserName UserName 768 const 10 Using where; Using index explain select * from new_light_user where UserName is not null; -- SIMPLE new_light_user ALL UserName null null null 17979 Using where
索引字段要定义为 NOT NULL, 不然:
IS NULL
, IS NOT NULL
除非查询值的筛选力度很高, 好比 status = 0
(表示未完成), 由于大多数值是 1, 所以这种状况下建索引仍是有意义的.
本文内容主要是 《MySQL实战45讲》 课程中第 20,21,30 课程的我的笔记及相关理解.
主要是对于加锁规则的理解及分析.
如下仅针对 MySQL 的 InnoDB 引擎.
MyISM 引擎就只有表锁
MySQL 中的锁主要分为:
全局锁
flush tables with read lock;
表级锁
表锁
lock table 表名 read; lock table 表名 write;
元数据锁(Meta Data Lock, MDL)
在 MySQL 5.5 引入 MDL 锁.MySQL 5.6 之后支持 OnlineDDL
还有个自增锁, 后续补充.意向锁在此先不作讨论.
MDL支持的版本:
MDL锁目的: 控制对表元数据修改的并发.
MDL锁类型分为:
普通的增删改查会自动获取MDL读锁, 而对表的字段修改或建立索引等修改表元数据的操做会自动获取MDL写锁, 在此期间增删改查就会被阻塞掉.
OnlineDDL 是一种近似不锁表的特性, 它的过程以下:
这个期间会阻塞
大部分时间消耗在这里, 好比重建表(alter table 表 Engine=Innodb
)时, 须要将数据从旧表按主键顺序逐一添加到新表, 而大部分时间就消耗在这里.
同时在此期间, 全部对数据库的增删改操做都会记录在特定日志中, 待这部分执行完毕后再应用这些日志, 从而保证数据一致性.
这个期间会也阻塞
也就是说 OnlineDDL 其实仍是会锁表, 但只会在开始跟结束的时候锁, 中间大部分时间是不锁的.
对于
ALTER TABLE 表名 Engine=Innodb
这种DDL操做:
- 5.6以前是在Server层面上经过建立临时表来实现的(锁表+建立临时表+拷贝数据+替换表)
- 5.7及以后的OnlineDDL是在InnoDB层面上处理的, 它会建立临时文件.
部分DDL操做不支持OnlineDDL, 好比添加全文索引(FULLTEXT)和空间索引(SPATIAL)
行锁也叫作记录锁, 这个锁是加在具体的索引项上的.
行锁分为两种:
行锁冲突状况:
须要明确:
记录之间是存在间隙的, 这个间隙也是能够加上锁实体, 称为间隙锁.
间隙锁存在的目的: 解决幻读问题.
间隙锁冲突状况:
须要明确:
间隙锁的概念是动态的
对间隙(a,b)加锁后, 存在间隙锁 (a,b).此时若 a 不存在(删除), 则间隙锁会向左延伸直到找到一条记录.
若b不存在了(删除), 则间隙锁会向右延伸直到找到一条记录.
假设主键上存在记录 id=5 和 id=10 和 id=15 的3条记录, 当存在某个间隙锁 (10,15) 时, 若咱们将 id=10 这一行删掉, 则间隙锁 (10, 15) 会动态扩展成 (5, 15), 此时想要插入 id=7 的记录会被阻塞住.
此处的删除指的是事务提交后, 不然间隙锁依旧是 (10,15)
next-key lock = 行锁 + 间隙锁
next-key lock 的加锁顺序:
若是加完间隙锁后, 再加行锁时被阻塞进入锁等待时, 间隙锁在此期间是不会释放的.
两阶段锁协议指的是:
了解这个协议的启发在于:
索引搜索指的是就是:
order by desc
就是用最大的值来找第一个
order by
就是用最小的值来找第一个
等值查询指的是:
在索引树上利用树搜索快速定位 xx=yy
的过程
where xx > yy
时, 也是先找到xx = yy
这条记录, 这一个步骤是等值查询.但后续的向右遍历则属于范围查询.
xx=yy
向右遍历的过程.该部分源自《MySQL实战45讲》中的 《21-为何我只改了一行的语句, 锁这么多》
如下仅针对 MySQL 的 InnoDB 引擎在 可重复读隔离级别, 具体MySQL版本:
如下测试若未指定, 则默认使用如下表, 相关案例为了不污染原始数据, 所以在不影响测试结果前提下, 都放在事务中执行, 且最终不提交.
create table c20( id int not null primary key, c int default null, d int default null, key `c`(`c`) ) Engine=InnoDB; insert into c20 values(0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25); /* +----+------+------+ | id | c | d | +----+------+------+ | 0 | 0 | 0 | | 5 | 5 | 5 | | 10 | 10 | 10 | | 15 | 15 | 15 | | 20 | 20 | 20 | | 25 | 25 | 25 | +----+------+------+ */
原则2: 访问到的对象才会加锁
select id from t where c = 15 lock in share mode;
加读锁时, 覆盖索引优化状况下, 不会访问主键索引, 所以若是要经过
lock in share mode
给行加锁避免数据被修改, 那就须要绕过索引优化, 如 select 一个不在索引中的值.但若是改为
for update
, 则 mysql 认为接下来会更新数据, 所以会将对应主键索引也一块儿锁了
优化1: 索引上的等值查询, 对惟一索引加锁时, next-key lock 会退化为行锁
select * from t where id = 10 for update;
引擎会在主键索引上查找到 id=10 这一行, 这一个操做是等值查询.
锁范围是
优化2: 索引上的等值查询, 向右遍历时且最后一个值不知足等值条件时, next-key Lock 会退化为间隙锁
select * from t where c = 10 for update;
因为索引c是普通索引, 引擎在找到 c=10 这一条索引项后继续向右遍历到 c=15 这一条, 此时锁范围是 (5, 10], (10, 15)
BUG 1: 惟一索引上的范围查询会访问到不知足条件的第一个值
id> 10 and id <=15, 这时候会访问 id=15 以及下一个记录.
对索引上的更新操做, 本质上是 删除+插入
读提交下有一个针对 update 语句的 "semi-consistent" read 优化.
若是 update 语句碰到一个已经被锁了的行, 会读入最新的版本, 而后判断是否是知足查询条件, 若知足则进入锁等待, 若不知足则直接跳过.注意这个策略对 delete 是无效的.
https://time.geekbang.org/col...
在可重复读隔离级别, binlog_format = statement 时, 该语句会对被 select 的那个表访问到的记录和间隙加锁
小伙子, 很危险的.
生产环境大表复制数据通常用 pt-archiver 工具来处理, 避免 insert ... select ... 锁致使的长阻塞.
pt-archiver: 数据归档工具
或者简单用 select ... into outfile 和 load data infile 组合来代替 insert ... select 完成插入操做.
例子1
begin; select * from c20 where id=5 for update;
在主键索引 id 上快速查找到 id=5 这一行是等值查询
例子2
begin; select * from c20 where id > 9 and id < 12 for update;
在主键索引 id 上找到首个大于 9 的值, 这个过程实际上是在索引树上快速找到 id=9 这条记录(不存在), 找到了 (5,10) 这个间隙, 这个过程是等值查询.
而后向右遍历, 在遍历过程当中就不是等值查询了, 依次扫描到 id=10 , id=15 这两个记录, 其中 id=15 不符合条件, 所以最终锁范围是 (5,10], (10, 15]
例子3
begin; select * from c20 where id > 9 and id < 12 order by id desc for update;
根据语义 order by id desc
, 优化器必须先找到第一个 id < 12 的值, 在主键索引树上快速查找 id=12 的值(不存在), 此时是向右遍历到 id=15, 根据优化2, 仅加了间隙锁 (10,15) , 这个过程是等值查询.
接着向左遍历, 遍历过程就不是等值查询了, 最终锁范围是: (0,5], (5, 10], (10, 15)
我的理解:
例子4
begin; select * from c20 where c>=15 and c<=20 order by c desc lock in share mode;
执行过程:
我的理解:
例子5
begin; select * from c20 where c<=20 order by c desc lock in share mode;
这里留意一下 , 加锁范围并非 (20, 25], (15, 20], (10,15], (5,10], (0, 5], (-∞, 5], 而是
...........
..........
.........
........
.......
......
.....
......
.......
........
.........
..........
...........
全部行锁+间隙锁.
具体为何, 其实只要 explain 看一下就明白了.
+------+-------------+-------+------+---------------+------+---------+------+------+-----------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +------+-------------+-------+------+---------------+------+---------+------+------+-----------------------------+ | 1 | SIMPLE | c20 | ALL | c | NULL | NULL | NULL | 14 | Using where; Using filesort | +------+-------------+-------+------+---------------+------+---------+------+------+-----------------------------+
但若是是 c<=19, 则会使用索引 c, 这说明 innodb 引擎有本身一套规则用于"估算"当前使用二级索引仍是主键索引哪一个开销会更小.
explain select * from c20 where c<=19 order by c desc lock in share mode; +------+-------------+-------+-------+---------------+------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +------+-------------+-------+-------+---------------+------+---------+------+------+-------------+ | 1 | SIMPLE | c20 | range | c | c | 5 | NULL | 4 | Using where | +------+-------------+-------+-------+---------------+------+---------+------+------+-------------+
例子6
begin; select * from c20 where c>=10 and c<15 for update;
加锁范围是
索引 c 的 (5,10], (10,15]
这里对索引 c 的 15 好像是退化成行锁了, 不是很理解.
主键索引的 [10]
访问到的才会加锁, 因为没有访问主键 id=15, 所以不会对齐加锁.
例子7 - 我的不理解的地方
-- T1 事务A begin; select * from c20 where id>=15 and id<=20 order by id desc lock in share mode; -- T2 事务B begin; update c20 set d=d+1 where id=25; -- OK insert into c20 values(21,21,21); -- 阻塞 -- T3 事务A 人为制造死锁, 方便查看锁状态 update c20 set d=d+1 where id=25; -- OK /* 此时 事务B 提示: ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction */
我的不理解的:
根据order by id desc
, T1 时刻事务A首先在主键索引上搜索 id=20 这一行, 正常来讲主键索引上 id=20 的只有一行, 不必向右遍历.
加锁范围:
mysql> show engine innodb status ------------------------ LATEST DETECTED DEADLOCK ------------------------ 2019-09-27 10:34:29 0xe2e8 *** (1) TRANSACTION: TRANSACTION 1645, ACTIVE 100 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 3 lock struct(s), heap size 1080, 4 row lock(s), undo log entries 1 MySQL thread id 82, OS thread handle 77904, query id 61115 localhost ::1 root update insert into c20 values(21,21,21) *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 23 page no 3 n bits 80 index PRIMARY of table `test_yjx`.`c20` trx id 1645 lock_mode X locks gap before rec insert intention waiting Record lock, heap no 7 PHYSICAL RECORD: n_fields 5; compact format; info bits 0 0: len 4; hex 80000019; asc ;; 1: len 6; hex 00000000066d; asc m;; 2: len 7; hex 6e0000019a0110; asc n ;; 3: len 4; hex 80000019; asc ;; 4: len 4; hex 8000001a; asc ;; *** (2) TRANSACTION: TRANSACTION 1646, ACTIVE 271 sec starting index read mysql tables in use 1, locked 1 5 lock struct(s), heap size 1080, 5 row lock(s) MySQL thread id 81, OS thread handle 58088, query id 61120 localhost ::1 root updating update c20 set d=d+1 where id=25 *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 23 page no 3 n bits 80 index PRIMARY of table `test_yjx`.`c20` trx id 1646 lock mode S locks gap before rec Record lock, heap no 7 PHYSICAL RECORD: n_fields 5; compact format; info bits 0 0: len 4; hex 80000019; asc ;; 1: len 6; hex 00000000066d; asc m;; 2: len 7; hex 6e0000019a0110; asc n ;; 3: len 4; hex 80000019; asc ;; 4: len 4; hex 8000001a; asc ;; *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 23 page no 3 n bits 80 index PRIMARY of table `test_yjx`.`c20` trx id 1646 lock_mode X locks rec but not gap waiting Record lock, heap no 7 PHYSICAL RECORD: n_fields 5; compact format; info bits 0 0: len 4; hex 80000019; asc ;; 1: len 6; hex 00000000066d; asc m;; 2: len 7; hex 6e0000019a0110; asc n ;; 3: len 4; hex 80000019; asc ;; 4: len 4; hex 8000001a; asc ;; *** WE ROLL BACK TRANSACTION (1)
上述的:
- (1) TRANSACTION(事务1) 指的是事务B
- (2) TRANSACTION(事务2) 指的是事务A
注意与上面的 事务A, 事务B 顺序是相反了, 别看错了.
分析:
(1) TRANSACTION
insert into c20 values(21,21,21)
最后一句执行语句(1) WAITING FOR THIS LOCK TO BE GRANTED
index PRIMARY of table test_yjx.c20
说明在等表 c20
主键索引上的锁lock_mode X locks gap before rec insert intention waiting
说明在插入一条记录, 试图插入一个意向锁, 与间隙锁产生冲突了 0: len 4; hex 80000019; asc ;;
冲突的间隙锁: 16进制的 19
, 即 10进制的 id=25 左边的间隙.(2) TRANSACTION
事务2信息
update c20 set d=d+1 where id=25
最后一句执行语句(2) HOLDS THE LOCK(S)
事务2持有锁的信息
index PRIMARY of table test_yjx.c20
说明持有c20表主键索引上的锁lock mode S locks gap before rec
说明只有间隙锁0: len 4; hex 80000019; asc ;;
间隙锁: id=25 左边的间隙(2) WAITING FOR THIS LOCK TO BE GRANTED:
事务2正在等待的锁
index PRIMARY of table test_yjx.c20
说明在等待 c20 表主键索引上的锁lock_mode X locks rec but not gap waiting
须要对行加写锁0: len 4; hex 80000019; asc ;;
等待给 id=25 加行锁(写)WE ROLL BACK TRANSACTION (1)
表示回滚了事务1我的猜想实际状况是:
没法证明本身的猜想. 已在课程21和课程30留下如下留言, 等待解答(或者无人解答). 2019年9月27日
-- T1 事务A begin; select * from c20 where id>=15 and id<=20 order by id desc lock in share mode; -- T2 事务B begin; update c20 set d=d+1 where id=25; -- OK insert into c20 values(21,21,21); -- 阻塞不能理解, 为何事务A执行的语句会给 间隙(20,25) 加上锁.
经过 show engine innodb status; 查看发现事务A确实持有上述间隙锁.
经过 explain select * from c20 where id>=15 and id<=20 order by id desc lock in share mode; 查看 Extra 也没有 filesort, key=PRIMARY, 所以我的认为是按照主键索引向左遍历获得结果.按照个人理解, 因为
order by id desc
, 所以首先是在主键索引上搜索 id=20, 同时因为主键索引上这个值是惟一的, 所以没必要向右遍历. 然而事实上它确实这么作了, 这让我想到了 BUG1: 主键索引上的范围查询会遍历到不知足条件的第一个.
可是这一步的搜索过程应该是等值查询才对, 彻底一脸懵住了...
不知道老师如今还能看到这条评论不?
-- T1 事务A begin; update c20 set d=d+1 where id=7; /* 1. 在主键索引上不存在id=7记录, 根据规则1: 加锁基本单位是 next-key lock, 所以加锁范围是(5,10] 2. 因为id=7是一个等值查询, 根据优化2, id=10不知足条件, 所以锁退化为间隙锁 (5,10) */ -- T2 事务B begin; insert into c20 values(8,8,8); -- 阻塞 update c20 set d=d+1 where id=10; -- OK
对应课程的案例一
-- T1 事务A begin; update c20 set d=d+1 where c=7; /* 分析 1. 加锁基本单位是next-key lock, 加锁范围就是 (5,10] -- 此时只是分析过程, 并不是加锁过程 2. 根据优化2, 索引上的等值查询(c=7)向右遍历且最后一个值不知足条件时, next-key lock 退化为间隙锁, 加锁范围变为 (5, 10) 3. 因为是在索引c上查询, 所以加锁范围其实是索引 c 上的 ((5,5), (10,10)) , 格式 (c, id) */ -- T2 事务B begin; insert into c20 values(4,5,4); -- OK insert into c20 values(6,5,4); -- 被间隙锁堵住 insert into c20 values(9,10,9); -- 被间隙锁堵住 insert into c20 values(11,10,9); -- OK
关注重点: 覆盖索引优化致使无需回表的状况对主键索引影响
-- T1 事务A begin; select id from c20 where c = 5 lock in share mode; -- 索引c是普通索引, 所以会扫描到 c=10 这一行, 所以加锁范围是 (0,5], (5,10) -- 同时因为优化2: 索引上的等值查询向右遍历且最后一个值不知足条件时next-key lock退化为间隙锁, 即加锁范围实际是 (0,5], (5,10) -- 注意, 该条查询因为只 select id, 实际只访问了索引c, 并无访问到主键索引, 根据规则2: 访问到的对象才会加锁, 所以最终只对索引c 的范围 (0,5], (5,10) 加锁 -- T2 事务B begin; update c20 set d=d+1 where id=5; -- OK, 由于覆盖索引优化致使并无给主键索引上加锁 insert into c20 values(7,7,7);
对应课程的案例二
注意, 上面是使用 lock in share mode
加读锁, 所以会被覆盖索引优化.
若是使用 for update
, mysql认为你接下来要更新行, 所以也会锁上对应的主键索引.
关注重点在于: 普通索引上的范围查询时对不符合条件的索引加锁时, 是否会对对应的主键索引产生影响.
-- T1 事务A begin; select * from c20 where c>=10 and c<11 for update; /* 1. 首先查找到 c=10 这一行, 锁范围 (5,10] 2. 接着向右遍历(这时候不是等值查询, 是遍历查询), 找到 c=15 这一行, 不符合条件, 查询结束. 根据规则2: 只有访问到的对象才会加锁, 因为不须要访问c=15对应的主键索引项, 所以这里的锁范围是索引c上的 (5,10], (10,15], 以及主键上的行锁[10] */ -- T2 事务B begin; select * from c20 where c=15 for update; -- 阻塞 select * from c20 where id=15 for update; -- OK
加锁范围
索引 c
主键
-- T1 事务A begin; select * from c20 where id>=10 and id<11 for update; /* 1. 首先在主键索引上查找 id=10 这一行, 根据优化1: 索引上的等值查询在对惟一索引加锁时, next-key lock 退化为行锁, 此时加锁范围是 [10] 2. 继续向右遍历到下一个 id=15 的行, 此时并不是等值查询, 所以加锁范围是 [10], (10,15] */ -- T2 事务B begin; insert into c20 values(8,8,8); -- OK insert into c20 values(13,13,13); -- 阻塞 update c20 set d=d+1 where id=15; -- 阻塞
对应课程案例三
这里要注意, 事务A首次定位查找id=10这一行的时候是等值查询, 然后续向右扫描到id=15的时候是范围查询判断.
主键索引的加锁范围
-- T1 事务A begin; select * from c20 where c >= 10 and c < 11 for update; /* 1. 首先在索引c上找到 c=10 这一行, 加上锁 (5,10] 2. 向右遍历找到 c=15 这一行, 不知足条件, 最终加锁范围是 索引c上的 (5,10], (10,15], 及主键索引 [5] */ -- T2 事务B begin; insert into c20 values(8,8,8); -- 阻塞 update c20 set d=d+1 where c=15; -- 阻塞 update c20 set d=d+1 where id=15; -- 阻塞
对应课程案例四
主键的加锁范围
-- T1 事务A begin; select * from c20 where id>10 and id<=15 for update -- T2 事务B begin; update c20 set d=d+1 where id=20; -- 阻塞 insert into c20 values(16,16,16); -- 阻塞
顺便提一下:
begin; select * from c20 where id>10 and id<15 for update; /* 1. 在主键索引上找到id=15这一行, 不知足条件, 根据原则1, 加锁 (10,15] */对应课程案例五
主键的加锁范围
-- T1 事务A begin; insert into c20 values(30,10,30); commit; /* 在索引c上, 此时有两行 c=10 的行 因为二级索引上保存着主键的值, 所以并不会有两行彻底一致的行, 以下: c 0 5 10 10 15 20 25 id 0 5 10 30 15 20 25 此时两个 (c=10, id=10) 和 (c=10, id=30) 之间也是存在间隙的 */ -- T2 事务B begin; delete from c20 where c=10; /* 1. 首先找到索引c上 (c=10, id=10) 这一行, 加锁 (5,10] 2. 向右遍历, 找到 (c=10, id=30) 这一行, 加锁 ( (c=10,id=10), (c=10,id=30) ] 3. 向右遍历, 找到 c=20 这一行, 根据优化2, 索引上的等值查询向右遍历且最后一个值不匹配时, next-key lock 退化为间隙锁, 即加锁 (10,15) 4. 总的加锁范围是 (5,10], ( (c=10,id=10), (c=10,id=30) ], (10,15] */ -- T3 事务C begin; insert into c20 values(12,12,12); -- 阻塞 update c20 set d=d+1 where c=15; -- OK -- T4 扫尾, 无视 delete from c20 where id=30;
对应课程案例六
delete 的加锁逻辑跟 select ... for update
是相似的.
事务 B 对索引 c 的加锁范围
-- T0 初始环境 insert into c20 values(30,10,30); -- T1 事务A begin; delete from c20 where c=10 limit 2; /* 1. 找到 c=10 的第一条, 加锁 (5,10] 2. 向右遍历, 找到 c=10,id=30 的记录, 加锁 ( (c=10,id=10), (c=10,id=30) ], 此时知足 limit 2 */ -- T2, 事务B begin; insert into c20 values(12,12,12); -- OK
若是不加 limit 2
则会继续向右遍历找到 c=15 的记录, 新增长锁范围 (10,15)
对应课程案例七
指导意义:
-- T1 事务A begin; select id from c20 where c=10 lock in share mode; /* 1. 在索引c上找到 c=10 这一行, 因为覆盖索引的优化, 没有回表, 所以只会在索引c上加锁 (5,10] 2. 向右遍历, 找到 c=15, 不知足, 根据优化2, 加锁范围退化为 (10,15) 3. 总的加锁范围是在索引c上的 (5,10], (10,15) */ -- T2 事务B begin; update c20 set d=d+1 where c=10; -- 阻塞 /* 1. 找到 c=10 这一行, 试图加上锁 (5,10], 按照顺序先加上间隙锁(5,10), 因为间隙锁之间不冲突, OK. 以后再加上 [10] 的行锁, 但被T1时刻的事务A阻塞了, 进入锁等待 */ -- T3 事务A insert into t values(8,8,8); -- OK, 但形成 事务B 回滚 /* 往 (5,10) 这个间隙插入行, 此时与 T2时刻事务B 加的间隙锁产生冲突. 同时因为 事务B 也在等待 T1时刻事务A 加的行锁, 两个事务间存在循环资源依赖, 形成死锁. 此时事务B被回滚了, 报错以下: ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction */
对应课程案例八
-- T1 事务A begin; select * from c20 where c>=15 and c<=20 order by c desc lock in share mode; /* 1. 在索引c上找到 c=20 这一行, 加锁 (15,20] 2. 向左遍历, 找到 c=15 这一行, 加锁 (10,15] 3. 继续向左遍历, 找到 c=10 这一行, 因为不知足优化条件, 所以直接加锁 (5,10], 不知足查询条件, 中止遍历. 4. 最终加锁范围是 (5,10], (10,15], (15, 20] */ -- T2 事务B insert into c20 values(6,6,6); -- 阻塞
对应课程的上期答疑
索引 c 的加锁范围
-- 表结构 create table t(a int not null, b int default null)Engine=Innodb; insert into t values(1,1),(2,2),(3,3),(4,4),(5,5); -- T1 事务A set session transaction isolation level read committed; begin; update t set a=6 where b=1; /* b没有索引, 所以全表扫描, 对主键索引上全部行加上行锁 */ -- T2 事务B set session transaction isolation level read committed; begin; update t set a=7 where b=2; -- OK /* 在读提交隔离级别下, 若是 update 语句碰到一个已经被锁了的行, 会读入最新的版本, 而后判断是否是知足查询条件, 若知足则进入锁等待, 若不知足则直接跳过. */ delete from t where b=3; -- 阻塞 /* 注意这个策略对 delete 是无效的, 所以delete语句被阻塞 */
对应课程评论下方 @时隐时现 2019-01-30 的留言
-- T1 事务A begin; select * from c20 where id>10 and id<=15 for update; /* 加锁 (10,15], (15, 20]??? */ -- T2 事务B 注意此处没加 begin, 是立刻执行并提交的单个事务. delete from c20 where id=10; -- OK /* 事务A在T1时刻加的间隙锁 (10,15) 此时动态扩展成 (5,15) */ -- T3 事务C insert into c20 values(10,10,10); -- 阻塞 /* 被新的间隙锁堵住了 */
对应课程评论下方 @Geek_9ca34e 2019-01-09 的留言
若是将上方的 T2时刻的事务B 和 T3时刻的事务C 合并在一个事务里, 则不会出现这种状况.
我的理解是, 事务未提交时, 期间删除/修改的数据仅仅是标记删除/修改, 此时记录还在, 所以间隙锁范围不变.
只有在事务提价后才会进行实际的删除/修改, 所以间隙锁才"会动态扩大范围"
-- T1 事务A begin; select c from c20 where c>5 lock in share mode; /* 找到 c=5, 不知足, 向右遍历找到 c=10, 加锁 (5,10], 继续遍历, 继续加锁... */ -- T2 事务B update c20 set c=1 where c=5; -- OK /* 删除了 c=5 这一行, 致使 T1时刻事务A 加的间隙锁 (5,10) 变为 (1,10) */ -- T3 事务C update c20 set c=5 where c=1; -- 阻塞 /* 将 update 理解为两步: 1. 插入 (c=5, id=5) 这个记录 -- 被间隙锁阻塞 2. 删除 (c=1, id=5) 这个记录 */
begin; select id from c20 where c in (5,20,10) lock in share mode;
经过 explain 分析语句:
mysql> explain select id from c20 where c in (5,20,10) lock in share mode; +----+-------------+-------+-------+---------------+------+---------+------+------+--------------------- | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra +----+-------------+-------+------------+-------+---------------+------+---------+------+------+--------- | 1 | SIMPLE | c20 | range | c | c | 5 | NULL | 3 | Using where; Using index | +----+-------------+-------+------------+-------+---------------+------+---------+------+------+--------- 1 row in set, 1 warning (0.00 sec)
显示结果太长, 所以将 partitions, filtered 列删除了
结果分析:
语句分析:
注意上述锁是一个个逐步加上去的, 而非一次性所有加上去.
考虑如下语句:
begin; select id from c20 where c in (5,20,10) order by id desc for update;
根据语义 order by id desc
, 会依次查找 c=20, c=10, c=5.
因为加锁顺序相反, 所以若是这两个语句并发执行的时候就有可能发生死锁.
show engine innodb status;
查看 LATEST DETECTED DEADLOCK 这一节, 记录了最后一次死锁信息.
示例
------------------------ LATEST DETECTED DEADLOCK ------------------------ 2019-09-24 16:24:18 0x5484 *** (1) TRANSACTION: TRANSACTION 1400, ACTIVE 191 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 2 lock struct(s), heap size 1080, 3 row lock(s) MySQL thread id 54, OS thread handle 74124, query id 36912 localhost ::1 root updating update c20 set d=d+1 where c=10 *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 23 page no 4 n bits 80 index c of table `test_yjx`.`c20` trx id 1400 lock_mode X waiting Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 4; hex 8000000a; asc ;; 1: len 4; hex 8000000a; asc ;; *** (2) TRANSACTION: TRANSACTION 1401, ACTIVE 196 sec inserting mysql tables in use 1, locked 1 5 lock struct(s), heap size 1080, 3 row lock(s), undo log entries 1 MySQL thread id 53, OS thread handle 21636, query id 36916 localhost ::1 root update insert into c20 values(8,8,8) *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 23 page no 4 n bits 80 index c of table `test_yjx`.`c20` trx id 1401 lock mode S Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 4; hex 8000000a; asc ;; 1: len 4; hex 8000000a; asc ;; *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 23 page no 4 n bits 80 index c of table `test_yjx`.`c20` trx id 1401 lock_mode X locks gap before rec insert intention waiting Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 4; hex 8000000a; asc ;; 1: len 4; hex 8000000a; asc ;; *** WE ROLL BACK TRANSACTION (1)
结果分为3个部分:
(1) TRANSACTION 第一个事务的信息
WAITING FOR THIS LOCK TO BE GRANTED
, 表示这个事务在等待的锁资源(2) TRANSACTION 第二个事务的信息
HOLDS THE LOCK(S)
显示该事务持有哪些锁第一个事务的信息中:
update c20 set d=d+1 where c=10
致使死锁时执行的最后一条 sql 语句WAITING FOR THIS LOCK TO BE GRANTED
index c of table test_yjx.c20
, 说明在等的是表 c20 的索引 c 上面的锁lock_mode X waiting
表示这个语句要本身加一个写锁, 当前状态是等待中.Record lock
说明这是一个记录锁n_fields 2
表示这个记录是两列, 即 字段c 和 主键字段 id0: len 4; hex 8000000a; asc ;;
是第一个字段(即字段c), 值(忽略里面的8)是十六进制 a, 即 10
值 8000000a 中的 8...我也不理解为何, 先忽略
1: len 4; hex 8000000a; asc ;;
是第二个字段(即字段id), 值是 10上面两行里的 asc 表示, 接下来要打印出值里面的"可打印字符", 但10不是可打印字符, 所以就显示空格
这里不太理解
第二个事务的信息中:
insert into c20 values(8,8,8)
致使死锁时最后执行的语句HOLDS THE LOCK(S)
index c of table test_yjx.c20 trx id 1401 lock mode S
表示锁是在表 c20 的索引 c 上, 加的是读锁hex 8000000a;
表示这个事务持有 c=10 这个记录锁WAITING FOR THIS LOCK TO BE GRANTED
index c of table test_yjx.c20 trx id 1401 lock_mode X locks gap before rec insert intention waiting
insert intention
表示试图插入一个记录, 这是一个插入意向锁, 与间隙锁产生锁冲突gap before rec
表示这是一个间隙锁, 而不是记录锁.补充:
lock_mode X waiting
表示 next-key locklock_mode X locks rec but not gap
表示只有行锁locks gap before rec
就是只有间隙锁
从上面信息能够知道:
第一个事务
第二个事务
案例
-- 前提: 表 T 上有普通索引 k -- 语句1 select * from T where k in (1,2,3,4,5); -- 语句2 select * from T where k between 1 and 5;
这两条语句的区别是:
语句1: 在索引k上进行了5次树查找
语句2: 在索引k上进行了1次树查找(k=1), 以后向右遍历直到id>5
很明显, 语句2 性能会更好.
文章连接: https://segmentfault.com/a/11...
本部分主要来自: 极客时间《MySQL实战45讲》的第12讲 - 为何个人MySQL会“抖”一下
WAL 是预写式日志, 关键点在于先写日志再写磁盘.
在对数据页进行修改时, 经过将"修改了什么"这个操做记录在日志中, 而没必要立刻将更改内容刷新到磁盘上, 从而将随机写转换为顺序写, 提升了性能.
但由此带来的问题是, 内存中的数据页会和磁盘上的数据页内容不一致, 此时将内存中的这种数据页称为 脏页
这里的日志指的是Redo Log(重作日志), 这个日志是循环写入的.
它记录的是在某个数据页上作了什么修改, 这个日志会携带一个LSN, 同时每一个数据页上也会记录一个LSN(日志序列号).
这个日志序列号(LSN)能够用于数据页是不是脏页的判断, 好比说 write pos对应的LSN比某个数据页的LSN大, 则这个数据页确定是干净页, 同时当脏页提早刷到磁盘时, 在应用Redo Log能够识别是否刷过并跳过.
这里有两个关键位置点:
当内存数据页和磁盘数据页内容不一致的时候, 将内存页称为"脏页".
内存数据页写入磁盘后, 两边内容一致, 此时称为"干净页".
将内存数据页写入磁盘的这个操做叫作"刷脏页"(flush).
InnoDB是以缓冲池(Buffer Pool)来管理内存的, 缓冲池中的内存页有3种状态:
因为InnoDB的策略一般是尽可能使用内存, 所以长时间运行的数据库中的内存页基本都是被使用的, 未被使用的内存页不多.
刷脏页的时机:
checkpoint 向前推动时, 须要将推动区间涉及的全部脏页刷新到磁盘.
此时若是是干净页, 则直接拿来复用.
若是是脏页, 则须要先刷新到磁盘(直接写入磁盘, 不用管Redo Log, 后续Redo Log刷脏页时会判断对应数据页是否已刷新到磁盘), 使之成为干净页再拿来使用.
固然平时忙的时候也会尽可能刷脏页.
此时须要将全部脏页刷新到磁盘.
InnoDB须要控制脏页比例来避免Redo Log写满以及单次淘汰过多脏页过多的状况.
这种状况尽可能避免, 所以此时系统就不接受更新, 全部更新语句都会被堵住, 此时更新数为0.
对于敏感业务来讲, 这是不能接受的.
此时须要将 write pos 向前推动, 推动范围内Redo Log涉及的全部脏页都须要flush到磁盘中.
Redo Log设置太小或写太慢的问题: 此时因为Redo Log频繁写满, 会致使频繁触发flush脏页, 影响tps.
这种状况实际上是常态.
当从磁盘读取的数据页在内存中没有内存时, 就须要到缓冲池中申请一个内存页, 这时候根据LRU(最近最少使用算法)就须要淘汰掉一个内存页来使用.
此时淘汰的是脏页, 则须要将脏页刷新到磁盘, 变成干净页后才能复用.
注意, 这个过程 Write Pos 位置是不会向前推动的.
当一个查询要淘汰的脏页数太多, 会致使查询的响应时间明显变长.
InnoDB 控制刷脏页的策略主要参考:
当脏页比例接近或超过参数 innodb_max_dirty_pages_pct
时, 则会全力, 不然按照百分比.
N = (write pos 位置的日志序号 - checkpoint对应序号), 当N越大, 则刷盘速度越快.
最终刷盘速度取上述二者中最快的.
innodb_io_capacity
InnoDB 有一个关键参数: innodb_io_capacity
, 该参数是用于告知InnoDB你的磁盘能力, 该值一般建议设置为磁盘的写IOPS.
该参数在 MySQL 5.5 及后续版本才能够调整.
测试磁盘的IOPS:
fio -filename=/data/tmp/test_randrw -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16k -size=500M -numjobs=10 -runtime=10 -group_reporting -name=mytest
注意, 上面的-filename
要指定具体的文件名, 千万不要指定分区, 不然会致使分区不可用, 须要从新格式化.
innodb_io_capacity
通常参考 写能力的IOPS
innodb_io_capacity
设置太低致使的性能问题案例:MySQL写入速度很慢, TPS很低, 可是数据库主机的IO压力并不大.
当innodb_io_capacity
设置太小时, InnoDB会认为磁盘性能差, 致使刷脏页很慢, 甚至比脏页生成速度还慢, 就会形成脏页累积, 影响查询和更新性能.
innodb_io_capacity
大小设置:
innodb_max_dirty_pages_pct
innodb_max_dirty_pages_pct
指的是脏页比例上限(默认值是75%), 内存中的脏页比例越是接近该值, 则InnoDB刷盘速度会越接近全力.
如何计算内存中的脏页比例:
show global status like 'Innodb_buffer_pool_pages%';
脏页比例 = 100 * Innodb_buffer_pool_pages_dirty / Innodb_buffer_pool_pages_total
的值
innodb_flush_neighbors
当刷脏页时, 若脏页旁边的数据页也是脏页, 则会连带刷新, 注意这个机制是会蔓延的.
当 innodb_flush_neighbors=1
时开启该机制, 默认是1, 但在 MySQL 8.0 中默认值是 0.
因为机械硬盘时代的IOPS通常只有几百, 该机制能够有效减小不少随机IO, 提升系统性能.
但在固态硬盘时代, 此时IOPS高达几千, 此时IOPS每每不是瓶颈, "只刷本身"能够更快执行完查询操做, 减小SQL语句的响应时间.
这里有一个案例:
测试在作压力测试时, 刚开始 insert, update 很快, 可是一会就变慢且响应延迟很高.
↑ 出现这种状况大部分是由于 Redo Log 设置过小引发的.
由于此时 Redo Log 写满后须要将 checkpoint 前推, 此时须要刷脏页, 可能还会连坐(innodb_flush_neighbors=1
), 数据库"抖"的频率变高.
其实此时内存的脏页比例可能还很低, 并无充分利用到大内存优点, 此时须要频繁flush, 性能会变差.
同时, 若是Redo Log中存在change buffer, 一样须要作相应的merge操做, 致使 change buffer 发挥不出做用.
对于实际场景:
在一台高性能机器上配置了很是小的Redo Log.
此时因为每次都很快写满Redo Log, 此时Write Pos会一直追着Check Point, 所以系统就会中止全部更新, 去推动 Check Point.
此时看到的现象就是: 磁盘压力很小, 可是数据库出现间歇性性能降低.
关键字:
原文: https://time.geekbang.org/col...
TODO