目录:/MySQL实战45讲 [161.8M]
┣━━01讲基础架构:一条SQL查询语句是如何执行的.html [54.2K]
┣━━02讲日志系统:一条SQL更新语句是如何执行的.html [65K]
┣━━03讲事务隔离:为什么你改了我还看不见.html [56.8K]
┣━━04讲深入浅出索引(上).html [59.9K]
┣━━05讲深入浅出索引(下).html [63.2K]
┣━━06讲全局锁和表锁:给表加个字段怎么有这么多阻碍.html [63.9K]
┣━━07讲行锁功过:怎么减少行锁对性能的影响.html [60.9K]
┣━━08讲事务到底是隔离的还是不隔离的.html [69K]
┣━━09讲普通索引和唯一索引,应该怎么选择.html [70K]
┣━━10讲MySQL为什么有时候会选错索引.html [67.1K]
┣━━11讲怎么给字符串字段加索引.html [54.6K]
┣━━12讲为什么我的MySQL会“抖”一下.html [62K]
┣━━13讲为什么表数据删掉一半,表文件大小不变.html [60.2K]
┣━━14讲count这么慢,我该怎么办.html [64.2K]
┣━━15讲答疑文章(一):日志和索引相关问题.html [73.4K]
┣━━16讲“orderby”是怎么工作的.html [74.3K]
┣━━17讲如何正确地显示随机消息.html [63.5K]
┣━━18讲为什么这些SQL语句逻辑相同,性能却差异巨大.html [62.1K]
┣━━19讲为什么我只查一行的语句,也执行这么慢.html [59.6K]
┣━━20讲幻读是什么,幻读有什么问题.html [67.2K]
┣━━21讲为什么我只改一行的语句,锁这么多.html [68.7K]
┣━━22讲MySQL有哪些“饮鸩止渴”提高性能的方法.html [75.2K]
┣━━23讲MySQL是怎么保证数据不丢的.html [65.4K]
┣━━24讲MySQL是怎么保证主备一致的.html [66.4K]
┣━━25讲MySQL是怎么保证高可用的.html [68.4K]
┣━━26讲备库为什么会延迟好几个小时.html [67.5K]
┣━━27讲主库出问题了,从库怎么办.html [63.3K]
┣━━28讲读写分离有哪些坑.html [67.1K]
┣━━29讲如何判断一个数据库是不是出问题了.html [49.4K]
┣━━30讲答疑文章(二):用动态的观点看加锁.html [38.9K]
┣━━31讲误删数据后除了跑路,还能怎么办.html [65.1K]
┣━━32讲为什么还有kill不掉的语句.html [59.4K]
┣━━33讲我查这么多数据,会不会把数据库内存打爆.html [61.1K]
┣━━34讲到底可不可以使用join.html [37.2K]
┣━━35讲join语句怎么优化.html [67.4K]
┣━━36讲为什么临时表可以重名.html [55.3K]
┣━━37讲什么时候会使用内部临时表.html [57.2K]
┣━━38讲都说InnoDB好,那还要不要使用Memory引擎.html [52.1K]
┣━━39讲自增主键为什么不是连续的.html [50.6K]
┣━━40讲insert语句的锁为什么这么多.html [53.8K]
┣━━41讲怎么最快地复制一张表.html [34K]
┣━━42讲grant之后要跟着flushprivileges吗.html [51.4K]
┣━━43讲要不要使用分区表.html [51.2K]
┣━━44讲答疑文章(三):说一说这些好问题.html [53.1K]
┣━━开篇词讲这一次,让我们一起来搞懂MySQL.html [40.6K]
┗━━直播回顾讲林晓斌:我的MySQL心路历程.html [51.7K]
1. 一条语句的执行过程
以查询语句为例,一条查询语句要经过以下几个步骤:
- 连接器。首先会在 Server 端验证连接的正确性,判断用户名密码是否正确,进行连接管理;
- 查询缓存。这个在 Mysql 8.0 之后就没有了,因为缓存命中率一向很低,只要修改了表就会导致缓存的失效;
- 分析器。主要进行词法语法的分析,判断语句是否符合规范,语句中的表以及字段在表中是否正确;
- 优化器。对查询语句进行优化,主要是索引的选择;
- 执行器。这一步才真正开始打开表来操作,需要首先验证是否可以有操作表的权限,然后调用InnoDB 等存储引擎的读写接口来完成语句的操作,最后返回结果;
- 存储引擎。主要负责存储数据以及提供数据读写的接口。
2. 讲日志系统:一条 SQL 更新语句的执行
「MySQL里经常说到的WAL技术,WAL的全称是Write-Ahead Logging,它的关键点就是先写日志,再写磁盘」
InnoDB中特有的日志 redo-log:
- 在 InnoDB 中可以保证即使数据库发生了重启,之前提交的记录并不会丢失,这个能力称为 “crash-safe”「提供崩溃恢复功能」;
- 是物理日志,记录的是“这个数据页上做了什么改变”,与存储引擎的作用相符,主要是处理存储方面的功能;
- 是循环写的,空间固定会用完的。用完了就把之前的擦除,然后继续写,用两个指针来控制。
Server 层独有的日志 binlog:
- 是 Server 层实现的,所有引擎都可以使用;
- 作用是用于归档,记录的是“做了什么逻辑操作”,重点在执行逻辑上,不在存储,因为 Server 就是负责 Mysql 功能方面;
- 是可以追加写的,写完一页换一页,不会擦除以前的。
mysql> update T set c=c+1 where ID=2;
上述的 update 语句执行顺序大体和 1 中一样,具体的执行器的流程如下:
- 执行器先找引擎,获取 ID = 2 这一行,ID 是主键,引擎会直接用索引树定位到这一行的数据,如果这一行的数据在内存中,那直接返回就好了,如果不在,则需要去磁盘中读取并加载到内存中,然后返回给执行器;
- 执行器拿到了这一行的数据,将 c 自加一,然后调用引擎去写入数据;
- 引擎会先将这个数据加载到内存中,并且去写 redo-log,此时注意,redo-log 并未提交,而是处于 prepare 状态,然后告知执行器执行完成了,随时可以提交事务;
- 执行器收到了存储引擎的消息后,就开始写入 bin-log,并把 bin-log写入磁盘;
- 执行器写入完毕后,调用引擎的提交事务接口,引擎把刚刚写入的 redo-log 改成 commit 状态,提交事务。
上述值得注意的是, redo-log 是 两阶段提交,也就是 先 prepare 再 commit。之所以这么做的原因是为了让两份日志之间的逻辑一致。
要注意,现阶段的数据恢复还是靠着 binlog + 整库备份去完成的,按理说 redo-log 也能完成而且速度肯定要比 bin-log 快得多,但是由于 redo-log 是会擦除的,不具备bin-log 的归档功能,所以现阶段还是没办法。
思考题:定期全量备份的周期“取决于系统重要性,有的是一天一备,有的是一周一备”。那么在什么场景下,一天一备会比一周一备更有优势呢?或者说,它影响了这个数据库系统的哪个指标?
一天一备:好处是“最长恢复时间”更短。在一天一备的模式里,最坏情况下需要应用一天的binlog。比如,你每天0点做一次全量备份,而要恢复出一个到昨天晚上23点的备份。但是坏处也是显而易见的,就是你需要大量的存储。
一周一备最坏情况就要应用一周的binlog了。这个恢复时间长,但是明显存储的消耗就小得多了。
3. 事务隔离
事务的四大特性:
- 原子性。要么不做,要么全做;
- 一致性。在处理事务的过程中,保证数据库状态和其他业务规则保持一致,比如转账,前后两者的账湖总额不变;
- 隔离性。不同事务之间互不影响;
- 持久性。事务提交之后需要持久化到数据库中去,即使崩溃,也能恢复。
事务的隔离级别:
- 读未提交。别人还没提交的东西,就能读取到,会产生幻读、不可重复读、脏读的问题;「一个事务还没提交时,它做的变更就能被别的事务看到。」
- 读提交。别人提交的东西,就能读取到。会产生幻读、不可重复读;「一个事务提交之后,它做的变更才会被其他事务看到。」
- 可重复读。在事务执行期间,别人就算提交了东西并改动了,当前事务看不到,不会去读最新的,还是可能会产生幻读的情况;「一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。」
- 序列化。加锁,牛逼。「顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。」
其中,最难理解的就是可重复读的隔离级别是如何实现的,这也是mysql的默认隔离级别:
就是使用的回滚技术,在MySQL中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。「由undo log完成的」
如图所示,这也是 数据库的多版本并发控制的基础,采用回滚机制。当然了,回滚日志不能一直保留,所以在必要的时候删除,删除的时间点就是当没有事务再需要用到这些回滚日志时,回滚日志会被删除。所以我们最好不要使用长事务,这样会导致回滚的数据很多很多。除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库。
如何避免长事务对业务的影响?
这个问题,我们可以从应用开发端和数据库端来看。
首先,从应用开发端来看:
- 确认是否使用了set autocommit=0。这个确认工作可以在测试环境中开展,把MySQL的general_log开起来,然后随便跑一个业务逻辑,通过general_log的日志来确认。一般框架如果会设置这个值,也就会提供参数来控制行为,你的目标就是把它改成1。
- 确认是否有不必要的只读事务。有些框架会习惯不管什么语句先用begin/commit框起来。我见过有些是业务并没有这个需要,但是也把好几个select语句放到了事务中。这种只读事务可以去掉。
- 业务连接数据库的时候,根据业务本身的预估,通过SET MAX_EXECUTION_TIME命令,来控制每个语句执行的最长时间,避免单个语句意外执行太长时间。(为什么会意外?在后续的文章中会提到这类案例)
其次,从数据库端来看:
- 监控 information_schema.Innodb_trx表,设置长事务阈值,超过就报警/或者kill;
- Percona的pt-kill这个工具不错,推荐使用;
- 在业务功能测试阶段要求输出所有的general_log,分析日志行为提前发现问题;
- 如果使用的是MySQL 5.6或者更新版本,把innodb_undo_tablespaces设置成2(或更大的值)。如果真的出现大事务导致回滚段过大,这样设置后清理起来更方便。
4. 深入浅出索引(上)
为何不使用 哈希表、数组、B 树、二叉搜索树,而采用了 B+?
为何不用哈希表?
哈希表进行等值查询的确很快,O(1) 的时间复杂度,但是其进行范围查询就必须遍历全表,并且如果相同的值太多,就容易导致哈希冲突,性能进一步下降。
为何不用数组?
数组的确可以很快的进行等值查询和范围查询,但是其更新很麻烦,如果中间需要删除,那就要移动后面所有数据,所以只适用于静态存储引擎。
为何不用 B 树?
B 树最大的问题就是非叶子节点有一个 Data 域,我们知道数据库的性能主要取决于磁盘 I/O 的次数,mysql 采用了磁盘预读的机制,也就是每一个树的节点都申请一页的存储空间,这样方便读写,就是因为 B 树存储了 Data 域,相比之下肯定就只能存更少的指针域了,这样就会导致整个树的高度拉高,磁盘 I/O 次数增加,二叉搜索树同样如此,树高过高导致磁盘 I/O 次数增加。
为何用 B+ 树?
- 我觉得第一点就是因为其树低,每个非叶子节点只存储了指针域和主键(这里说的是聚集索引);
- 叶子节点有链表,方便范围查询;
- 查询效率很稳定。
为何最好使用自增主键?
因为这样就可以减少维护索引有序性带来的开销,如果不采用自增主键,那么在插入的时候可能会在中间,造成一个页分裂的过程,开销很大,会导致存储的碎片化。
5. 深入浅出索引(下)
什么叫回表?
普通索引在找到主键后,回到主键索引树搜索的过程,称为回表。
如何减少回表的次数呢?
覆盖索引。覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。其实就是联合索引,然后符合覆盖的要求就称之为覆盖索引,就是多个字段拼成一个索引,然后对应主键的映射。
最左前缀规则。只要满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左N个字段,也可以是字符串索引的最左M个字符。
基于上面对最左前缀索引的说明,我们来讨论一个问题:在建立联合索引的时候,如何安排索引内的字段顺序。
这里我们的评估标准是,索引的复用能力。因为可以支持最左前缀,所以当已经有了(a,b)这个联合索引后,一般就不需要单独在a上建立索引了。因此,第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。那如果既有联合索引,又有基于 a、b 各自的索引呢?那就考虑空间问题了,如果 a 的字段明显比 b 的长,则一页下可以容纳更多的 b,那肯定就创建一个(a,b)的联合索引和一个(b)的单字段索引。
索引下推。在MySQL 5.6之前,只能从ID3开始一个个回表。到主键索引上找出数据行,再对比字段值。
而MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
6. 全局锁和表级锁
根据加锁范围:MySQL里面的锁可以分为:全局锁、表级锁、行级锁
一、全局锁:
对整个数据库实例加锁。
MySQL提供加全局读锁的方法:Flush tables with read lock(FTWRL)
FTWRL的启动时间点:
- FTWRL 前有读写的话 ,FTWRL 都会等待 读写执行完毕后才执行;
- FTWRL 执行的时候要刷脏页的数据到磁盘,因为要保持数据的一致性 ,理解的执行FTWRL时候是 所有事务 都提交完毕的时候。
这个命令可以使整个库处于只读状态。使用该命令之后,数据更新语句、数据定义语句和更新类事务的提交语句等操作都会被阻塞。
使用场景:全库逻辑备份。
风险:
1.如果在主库备份,在备份期间不能更新,业务停摆
2.如果在从库备份,备份期间不能执行主库同步的binlog,导致主从延迟
官方自带的逻辑备份工具mysqldump,当mysqldump使用参数–single-transaction的时候,会启动一个事务,确保拿到一致性视图。而由于MVCC的支持,这个过程中数据是可以正常更新的。
一致性读是好,但是前提是引擎要支持这个隔离级别。
如果要全库只读,为什么不使用set global readonly=true的方式?
1.在有些系统中,readonly的值会被用来做其他逻辑,比如判断主备库。所以修改global变量的方式影响太大。
2.在异常处理机制上有差异。如果执行FTWRL命令之后由于客户端发生异常断开,那么MySQL会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为readonly之后,如果客户端发生异常,则数据库就会一直保持readonly状态,这样会导致整个库长时间处于不可写状态,风险较高。
二、表级锁
MySQL里面表级锁有两种,一种是表锁,一种是元数据锁(meta data lock,MDL)
表锁的语法是:lock tables … read/write
可以用unlock tables主动释放锁,也可以在客户端断开的时候自动释放。lock tables语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。
对于InnoDB这种支持行锁的引擎,一般不使用lock tables命令来控制并发,毕竟锁住整个表的影响面还是太大。
MDL:不需要显式使用,在访问一个表的时候会被自动加上。Server 层面实现的!!!!
MDL的作用:保证读写的正确性。
在对一个表做增删改查(DML)操作的时候,加MDL读锁;当要对表做结构变更(DDL)操作的时候,加MDL写锁。
读锁之间不互斥。因为有隔离级别在,mvcc,所以读锁之间无需复制即可,读写锁之间,写锁之间是互斥的,用来保证变更表结构操作的安全性。
MDL 会直到事务提交才会释放,在做表结构变更的时候,一定要小心不要导致锁住线上查询和更新。所以小心在给小表做 DDL 时把这个库搞崩。
数据库四种语言:
- DDL (Data Definition Language):数据定义语言,无需提交就可以执行的,主要有 CREATE、ALTER、DROP等等;
- DML(Data Manipulation Language):数据操作语言,这个也是需要提交事务的哦,INSERT、UPDATE、DELETE、TRUNCATE、SELECT、以及 LOCK TABLE、CALL、EXPLAIN PLAN
- DCL(Data Control Language):数据控制语言,包括 GRANT、REVOKE
- TCL(Transaction Control Language):事务控制语言,包括 ROLLBACK、COMMIT、SAVEPOINT、SET TRANSACTION。
今日思考题:备份一般都会在备库上执行,你在用–single-transaction方法做逻辑备份的过程中,如果主库上的一个小表做了一个DDL,比如给一个表上加了一列。这时候,从备库上会看到什么现象呢?
假设这个DDL是针对表t1的, 这里我把备份过程中几个关键的语句列出来:
1
2
3
4
5
6
7
8
9
10
11
12
13 >Q1:SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
>Q2:START TRANSACTION WITH CONSISTENT SNAPSHOT;
>/* other tables */
>Q3:SAVEPOINT sp;
>/* 时刻 1 */
>Q4:show create table `t1`;
>/* 时刻 2 */
>Q5:SELECT * FROM `t1`;
>/* 时刻 3 */
>Q6:ROLLBACK TO SAVEPOINT sp;
>/* 时刻 4 */
>/* other tables */
>
在备份开始的时候,为了确保RR(可重复读)隔离级别,再设置一次RR隔离级别(Q1);
启动事务,这里用 WITH CONSISTENT SNAPSHOT确保这个语句执行完就可以得到一个一致性视图(Q2);
设置一个保存点,这个很重要(Q3);
show create 是为了拿到表结构(Q4),然后正式导数据 (Q5),回滚到SAVEPOINT sp,在这里的作用是释放 t1的MDL锁 (Q6。当然这部分属于“超纲”,上文正文里面都没提到。
DDL从主库传过来的时间按照效果不同,我打了四个时刻。题目设定为小表,我们假定到达后,如果开始执行,则很快能够执行完成。
参考答案如下:
- 如果在Q4语句执行之前到达,现象:没有影响,备份拿到的是DDL后的表结构。
- 如果在“时刻 2”到达,则表结构被改过,Q5执行的时候,报 Table definition has changed, please retry transaction,现象:mysqldump终止;
- 如果在“时刻2”和“时刻3”之间到达,mysqldump占着t1的MDL读锁,binlog被阻塞,现象:主从延迟,直到Q6执行完成。
- 从“时刻4”开始,mysqldump释放了MDL读锁,现象:没有影响,备份拿到的是DDL前的表结构。
7. 讲行锁功过:怎么减少行锁对性能的影响
行锁是在存储引擎级别实现的,并不是像全局锁和表级锁一样在 Server 端实现。
InnoDB 支持行锁,不支持列锁,并且 innodb 行级锁是通过锁索引记录实现的,如果更新的列没建索引是会锁住整个表的。因为不走索引,所以必须遍历整个表,那么就会读取一行,锁一行,又因为是两阶段锁,只有更新成功,事务提交之后才会释放锁。MyISAM 最小的锁粒度是表,并且由于表锁的存在,同时刻只能是单个线程对表进行操作,这很大程度上限制了并发度,所以这也是 InnoDB 取代 MyISAM 的重要原因吧。
- 两阶段锁的概念是什么? 对事务使用有什么帮助?
两阶段锁是指行锁是在需要的时候加锁,但是并不是在不用了之后就释放锁,而是等到事务提交才释放锁,这一点跟 MDL (元数据锁)很像;
对事务的帮助就是:一定要将并发度高、时间较长的操作的行放到事务提交前,防止过多的线程等待锁。
- 为什么要使用两阶段锁协议,为何不在事务启动时就加锁,又为什么不在语句执行完就解锁呢?
首先回答一下为什么不在事务启动时就加锁,我觉得原因有两个:
- 减小锁的粒度和加锁时长,提高并发度。
- 事务启动的时候,并没有明确说明需要修改什么行,此时如果要锁定行必须锁定整个表才行。
再回答一下为什么不在语句执行完就解锁。
因为这样事务如果回滚的话,就会有问题了。例如下面这种情况:
如果事务 A 回滚,那肯定无法回滚了,因为已经有多个事务修改了这个值,并且人家已经提交了,无法回滚。
- 死锁的概念是什么? 举例说明出现死锁的情况。
两个或多个线程在互相等待对方释放资源的过程称为死锁状态,出现死锁的情况一般都是嵌套锁(跟多线程中嵌套同步代码块一样)。
所以我的建议是在加锁的时候一般都做到顺序加锁,这样就能防止锁互相嵌套问题。
- 死锁的处理策略有哪两种?
在数据库中,死锁的处理策略有:
- 有个超时机制,这个超时时间可以通过参数innodb_lock_wait_timeout来设置。时间到了自动释放锁,并且回滚当前事务,这个默认是 50s,时间太长,一般不考虑;
- 发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数innodb_deadlock_detect设置为on,表示开启这个逻辑。这个默认是开启的,但是吧,这个很消耗资源。
- 有哪些思路可以解决热点更新导致的并发问题?
热点更新导致的并发问题,症结就在于死锁检测非常的消耗资源,所以可以有以下解决措施:
- 尽量避免死锁,比如上文说的加锁按顺序,然后直接将死锁检测关闭,但是这样容易造成业务有损,一般死锁检测虽然浪费资源但是是业务无损的,所以关闭可能不是特别可取;
- 控制并发度,对应相同行的更新,在进入引擎之前排队。这样在InnoDB内部就不会有大量的死锁检测工作了;
- 将热点细分,比如购买电影票,把影院的账户总额拆成10条记录来存储,其10条记录的和等于影院账户余额,这样能尽量减少死锁的情况。
- 什么时候会进行死锁检测?一旦触发死锁检测是检测所有事务吗?
如果他要加锁访问的行上有锁,他才要检测。
不是每次死锁检测都都要扫所有事务。比如某个时刻,事务等待状态是这样的:
B在等A,
D在等C,
现在来了一个E,发现E需要等D,那么E就判断跟D、C是否会形成死锁,这个检测不用管B和A
今日面试题:
如果你要删除一个表里面的前10000行数据,有以下三种方法可以做到:
- 第一种,直接执行delete from T limit 10000;
- 第二种,在一个连接中循环执行20次 delete from T limit 500;
- 第三种,在20个连接中同时执行delete from T limit 500。
你会选择哪一种方法呢?为什么呢?
在一个连接中循环执行20次 delete from T limit 500。
确实是这样的,第二种方式是相对较好的。
第一种方式(即:直接执行delete from T limit 10000)里面,单个语句占用时间长,锁的时间也比较长;而且大事务还会导致主从延迟。
第三种方式(即:在20个连接中同时执行delete from T limit 500),会人为造成锁冲突。
确实是这样的,第二种方式是相对较好的。
第一种方式(即:直接执行delete from T limit 10000)里面,单个语句占用时间长,锁的时间也比较长;而且大事务还会导致主从延迟。
第三种方式(即:在20个连接中同时执行delete from T limit 500),会人为造成锁冲突。
8. 事务到底是隔离还是不隔离的
这章知识点很多,难度比较大!
- 先回顾一下什么是 MVCC,如何实现的?
MVCC,多版本并发控制,在查询记录的时候,不同时刻启动的事务会有不同的read-view。如图中看到的,在视图A、B、C里面,这一个记录的值分别是1、2、4,同一条记录在系统中可以存在多个版本。「存储在undo log 日志中」
- 什么是 read-view?
一致性视图,全称是 consistent read-view,这里稍微补充一下,在 mysql中有两种视图:
- 一个是view。它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建视图的语法是create view … ,而它的查询方法与表一样。
- 另一个是InnoDB在实现MVCC时用到的一致性读视图,即consistent read view,用于支持RC(Read Committed,读提交)和RR(Repeatable Read,可重复读)隔离级别的实现。
在查询的时候,我们是需要用到 read-view的,但是在 update 的时候,是必须只能读当前最新值哦!!!「这里的最新值肯定是已经提交的事务去做的,如果未提交的事务去操作这个值,那肯定是加了行锁中的独占锁啦,所以你在 update 的话必须等其他事务释放锁。」
说了这么多,回到问题,read-view其实就是一个数组,每个事务或者语句都有的一个数组,只不过这个数组有点特殊。 InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务ID。“活跃”指的就是,启动了但还没提交。
数组里面事务ID的最小值记为低水位,当前系统里面已经创建过的事务ID的最大值加1记为高水位。
这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。
- 查询的时候,是如何用到 read-view 的?
InnoDB的行数据有多个版本,每个数据版本有自己的row trx_id,每个事务或者语句有自己的一致性视图。普通查询语句是一致性读,一致性读会根据row trx_id和一致性视图(read-view)确定数据版本的可见性。
- 对于可重复读,查询只承认在事务启动前就已经提交完成的数据;「因为对于可重复读的隔离级别,一致性视图是在事务开始的时候进行全库快照生成的」
- 对于读提交,查询只承认在语句启动前就已经提交完成的数据;「因为对于读提交的隔离级别来说,一致性视图是在每一次执行语句的时候生成的,跟事务没有关系」
总结上述两种情况,就是只有视图生成前事务提交了的数据我能看到,其他的我都看不到。
而当前读,总是读取已经提交完成的最新版本。「第 2 个问题已经讲了这个了」
- 上面一直强调的,当前读,什么时候会出现当前读的情况?
首先我们要明确,当前读,必须要读的是该行的最新版本,此时的视图快照已经失效了,而且你要想读该行的最新版本,就必须去加锁!!!!「意思很明确,就是 当前读 == 给该行加锁」
所以,什么时候会出现行的当前读呢?那就是给该行加锁『悲观锁』了,那问题就转化成了什么时候会给行加锁呢?
那这个问题就非常简单了,有以下几种情况会使得当前行加上悲观锁:
- SELECT … FROM … LOCK IN SHARE MODE,select 的时候强行加上共享锁,如果不加其实就是默认的快照读;
- SELECT … FROM … FOR UPDATE,select 的时候强行加上独占锁;
- UPDATE … WHERE … ,独占锁;
- DELETE FROM … WHERE … ,独占锁;
INSERT
语句将在插入的记录上加一把排他锁「独占锁」。好吧,其实也就是 select 啥都不加会导致不给行锁加悲观锁,其他的都会加的,意味着都是要去读当前值,意味着这个 rc 级别的隔离就无效了,不过话说回来,人家 rc 本身就是 可重复读 的隔离级别,本身就是用于 数据修改了我不去读 。
http://www.fordba.com/locks-set-by-different-sql-statements-in-innodb.html 这篇文章对 sql 语句加锁分析的我觉得是我看到的最好的了,受益匪浅!
- 第 4 个问题中提到的“lock in shared mode” “for update”是什么锁?
这个很简单啦,就是 mysql 中的悲观锁中的共享锁和独占锁。
- 既然谈到锁了,那你再回顾一下现在接触到的所有锁吧,也可以自己拓展一下!
其实,上面讲到的锁都是可以跟 lock 体系对应上的,比如说上文的乐观锁悲观锁,对应的就是 lock 体系的乐观锁悲观锁,乐观锁就是底层是 CAS,CAS 中有三个值:当前内存值、预读的值、想修改的值,如果当前值和预读值一样,我就默认没被修改过,直接修改成想修改的值,当然这样会造成一个问题:ABA 问题,在 mysql 中最主要的方法是加一个 version 字段,用 version 字段去判断是否被修改过,在 lock 体系中,最主要的方法是加一个 stamp,类似于时间戳这样的东西,在 StampedLock 里面有去用到。当然了,CAS 还会有另外两个问题:一个是自旋会造成cpu资源的浪费,一个是只能保证单个变量的原子性「你如果想保证多个的话就直接写到一个对象中就完事了」。
回到正题,上面讲完乐观锁,现在讲讲悲观锁吧,mysql 中的独占锁和共享锁和 AQS 中的完全一致,这两思想也完全一样,至于 mysql 中内部如何实现的我不太清楚,因为貌似是用 c 写的,看的兴趣不大, AQS 的底层源码是有看过的,AQS 就是用的模板方法的设计思想嘛,对外我们想要去实现读写锁的话只需要继承几个方法就可以了,比如独占锁 ReentrantLock 只需要继承 tryacquired()、tryRealease()、isHeldExclusive()…「说不定会让我手动去实现一个,这个我得注意一下,提前写一个,刚才写的过程中,又发现了 transient 这个关键词,真的牛皮,这个博客讲的真的好:https://juejin.im/post/5dda467051882572f8249f11#heading-10」。。。。懒得讲了。。。
9. 普通索引和唯一索引,应该如何选择
- 在查找过程中,普通索引和唯一索引的性能基本一样,但是在更新过程中,如果记录不在内存中,那普通索引和唯一索引的性能差距就很大了,因为普通索引可以使用 changebuffer,而唯一索引因为有唯一性约束,需要将所有数据读入到内存中,这样就失去了 changebuffer 的意义,所以速度很慢。
- changebuffer:存储用户更新的语句。在下次查询需要访问这个数据页的时候,才将数据页读入内存,执行 change buffer中与这个页有关的操作,注意哦,change buffer是可以持久化,将change buffer中的操作应用到原数据页,得到最新结果的过程称为merge。除了访问这个数据页会触发merge外,系统有后台线程会定期merge。在数据库正常关闭(shutdown)的过程中,也会执行merge操作。显然,如果能够将更新操作先记录在change buffer,减少读磁盘,语句的执行速度会得到明显的提升。而且,数据读入内存是需要占用buffer pool的,所以这种方式还能够避免占用内存,提高内存利用率。
- 故 changebuffer 适合写多读少的场景,这样能最大限度的加快写入的速度,所以说 changebuffer 可以避免写入的时候随机去读取数据到内存中,节省的是随机读磁盘的 IO 消耗。
- redo log 主要节省的是随机写磁盘的IO消耗(转成顺序写),而change buffer主要节省的则是随机读磁盘的IO消耗。
tip: 为什么说 redo log 是将磁盘的随机写变成顺序写?
如果没有 redo log,那么每次数据来了我们都要直接去落盘,每一次数据我们都需要去进行磁盘随机读写,而有了 redo log我们可以先将数据顺序直接追加到文件末尾「redo log文件的大小是固定的,所以可以是顺序写」,然后redo log再在之后进行落盘,这样的话我们的数据就是相当于顺序写入磁盘了。
kafka 的顺序写其实和mysql 顺序写是一样的,都是采用了日志,kafka中使用 log,而 mysql 中采用 redo-log,数据来了先顺序追加到日志中,比如说日志是 4 g大小,那这 4 g 是一起落盘的,所以说这4g就是顺序写入磁盘的,因为他们是在一块的,不需要多次磁盘寻址写入。
这个写的还可以吧: 浅析MySQL事务中的redo与undo
10. Mysql 为什么会选错索引
- 选索引的任务,是交给了优化器去做的,而优化器选择索引的最优方案主要是看扫描行数「通过取样去判断,所以这块可能会造成误差」、临时表和排序等等;
- 如果选错了索引,我们可以强制使用 force index,然后如果更快的话,我们可以用 explain 拿到 rows 的大小,然后使用 analyze table t 命令重新统计索引信息,这样下次就不会选错了。
11. 怎么给字符串加索引
- 直接创建完整索引,但是这样比较占用空间;
- 创建前缀索引,节省空间,但是容易导致回表次数增多,并且无法使用范围查询;
- 倒序存储,主要用来解决字符串本身的前缀区分度不高,例如身份证号;
- 创建hash字段索引,查询稳定,但是不支持范围查询,同时因为要额外增加字段和索引,消耗资源。
12. 为什么 Mysql 会 ”抖“ 一下?
我们在第 9 节的内容其实跟这节也有点关系,因为都是有关于 redo log,我们在第 9 节谈到,redo log的出现,将我们随机写磁盘变为了顺序写磁盘「当然 redo log还有crash-safe的功能」,但是我们一直没有去谈写完 redo log之后数据,借此机会,我还是希望能够好好地回顾一下整个数据库更新的过程。
前提:假设我要将 id = 2 中的 c + 1 ,步骤如下:
我们来细致分析一下在 写入 redo log,和提交事务分别发生了什么。
首先,我们需要知道的是,redo log 分为两部分:redo log buffer 和 redo log file,一个是在内存中,一个是在磁盘中,在写入 redo log 的时候,是先将数据写至 redo log buffer,然后再将 redo log buffer 中的数据顺序写入到redo log file,写完后处于 prepare 状态,在提交事务之前,就已经将 binlog 写入文件了,所以当提交事务的时候,redo log 和 binlog 全部持久化。
现在回到标题的问题。
- 什么叫”抖“一下?
一条SQL语句,正常执行的时候特别快,但是有时也不知道怎么回事,它就会变得特别慢,并且这样的场景很难复现,它不只随机,而且持续时间还很短。可以认为是”抖“了一下。
- 为什么 Mysql 会经常抖一下?
根本原因是数据库此时在大量刷脏页
- 什么是脏页?
内存页与磁盘页不一致的页称为脏页
- 什么时候需要刷脏页?
- redo log 满了,需要将脏页落盘;
- 内存中 buffer pool 满了,需要将脏页落盘,变成干净页;
- Mysql 空闲,后台进行刷脏页;
- Mysql 正常关闭前,进行刷脏页。
- 这四种情况的刷脏页,对系统影响如何?
- redo log 写满再刷脏页,对系统影响很大,因为此时系统不能进行更新了,会阻塞;
- buffer pool 满了刷脏页,是很常见的,但是如果脏页多了,清理起来很慢;
- Mysql 空闲时刷脏页,对系统不会有啥性能上的影响;
- Mysql 正常关闭前刷脏页同样对系统不会有什么影响。
- 那应该如何避免刷脏页带来的性能影响呢?
控制脏页比例,刷盘速度不能过慢,可以通过 inno_io_capacity来控制速度,当然过快也没有必要,因为这样势必会消耗更多的资源导致用户使用资源变少了。
总结:虽然 redo log 可以带来 crash-safe 和将随机写变为顺序写这两大功能,但是其也带来了内存脏页的问题。
13. 为什么表数据删掉一半,表文件大小不变
- 表空洞问题
- delete 命令删除行是不能回收表空间的,只是当前页被标记为”可复用“,例如300 400 500,删除了400之后,400这个数据页就可以被复用,比如 401 就可以插入到其中,但是如果下一个是 800,那 400 这个数据页就只能被浪费了,成为空洞。
- 不止是删除数据会导致空洞,插入数据也会。当随机插入数据时,会造成索引的数据页分裂。例如当 300 500 600是一页的时候,此时来了个 550,那么就不得不申请一个新的页面来保存数据了,此时 550 会在新的数据页中,而 600 紧随其后,此时原来的页就只剩下了 300 500 了,这样就造成了数据页空洞。「这也是磁盘按页分配按页预读的坏处吧..」
- 如何解决表空洞问题?
重建表。例如表 A 存在空洞情况,可以新建一个与表 A 结构相同的表 B,然后按照主键 ID 递增的顺序将数据一行一行的插入到 B 中,也就是将 B 变成临时表,最后将 A 替换即可。
直接使用 “alter table A engine=InnoDB”即可,系统会自动帮我们完成转存数据、交换表名、删除旧表的操作,而其中最慢的步骤就是往临时表插入数据的过程,在 MySQL 5.6版本之前,不能一边往临时表插入数据一边对原表 A 进行更新,但是在 MySQL 5.6 之后,可以进行 Online DDL。
- Online DDL 的流程?
- 建立一个临时文件,扫描 A 主键的所有数据页;
- 用数据页中表 A 的记录生成 B+ 树,存储到临时文件中;
- 生成临时文件的过程中,将所有对 A 的操作记录在一个日志文件中,对应的是图中 state2 的状态;
- 临时文件生成后,将日志文件中的操作应用到临时文件,得到一个逻辑数据上和 A 相同的数据文件,对应的就是 state3 的状态;
- 用临时文件替换 A 的数据文件。
总结一下,就是用了一个 row log 来保证 Online 的。
- optimize table、analyze table、alter table这三种重建表的区别
Alter table 就是上面的步骤啦,转存数据、交换表名、删除旧表;
analyze table 其实不是重建表,只是对表的索引信息做一个重新统计,没有修改数据,比如之前修改优化器统计的遍历的 row 数;
optimize table 等于 recreate + analyze
14. count 这么慢,我该怎么办?
- 为什么 InnoDB 不像 MyISAM 一样,专门来记录 count 值?
因为 InnoDB 的隔离级别是 rr 级别,可重复读,所以在不同事务下,由于多版本并发控制的原因(MVCC),每一行记录都要判断自己是否对这个会话可现,因此对于 count(*) 来说,只好把数据一行一行的读出并进行判断。
- 那这种方法太蠢了,没有优化吗?
你都知道蠢了,当然就有优化啊,你想想,InnoDB 是索引组织表,主键索引树的叶子节点是数据,而普通索引树上的叶子结点则是主键值,所以普通索引树比主键索引树肯定要小很多,而不管遍历哪个索引树其实结果都是一样的,所以 Mysql 会找到最小的那棵树进行遍历。
在保证逻辑正确的前提下,尽量减少扫描的数据量,是数据库设计的通用法则之一。
- 那我们之前在第 10 节谈到的 ”show table status“ 命令中的 row numbers 能用吗?
当然不能啊,那只是一个预估值,官方说这个误差可以达到 40% ,所以肯定不行啊。
- 那如果我有一个总是要统总数的业务,该怎么办呢?
提出这个问题,说明你肯定觉得优化后的方法依旧很慢,是的,所以我们只能自己去操作。
把 count 这个计数直接放到数据库里单独的一张计数表 C 中,一是解决了崩溃丢失的问题,因为InnoDB支持崩溃恢复,二是解决了事务导致的无法精确统计count的问题,直接将表C中的计数值这个操作也加入到事务中就很okay了。
- count(字段)、count(主键id)、count(1)、count(*)的区别
count(*)、count(主键id)和count(1) 都表示返回满足条件的结果集的总行数;而count(字段),则表示返回满足条件的数据行里面,参数“字段”不为NULL的总个数。
至于分析性能差别的时候,你可以记住这么几个原则:
- server层要什么就给什么;
- InnoDB只给必要的值;
- 现在的优化器只优化了count(*)的语义为“取行数”,其他“显而易见”的优化并没有做。
这是什么意思呢?接下来,我们就一个个地来看看。
对于count(主键id)来说,InnoDB引擎会遍历整张表,把每一行的id值都取出来,返回给server层。server层拿到id后,判断是不可能为空的,就按行累加。
对于count(1)来说,InnoDB引擎遍历整张表,但不取值。server层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加。
单看这两个用法的差别的话,你能对比出来,count(1)执行得要比count(主键id)快。因为从引擎返回id会涉及到解析数据行,以及拷贝字段值的操作。
对于count(字段)来说:
- 如果这个“字段”是定义为not null的话,一行行地从记录里面读出这个字段,判断不能为null,按行累加;
- 如果这个“字段”定义允许为null,那么执行的时候,判断到有可能是null,还要把值取出来再判断一下,不是null才累加。
也就是前面的第一条原则,server层要什么字段,InnoDB就返回什么字段。
但是count(*)是例外,并不会把全部字段取出来,而是专门做了优化,不取值。count(*)肯定不是null,按行累加。
看到这里,你一定会说,优化器就不能自己判断一下吗,主键id肯定非空啊,为什么不能按照count(*)来处理,多么简单的优化啊。
当然,MySQL专门针对这个语句进行优化,也不是不可以。但是这种需要专门优化的情况太多了,而且MySQL已经优化过count(*)了,你直接使用这种用法就可以了。
所以结论是:按照效率排序的话,count(字段)<count(主键id)<count(1)≈count(*),所以我建议你,尽量使用count(*)。
15. 日志和索引相关问题
在第 2 讲中,有提到 redo log 和 binlog 的两阶段提交,这也是分布式系统常用的手段。
其中,时刻 A 如果 crash 了,此时还没写 binlog,所以崩溃恢复肯定就将 redo log 中的数据进行回滚就行了,时刻 B 情况,会稍微复杂一点,因为这个时候有三种情况:
- binlog 写完了并且redo log 已经有了 commit 标志,那这个时候肯定就直接提交就好了;
- binlog 没写完,redo log 此时只有 prepare 标志,那这个时候肯定得回滚了,因为binlog没写完;
- binlog有写完了的标志,但是 redo log 此时只有 prepare 标志,没问题,这个也可以直接提交。
追问:
- binlog 如何判断写完了?
一个事务的binlog是有完整格式的:
- statement格式的binlog,最后会有COMMIT;
- row格式的binlog,最后会有一个XID event。
- redo log 如何知晓binlog 写完了,然后将状态从 prepare 变为 commit,又是如何扫描 prepare or commit 标志,然后提交的呢?
它们有一个共同的数据字段,叫XID。binlog 写完后,有相应的通知机制,此时redo log 对应的 XID 状态变化。
崩溃恢复的时候,会按顺序扫描redo log:
- 如果碰到既有prepare、又有commit的redo log,就直接提交;
- 如果碰到只有parepare、而没有commit的redo log,就拿着XID去binlog找对应的事务。
- 只要binlog不可以吗,也可以提供顺序写磁盘的功能,如果 binlog 也加一个 crash-safe 的功能,那不是既能归档,又能崩溃恢复吗?
binlog存储的是逻辑日志,而且并没有标记读到哪了,并不能记录数据页的更改,所以不能做到崩溃恢复。
- 那能不能只要 redo log,不要 binlog?
从逻辑上可以,因为 redo log 除了不能归档,其他都能做。
但是,如果你了解一下业界各个公司的使用场景的话,就会发现在正式的生产库上,binlog都是开着的。因为binlog有着redo log无法替代的功能。
一个是归档。redo log是循环写,写到末尾是要回到开头继续写的。这样历史日志没法保留,redo log也就起不到归档的作用。
一个就是MySQL系统依赖于binlog。binlog作为MySQL一开始就有的功能,被用在了很多地方。其中,MySQL系统高可用的基础,就是binlog复制。
还有很多公司有异构系统(比如一些数据分析系统),这些系统就靠消费MySQL的binlog来更新自己的数据。关掉binlog的话,这些下游系统就没法输入了。
总之,由于现在包括MySQL高可用在内的很多系统机制都依赖于binlog,所以“鸠占鹊巢”redo log还做不到。你看,发展生态是多么重要。
- 数据的最终落盘,是从 redo log更新的还是从 buffer pool 更新的呢?
实际上,redo log并没有记录数据页的完整数据,所以它并没有能力自己去更新磁盘数据页,也就不存在“数据最终落盘,是由redo log更新过去”的情况。
- 如果是正常运行的实例的话,数据页被修改以后,跟磁盘的数据页不一致,称为脏页。最终数据落盘,就是把内存中的数据页写盘。这个过程,甚至与redo log毫无关系。
- 在崩溃恢复场景中,InnoDB如果判断到一个数据页可能在崩溃恢复的时候丢失了更新,就会将它读到内存,然后让redo log更新内存内容。更新完成后,内存页变成脏页,就回到了第一种情况的状态。
- 在我们第 12 讲的,如果 redo log 满了,也是要刷脏页的,此时会将 redo log对应的内存中的脏页刷入磁盘。
总而言之,其实 redo log 就是一个中间者,比如10 到 19,redo log 只是记录的 +9,所以肯定是从buffer pool 中更新的数据。
- redo log buffer 是什么?是先修改内存,还是先写 redo log 文件?
Redo log 是分为两块的:一块就是 redo log buffer,一块就是 redo log 文件,正常肯定是先到缓存,再到文件中啦。这里的写文件是一个顺序写的过程。等不忙的时候再执行 redo log 文件,进行最终的落盘。「也就是我们之前一直提到的刷脏页」
16. ”order by“ 是如何工作的?
- order by 是 Mysql 中是如何工作的?
有两种算法执行 order by。
- 全字段排序;
- rowid 排序。
科普:
MySQL会为每个线程分配一个内存(sort_buffer)用于排序该内存大小为sort_buffer_size
- 如果排序的数据量小于sort_buffer_size,排序将会在内存中完成如果排序数据量很大,内存中无法存下这么多数据,则会使用磁盘临时文件来辅助排序,也称外部排序
- 在使用外部排序时,MySQL会分成好几份单独的临时文件用来存放排序后的数据,然后在将这些文件合并成一个大文件
mysql会通过遍历索引将满足条件的数据读取到sort_buffer,并且按照排序字段进行快速排序
- 如果查询的字段不包含在辅助索引中,需要按照辅助索引记录的主键返回聚集索引取出所需字段
- 该方式会造成随机IO,在MySQL5.6提供了MRR的机制,会将辅助索引匹配记录的主键取出来在内存中进行排序,然后在回表
- 按照情况建立联合索引来避免排序所带来的性能损耗,允许的情况下也可以建立覆盖索引来避免回表
全字段排序过程:
通过索引将所需的字段全部读取到sort_buffer中
按照排序字段进行排序
- 将结果集返回给客户端
优点: MySQL认为内存足够大时会优先选择全字段排序,因为这种方式比rowid 排序避免了一次回表操作
缺点:
- 造成sort_buffer中存放不下很多数据,因为除了排序字段还存放其他字段,对sort_buffer的利用效率不高
- 当所需排序数据量很大时,会有很多的临时文件,排序性能也会很差
rowid 排序
通过控制排序的行数据的长度来让sort_buffer中尽可能多的存放数据,max_length_for_sort_data
只将需要排序的字段和主键读取到sort_buffer中,并按照排序字段进行排序
- 按照排序后的顺序,取id进行回表取出想要获取的数据
- 将结果集返回给客户端
优点:更好的利用内存的sort_buffer进行排序操作,尽量减少对磁盘的访问
缺点:回表的操作是随机IO,会造成大量的随机读,不一定就比全字段排序减少对磁盘的访问
- 两种算法的应用场景?
如果MySQL实在是担心排序内存太小,会影响排序效率,才会采用rowid排序算法,这样排序过程中一次可以排序更多行,但是需要再回到原表去取数据。
如果MySQL认为内存足够大,会优先选择全字段排序,把需要的字段都放到sort_buffer中,这样排序后就会直接从内存里面返回查询结果了,不用再回到原表去取数据。
这也就体现了MySQL的一个设计思想:如果内存够,就要多利用内存,尽量减少磁盘访问。
对于InnoDB表来说,rowid排序会要求回表多造成磁盘读,因此不会被优先选择。
这个结论看上去有点废话的感觉,但是你要记住它,下一篇文章我们就会用到。
看到这里,你就了解了,MySQL做排序是一个成本比较高的操作。那么你会问,是不是所有的order by都需要排序操作呢?如果不排序就能得到正确的结果,那对系统的消耗会小很多,语句的执行时间也会变得更短。
其实,并不是所有的order by语句,都需要排序操作的。从上面分析的执行过程,我们可以看到,MySQL之所以需要生成临时表,并且在临时表上做排序操作,其原因是原来的数据都是无序的。
- 那如果原来的数据就是有序的,是不是就可以不用排序了?如何做到有序呢?
是的,如果数据本身就是有序的,就无须排序了。
这里,我们就可以采用覆盖索引,因为索引本身就是有序的,这样就不需要额外排序带来系统的消耗了,但是同样的维护索引也是有维护代价的。
17. 如何正确地显示随机消息
MySQL对临时表排序的执行过程。
如果你直接使用order by rand(),这个语句需要Using temporary 和 Using filesort,查询的执行代价往往是比较大的。所以,在设计的时候你要尽量避开这种写法。
18. 为什么这些SQL语句逻辑相同,性能却差异巨大
- 条件字段函数操作
对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。
会遍历全表,所以很慢…
- 隐式类型转换
mysql> select * from tradelog where tradeid=110717;
例如上面这条语句, tradeid 是 varchar 类型,但是传进来整数型,这个时候就会类型转换,导致不能走索引了。
其实类型转换就是相当于函数操作了,所以会走全表而不是走索引。
- 隐式字符编码转换
字符集不同只是条件之一,连接过程中要求在被驱动表的索引字段上加函数操作,是直接导致对被驱动表做全表扫描的原因。
总而言之,这些 sql 语句之所以执行慢,就是因为加了在条件中加了函数操作。
19. 为什么我只查一行的语句,也执行这么慢
方法:执行一下show processlist命令,看看当前语句处于什么状态。
原因可能有:
第一类:查询一条数据长时间不返回
- 等 MDL 锁。使用show processlist命令,发现是 Waiting for table metadata lock。出现这个状态表示的是,现在有一个线程正在表t上请求或者持有MDL写锁,把select语句堵住了。
- 等行锁。
第二类:查询慢
一致性读的问题,需要不断回滚undo log,所以慢。这里因为只是查一行,所以不能算是慢查询,只能算坏查询,等数据量上去了,就变成慢查询了。
20. 幻读是什么,幻读有什么问题?「未认真研读」
- 幻读是什么?
幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。
这里,我需要对“幻读”做一个说明:
- 在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。因此,幻读在“当前读”下才会出现。「加行锁读的时候才会出现,破坏mvcc,例如update、insert」
- 幻读仅专指“新插入的行”。
- 幻读有什么问题?
- 首先是语义上有问题。session A在T1时刻就声明了,“我要把所有d=5的行锁住,不准别的事务进行读写操作”。而实际上,这个语义被破坏了。
- 数据一致性问题。
即使把所有的记录都加上锁,还是阻止不了新插入的记录
- 那这么难对付,Mysql 是如何解决呢?
产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB只好引入新的锁,也就是间隙锁(Gap Lock)。
间隙锁和行锁合称next-key lock,每个next-key lock是前开后闭区间。
间隙锁和next-key lock的引入,帮我们解决了幻读的问题,但同时也带来了一些“困扰”。
间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的
21. 为什么我只改一行的语句,锁这么多「未研读」
有点难…有时间再研读
22. MySQL有哪些“饮鸩止渴”提高性能的方法
- 面对短连接风暴时,如何解决?
第一种方法:先处理掉那些占着连接但是不工作的线程。
第二种方法:减少连接过程的消耗。有的业务代码会在短时间内先大量申请数据库连接做备用,如果现在数据库确认是被连接行为打挂了,那么一种可能的做法,是让数据库跳过权限验证阶段。
跳过权限验证的方法是:重启数据库,并使用–skip-grant-tables参数启动。这样,整个MySQL会跳过所有的权限验证阶段,包括连接过程和语句执行过程在内。
- 慢查询问题如何解决?
在MySQL中,会引发性能问题的慢查询,大体有以下三种可能:
索引没有设计好;
SQL语句没写好;
MySQL选错了索引。
导致慢查询的第一种可能是,索引没有设计好。这种场景一般就是通过紧急创建索引来解决。MySQL 5.6版本以后,创建索引都支持Online DDL了,对于那种高峰期数据库已经被这个语句打挂了的情况,最高效的做法就是直接执行alter table 语句。比较理想的是能够在备库先执行。假设你现在的服务是一主一备,主库A、备库B,这个方案的大致流程是这样的:
- 在备库B上执行 set sql_log_bin=off,也就是不写binlog,然后执行alter table 语句加上索引;
- 执行主备切换;
- 这时候主库是B,备库是A。在A上执行 set sql_log_bin=off,然后执行alter table 语句加上索引。
这是一个“古老”的DDL方案。平时在做变更的时候,你应该考虑类似gh-ost这样的方案,更加稳妥。但是在需要紧急处理时,上面这个方案的效率是最高的。
导致慢查询的第二种可能是,语句没写好。
这时,我们可以通过改写SQL语句来处理。MySQL 5.7提供了query_rewrite功能,可以把输入的一种语句改写成另外一种模式。
比如,语句被错误地写成了 select * from t where id + 1 = 10000,你可以通过下面的方式,增加一个语句改写规则。
1
2
3
4 > mysql> insert into query_rewrite.rewrite_rules(pattern, replacement, pattern_database) values ("select * from t where id + 1 = ?", "select * from t where id = ? - 1", "db1");
>
> call query_rewrite.flush_rewrite_rules();
>
这里,call query_rewrite.flush_rewrite_rules()这个存储过程,是让插入的新规则生效,也就是我们说的“查询重写”。
导致慢查询的第三种可能,就是MySQL选错了索引。
这时候,应急方案就是给这个语句加上force index。
- 如何避免慢查询导致性能问题?
上面我和你讨论的由慢查询导致性能问题的三种可能情况,实际上出现最多的是前两种,即:索引没设计好和语句没写好。而这两种情况,恰恰是完全可以避免的。比如,通过下面这个过程,我们就可以预先发现问题。
- 上线前,在测试环境,把慢查询日志(slow log)打开,并且把long_query_time设置成0,确保每个语句都会被记录入慢查询日志;
- 在测试表里插入模拟线上的数据,做一遍回归测试;
- 观察慢查询日志里每类语句的输出,特别留意Rows_examined字段是否与预期一致。
不要吝啬这段花在上线前的“额外”时间,因为这会帮你省下很多故障复盘的时间。
23 - 28 讲
后续章节暂时感觉没有太多看的必要,最后以一道题结束本篇文章。
我们假设原来的大表有10000行数据,而符合条件的数据有100行,我们最后的结果集是只要10行
我们知道,limit是一个语句最后执行的,也就是说会先把符合条件的数据全部找到,最后再进行 limit。
在第一种情况中,会先去非聚集索引中找符合条件的主键值,然后进行回表,我们可以知道,会遍历大表10000行,回表100次,最后数据全部拿到之后,会进行 limit;
而在第二种情况,会直接先去非聚集索引中找符合条件的主键值,这一步和第一种情况是一致的,也是会遍历大表10000行,但是注意,这里在内部有进行 limit,也就是说,找到了符合条件的主键值,就直接 limit了,这样就会只剩下 10 个有效的主键值,然后再去回表,也是相当于只会回表十次。所以应该快的原因在这了….一个回表100次,一个回表 10 次,这还只是在符合条件的数据只有100行的情况下,如果符合条件的数据是1000000行,那这个回表的次数减少的可就很恐怖了。。。
一开始自己的想法以为是 right join 带来的性能的巨大提升,后面发现并不是,但是也顺带看了一遍 join 提升性能的一些方法。
「第34、35讲」
第一个问题:能不能使用join语句?
- 如果可以使用Index Nested-Loop Join算法,也就是说可以用上被驱动表上的索引,其实是没问题的;
- 如果使用Block Nested-Loop Join算法,扫描行数就会过多。尤其是在大表上的join操作,这样可能要扫描被驱动表很多次,会占用大量的系统资源。所以这种join尽量不要用。
所以你在判断要不要使用join语句时,就是看explain结果里面,Extra字段里面有没有出现“Block Nested Loop”字样。
第二个问题是:如果要使用join,应该选择大表做驱动表还是选择小表做驱动表?
- 如果是Index Nested-Loop Join算法,应该选择小表做驱动表;
- 如果是Block Nested-Loop Join算法:
- 在join_buffer_size足够大的时候,是一样的;
- 在join_buffer_size不够大的时候(这种情况更常见),应该选择小表做驱动表。
所以,这个问题的结论就是,总是应该使用小表做驱动表。
总结
后续其实还有10多节未看,但是稍微略读了下,感觉可读性不是很强了,毕竟不是专业 DBA,暂时 MySQL 告一段落了…