文章目录
- 00.问题分类
- 01.一条SQL查询语句是如何执行的?
- 02.一条SQL更新语句是如何执行的?
- 03.事务隔离:为什么你改了我还看不见?
- 04.深入浅出索引(上)
- 05.深入浅出索引(下)
- 06.全局锁和表锁:给表加个字段怎么有这么多阻碍
- 07.行锁功过:怎么减少行锁对性能的影响?
- 08.事务到底是隔离还是不隔离的
- 09.普通索引和唯一索引,该怎么选择?
- 10.MySQL为什么有时候会选错索引
- 11.怎么给字符串字段加索引
- 12.为什么我的MySQL会“抖”一下
- 13.为什么表数据删掉一半,表文件大小不变?
- 14.count(*)这么慢,我该怎么办?
- 15.答疑文章(一):日志和索引相关问题
- 16.“order by”是怎么工作的?
- 17.如何正确地显示随机消息
- 18.为什么这些SQL语句逻辑相同,性能却差异巨大?
- 19.为什么我只查一行的语句,也执行这么慢?
- 20.幻读是什么,幻读有什么问题?
- 21.为什么我只改一行的语句,锁这么多?
- 22.MySQL有哪些“饮鸩止渴”提高性能的方法?
- 23. MySQL是怎么保证数据不丢的?
- 24.MySQL是怎么保证主备一致的?
- 25.MySQL是怎么保证高可用性的?
- 26.备库为什么会延迟好几个小时?
- 27.主库出问题了,从库怎么办?
- 28.读写分离有哪些坑?
- 29.如何判断一个数据库是不是出问题了?
- 30.答疑文章(二):用动态的观点看加锁
- 31.误删数据后除了跑路,还能怎么办?
- 32.为什么还有kill不掉的语句?
- 33.我查这么多数据,会不会把数据库内存打爆?
- 34.到底可不可以使用join?
- 35.join语句怎么优化?
- 36.为什么临时表可以重名
- 37.什么时候会使用内部临时表?
- 38.都说InnoDB好,那还要不要使用Memory引擎?
- 39.自增主键为什么不是连续的?
- 40.insert语句的锁为什么这么多?
- 41.怎么最快地复制一张表
- 42.grant之后要跟着flush privileges吗?
- 43.要不要使用分区表?
- 44.答疑文章(三):说一说这些好问题
- 45.自增id用完怎么办?
00.问题分类
- 快照读,未提交读,提交读,可重复读,串行化
- 脏读,不可重复度,幻读
- 全局锁、表锁、行锁(读、写锁),间隙锁,next-key lock
- 索引:主键索引,覆盖索引,唯一索引,普通索引
- DDL
01.一条SQL查询语句是如何执行的?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F1uyIPXc-1637156130482)(https://i.loli.net/2021/09/06/G8zsINRKHTe6Ckg.png)]
02.一条SQL更新语句是如何执行的?
- WAL技术(Write-Ahead Logging):先写日志,再写磁盘
- redolog(重做日志):
- 物理日志
- 循环写的,空间会用完
- InnoDB引擎特有的
- 保证crash-safe能力
- innodb_flush_log_at_trx_commit = 1:每次事务的 redo log 都直接持久化到磁盘
- binlog(归档日志):
- 逻辑日志
- 可追加写的,不会覆盖
- MySQL的Server层实现的,所有引擎都可以
- sync_binlog = 1:每次事务的 binlog 都持久化到磁盘
- 保证数据库的一致性:两阶段提交,必须保证两份日志相同,不能一个成功一个失败
03.事务隔离:为什么你改了我还看不见?
- 事务:ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性)
- 事务并发的问题:
- 脏读:A修改数据还未提交,B读了原来的数据
- 不可重复读:A查询两次,B中间删除了数据,A两次读的不一样
- 幻读:A查询数据,B插入了数据,A再读发现了插入的新数据,出现幻觉
- 隔离级别:
- 未提交读RU:事务还没提交,其他事务能看到变更
- 提交读RC:只有事务提交时,其他事务才能看到变更
- 可重复读RR:对一个数据读取多次时相同的
- 串行化:加锁,后一个事务必须等前一个事务执行完成,才能继续
- 配置方式:启动参数 transaction-isolation
- InnoDB:
- 默认隔离级别:REPEATABLE-READ(可重读)
- 使用Next-Key Lock 锁算法,避免幻读的产生
- 事务隔离的实现:MVCC:多版本并发控制,作用是让事务在并发发生时,在一定隔离级别前提下,可以保证在某个事务中能实现一致性读,也就是该事务启动时根据某个条件读取到的数据,知道事务结束时,再次执行相同条件,还是读到同一份数据,不会发生变化。
- 事务的启动方式:
- 显式启动begin 或 start transaction,提交commit,回滚rollback。
- set autocommit=1, 启动事务;事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。
04.深入浅出索引(上)
- 常见索引模型:哈希表、有序数组、搜索树
- InnoDB中的索引模型:
- B+树
- 索引类型:
- 主键索引/聚簇索引:叶子节点存整行数据、只要搜索主键即可拿到数据
- 非主键索引/二级索引:叶子节点存主键的值、先在二级索引里拿到主键值,在到主键索引树搜索一次(回表)
- 从性能和存储空间方面考量,自增主键往往是更合理的选择
- 索引维护:页分裂、页合并
05.深入浅出索引(下)
- 覆盖索引:一个索引包含了满足查询语句中字段与条件的数据
- 最左前缀原则:
- B+树这种索引结构,可以利用索引的"最左前缀"来定位记录
- 最左前缀可以是联合索引的最左N个字段,也可以是字符串索引的最左M个字符
- 加速检索
- 组合/联合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并
- 索引下推:MySQL5.6及之后在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数
06.全局锁和表锁:给表加个字段怎么有这么多阻碍
- 全局锁:
- 使用场景:做全库逻辑备份
- 加全局读锁命令:Flush tables with read lock(FTWRL)
- 整个库都只读的风险
- 如果在主库备份,在备份期间不能更新,业务停摆
- 如果在从库备份,备份期间不能执行主库同步的binlog,导致主从延迟
- 官方自带逻辑备份工具:
- mysqldump
- mysqldump使用参数–single-transaction的时候,会启动一个事务,确保拿到一致性视图。而由于MVCC的支持,这个过程中数据是可以正常更新的
- 有了这个功能,还要FTWRL干什么?:一致性读是好,但前提是引擎要支持这个隔离级别。(MyISAM是不支持事务的引擎);single-transaction 方法只适用于所有的表使用事务引擎的库。
- 既然要全库只读,为什么不使用 set global readonly=true而用FWTRL?:
- 一是,在有些系统中,readonly 的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。因此,修改 global 变量的方式影响面更大,我不建议你使用。
- 二是,在异常处理机制上有差异。如果执行 FTWRL 命令之后由于客户端发生异常断开,那么 MySQL 会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为 readonly 之后,如果客户端发生异常,则数据库就会一直保持 readonly 状态,这样会导致整个库长时间处于不可写状态,风险较高。
- 表级锁:
- 一般是数据库引擎不支持行锁时候会用(MyISAM不支持行锁)
- 类别:表锁、元数据锁(meta data lock, MDL)
- 语法:
- 上锁:lock tables 表名 read/write
- 释放:可以用unlock tables主动释放锁,也可以在客户端断开的时候自动释放。
- MDL:
- 不需要显式使用,在访问一个表的时候会被自动加上
- 作用:保证读写的正确性
- 在对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁。
- 读锁之间不互斥。读写锁之间,写锁之间是互斥的,用来保证变更表结构操作的安全性
- MDL 会直到事务提交才会释放,在做表结构变更的时候,一定要小心不要导致锁住线上查询和更新。
07.行锁功过:怎么减少行锁对性能的影响?
- 行锁:MyISAM 引擎就不支持行锁
- 两阶段锁:
- 在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议(加锁阶段、解锁阶段)。
- 解决冲突方案:
- 最可能影响并发度的锁尽量往后放
- 先进行插入再进行更新(14.问题)
- 死锁:
- 并发系统中不同线程出现循环资源依赖,等待别人释放资源,均陷入无限等待的状态
- 解决:
- 直接进入等待,直到超时。超时时间可以通过innodb_lock_wait_timeout设置(默认50s)
- 发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。
- 死锁检测:
- innodb_deadlock_detect
- 缺点:默认值是on,消耗大量CPU资源
- 解决性能问题
- 如果你能确保这个业务一定不会出现死锁,可以临时把死锁检测关掉。
- 控制并发度:相同行的操作在进入引擎之前排队
- 将一行改成逻辑上的多行来减少锁冲突。(例子:放在多条记录上,账户总额等于10个记录的值的总和,增加金额时随机选一条加),但可能业务会复杂。
08.事务到底是隔离还是不隔离的
-
启动事务方式
- 一致性视图是在执行第一个快照读语句时创建的
- 一致性视图是在执行 start transaction with consistent snapshot 时创建的。(立即启动)
-
视图
- 一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。
- InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。
-
InnoDB的一个特点:每个行数据有多个版本,每个版本有自己的row_trx_id,每个事务或者语句有自己的一致性视图。根据row_trx_id和一致性视图确定可见性。
-
可重复读的能力怎么实现:
- 核心:一致性读(InnoDB在RR和RC下处理SELECT请求的默认模式。)
- 查询数据(一致性读)(select):
- 自己的最新的更新总是可见
- 版本未提交:不可见
- 版本已提交,但是是在视图创建后提交的:不可见
- 版本已提交,而且是在视图创建前提交的:可见。
- 查询(变成当前读的方式):select 后面加lock in share mode 或 for update
- 更新数据(当前读)(update/insert):
- 更新数据都是先读后写的,而这个读,只能读当前的值(别的版本提交了的,不管之前之后),称为“当前读”(current read)
- 如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。
-
读提交区别:
- 对于可重复读,查询只承认在事务启动前就已经提交完成的数据
- 对于读提交,查询只承认在语句启动前就已经提交完成的数据;
09.普通索引和唯一索引,该怎么选择?
- 普通索引
- 概念:创建索引时,不附加任何限制条件
- 查询:查找到第一个满足条件的记录后,继续向后遍历,直到不满足
- 更新:将数据页从磁盘读入内存,更新数据页
- 唯一索引
- 概念:创建索引时,限制所i你的值必须是唯一的
- 查询:查找到第一个满足条件的记录后,直接停止继续检索
- 更新:将数据页从磁盘读入内存,判断是否唯一,再更新数据页
- change buffer
- 作用:当需要更新一个数据页,如果数据页在内存种就直接更新,如果不在,InnoDB先将操作缓存在change buffer中。下次需要访问数据页的时候,将数据页读入内存,执行change buffer中的与这个页有关的操作
- 性质:是可以持久化的数据,在内存中有拷贝,也会被写入到磁盘上
- merge:将change buffer中的操作应用到原数据页上,得到最新结果的过程
- 好处:将数据从磁盘读入内存涉及随机IO的访问,是数据库里面成本最高的操作之一。
change buffer 因为减少了随机磁盘访问 - 使用场景:写多读少的业务
- 不适应的场景:一个业务的更新模式是写入之后马上会做查询
- 一点区别:
- redo log主要节省的是随机写磁盘的IO消耗(转成顺序写)
- hange buffer主要节省的则是随机读磁盘的IO消耗
- 索引选择:尽可能使用普通索引。
10.MySQL为什么有时候会选错索引
- 创建索引:
- CREATE TABLE时候:CREATE TABLE( …, INDEX(列名))
- CREATE INDEX时候:CREATE INDEX 索引名 ON 表名 (列名)
- ALTER TABLE时候:ALTER TABLE 表名 ADD INDEX 索引名 列名
- 原因:
- Server层的优化器决定最佳索引来检索数据
- 用各种因素综合评估:是否使用临时表、是否排序、扫描的行数多少、回表的次数等。
- 修正统计信息:analyze table 表名
- 方案
- 使用force index强制选择一个索引
- 修改语句,引导MySQL使用我们期望的索引
- 创建一个更合适的索引,或者删掉误用的索引
11.怎么给字符串字段加索引
- 完整索引:比较占空间
- 前缀索引:节省空间,但会增加查询扫描次数,并且不能使用覆盖索引(即使已经包含了检索的全部信息,InnDB还是要回到id索引再查一下)
- 倒序索引:存的时候倒序存,查询时候用reverse(),不支持范围查询,只支持等值查询
- hash字段索引:查询性能稳定,但有额外的存储和计算消耗,不支持范围索引,只支持等值查询
12.为什么我的MySQL会“抖”一下
- 脏页:内存数据页和磁盘数据页内容不一致的时候
- 干净页:内存数据写入到磁盘后,内存和磁盘上的数据页的内容一致
- “抖一下”:刷脏页
- 四种场景:
- redolog写满了,停止更新操作,checkpoint向前推,对应的脏页flush到磁盘上
- 对应的系统内存不足,淘汰一些数据页,如果是脏页,就先将脏页写到磁盘
- InnoDB用缓冲池(buffer pool)管理内存,缓冲池中的内存页有三种状态
- 未使用的、使用了并且是干净页、使用了并且是脏页的
- MySQL空闲的时候,刷脏页
- MySQL正常关闭的时候把内存的脏页都flush到磁盘上
- InnoDB刷脏页的控制策略
- innodb_io_capacity:InnoDB所在主机的IO能力
- innodb_max_dirty_pages_pct:脏页比例上限
- redo log写盘速度
- innodb_flush_neighbors:值为1表示当前数据页旁边的也是脏页,就一起刷新,0时候表示自己刷自己的
13.为什么表数据删掉一半,表文件大小不变?
-
表结构和和数据:
- MySQL8.0以前,表结构是存在以.frm为后缀的文件里
- MySQL8.0,已经允许把表结构定义放在系统数据表里了
-
innodb_file_per_table:
- ON:每一个InnoDB表数据存储在一个以.idb为后缀的文件中
- OFF:表的数据放在共享表空间,也就是跟数据字典放在一起
- 推荐ON
-
数据删除流程:
- 删掉一个记录,只会把这个记录标为删除,单磁盘大小并不会缩小(只限于符合范围条件的数据复用)
- 删掉一整页,整个数据页可以被复用(任何位置都可以复用)
- 插入数据可能造成数据页分裂,造成空洞
-
重建表:
- 空间收缩,去掉空洞
- alter table A engine = InnoDB(MySQL自动完成转存数据、交换表名、删除旧表的操作)
-
Online DDL:
-
保证重建表时候其他操作不被丢失
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X1ND4YZ1-1637156130485)(https://i.loli.net/2021/08/30/C7P5eOKiYaZhrEX.png)]
-
-
Online 和 inplace:
- DDL过程如果是Online的,就一定是inplace的
- inplace的DLL不一定是Online的(添加全文索引和空间索引就属于这种情况)
-
重建表的区别:
- alter table
- analyze table:其实不是重建表,对表的索引信息作重新统计,没有修改数据,过程中加了MDL锁(元数据锁)
- optimize table(recreate + analyze)
14.count(*)这么慢,我该怎么办?
-
count(*)的实现方式
-
MyISAM把一个表的总行数存在了磁盘上,直接返回个数(如果加了where条件,MyISAM也不能返回的这么快)
-
InnoDB需要把数据一行一行地从引擎里面读出来,然后累积计数
-
因为MVCC(多版本并发控制),InnoDB表只能一行一行读
-
InnoDB的优化:选择最小的索引树遍历得到结果(普通索引树比主键索引树小)
-
show table status
该命令中有一个结果TABLE_ROWS能显示有多少行,但是不准确
-
-
用缓存系统保存计数
- 用一个Redis服务保存总行数
- 在并发系统中,无法精确控制不同线程的执行时刻,可能导致数据还没插入,计数器已经+1,或者计数器还没+1,数据已经插入
-
在数据库保存计数
- 将计数直接放到数据库(InnoDB)里单独的一张计数表中,可以解决崩溃丢失恢复数据和计数不精确的问题
-
不同的count用法
- count():如果count函数的参数不是NULL,累计值就+1
- count(主键id):遍历整张表,把每一行的id取出来,返回给server层。拿到id后,判断不可能为空,就按行累加
- count(1):遍历整张表,但不取值,对于返回的每一行,放一个数字1进去,判断不可能为空,就按行累加
- count(字段):如果字段定义为not null,一行行地从记录里面读出这个字段,判断不能为 null,按行累加;如果这个“字段”定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加。
- count(*):并不会把全部字段取出来,而是专门做了优化,不取值。count(*) 肯定不是 null,按行累加。
- count(*) ≈ count(1) > count(主键id) > count(字段)
15.答疑文章(一):日志和索引相关问题
16.“order by”是怎么工作的?
-
全字段排序
-
rowid排序
-
全字段排序和rowid 排序对比
- MySQL担心内存太小会影响排序效率,才会使用rowid排序算法,一次排序更多行,但是需要再回到原表中取数据
- MySQL认为内存足够大,优先选择全字段排序,把所有需要的字段放到sort_buffer中,排序后直接返回查询结果
- MySQL设计思想:如果内存够,就要多利用内存,尽量减少磁盘访问(rowid排序会要求回表造成磁盘读)
-
原本就是按照name排序
-
覆盖索引
17.如何正确地显示随机消息
order by rand() limit 3 //随机选取三条记录
-
内存临时表
- 使用了内存临时表,内存临时表排序的时候使用了rowid排序方法
- rowid排序中关于pos的解释:
- 对于有主键的 InnoDB 表来说,这个 rowid 就是主键 ID;
- 对于没有主键的 InnoDB 表来说,这个 rowid 就是由系统生成的;
- MEMORY 引擎不是索引组织表。在这个例子里面,你可以认为它就是一个数组。因此,这个 rowid 其实就是数组的下标。
-
磁盘临时表
- 临时表大小超过了 tmp_table_size,那么内存临时表就会转成磁盘临时表。
- 使用磁盘临时表的时候,对应的就是一个没有显式索引的 InnoDB 表的排序过程。
- 不用到临时文件(没有用到归并排序算法),而是用了优先队列排序算法。
- 若堆的大小是 1000 行 (name,rowid)时候,超过了 sort_buffer_size 大小,只能使用归并排序算法。
-
随机排序方法
- 方法1:取得这个表的主键 id 的最大值 M 和最小值 N;用随机函数生成一个最大值到最小值之间的数 X = (M-N)*rand() + N;取不小于 X 的第一个 ID 的行。(缺点:id为1,2,40000时有空洞,选择不同行的概率不同,不是真正随机)
- 方法2:取得整个表的行数,并记为 C;取得 Y = floor(C * rand()),floor 函数在这里的作用,就是取整数部分;再用 limit Y,1 取得一行。
18.为什么这些SQL语句逻辑相同,性能却差异巨大?
-
条件字段函数操作
-
对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能,做全表扫描。
mysql> select count(*) from tradelog where month(t_modified)=7; #加了 month() 函数操作,MySQL 无法再使用索引快速定位功能,而只能使用全索引扫描。
-
-
隐式类型转换
-
字符串和数字做比较的话,是将字符串转换成数字。
mysql> select * from tradelog where tradeid=110717; #两条语句相同 mysql> select * from tradelog where CAST(tradid AS signed int) = 110717;
-
-
隐式字符编码转换
-
#tradelog 称为驱动表,把 trade_detail 称为被驱动表,把 tradeid 称为关联字段 #这两个表的字符集不同,一个是 utf8,一个是 utf8mb4,所以做表连接查询的时候用不上关联字段的索引 select d.* from tradelog l, trade_detail d where d.tradeid=l.tradeid and l.id=2; #这样修改会导致加了函数操作走全表扫描 select * from trade_detail where CONVERT(traideid USING utf8mb4)=$L2.tradeid.value; #方法1:把 trade_detail 表上的 tradeid 字段的字符集也改成 utf8mb4,这样就没有字符集转换的问题了。 alter table trade_detail modify tradeid varchar(32) CHARACTER SET utf8mb4 default null; #方法2:我主动把 l.tradeid 转成 utf8,就避免了被驱动表上的字符编码转换,这次索引走对了。 select d.* from tradelog l , trade_detail d where d.tradeid=CONVERT(l.tradeid USING utf8) and l.id=2;
-
19.为什么我只查一行的语句,也执行这么慢?
-
查询长时间不返回:
- 等MDL锁:有一个线程正在表t上请求或者持有MDL写锁,把select语句堵住了
- 等flush:flush命令被别的语句堵住了,flush语句又堵住了select语句
- 等行锁:其他事务启动占有写锁,还不提交
-
查询慢:
- 坏查询不一定是慢查询
- 加锁用RR和不加锁用RC导致
20.幻读是什么,幻读有什么问题?
-
幻读是什么?
- 幻读是指在同一个事务中,存在前后两次查询同一个范围的数据,但是第二次查询却看到了第一次查询没看到的行
-
幻读出现场景:
- 事务的隔离级别为可重复读,且是当前读
- 幻读仅专指新插入的行
-
幻读代理的问题:
- 对行锁语义的破坏
- 破坏了数据一致性
-
怎么避免幻读?
- 存储引擎采用加间隙锁的方式来避免出现幻读
-
为什么会出现幻读?
- 行锁只能锁定存在的行,针对新插入的操作没有限定
-
间隙锁(Gao Lock)
-
间隙锁,是专门用于解决幻读这种问题的锁,它锁了行与行之间的间隙,能够阻塞新插入的操作
-
间隙锁的问题:降低并发度,可能导致死锁。
-
间隙锁之间是不冲突的,间隙锁会阻塞插入操作。另外,间隙锁在可重复读级别下才是有效的
-
锁定区域:开区间
-
-
临键锁(next-key lock)
- 间隙锁和行锁组合起来
- 锁定区域:根据索引会形成一个个左开右闭的一个区间,根据查询的条件其所在的区间,并且包括其后的区间。
21.为什么我只改一行的语句,锁这么多?
- 间隙锁在可重复读隔离级别下才有效
- 总结的加锁规则里面,包含了两个“原则”、两个“优化”和一个“bug”:
- 原则 1:加锁的基本单位是 next-key lock。希望你还记得,next-key lock 是前开后闭区间。
- 原则 2:查找过程中访问到的对象才会加锁。
- 优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
- 优化 2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。
- 一个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
22.MySQL有哪些“饮鸩止渴”提高性能的方法?
- 短连接风暴
- max_connections:控制一个 MySQL 实例同时存在的连接数的上限
- 方法1:先处理掉那些占着连接但是不工作的线程。(优先断开事务外空闲太久的连接,再考虑断开事务内空闲太久的连接。)
- 方法2:减少连接过程的消耗。(让数据库跳过权限验证阶段:重启数据库,并使用–skip-grant-tables 参数启动,–skip-grant-tables不安全)
- 慢查询性能问题
- 索引没有设计好:alter table,主备库的切换
- SQL 语句没写好:query_rewrite 功能(把输入的一种语句改写成另外一种模式)
- MySQL 选错了索引:加force index
- QPS 突增问题
23. MySQL是怎么保证数据不丢的?
-
binlog的写入机制:
-
写入逻辑:事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中,并清空binlog cache。
-
一个事务的 binlog 是不能被拆开的,因此不论这个事务多大,也要确保一次性写入。
-
binlog_cache_size:超过大小,binlog cache中的部分内容要暂存到磁盘
-
write:把日志写入到page cache;fsync:数据持久化到磁盘
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BcO0wpXS-1637156130495)(https://i.loli.net/2021/08/26/kTc5W9p6MxhGLrJ.png)]
-
sync_binlog:
- = 0:只write,不fsync
- = 1:每次都write、fsync
- = N:wirte累计N个事务后fsync(常见设置100~1000)
-
-
redo log的写入机制:
-
写入逻辑:事务在执行过程中,生成的 redo log 是要先写到 redo log buffer 的。
-
三种状态:
-
存在redo log buffer中,在内存中
-
write,在page cache里
-
持久化到磁盘,对应hard disk
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UxuzJywp-1637156130496)(https://i.loli.net/2021/08/26/ZpmJdS8qVQvxYeu.png)]
-
-
innodb_flush_log_at_trx_commit:
- = 0:只停留在redo log buffer
- = 1:直接持久化到磁盘
- = 2:只写到page cache
-
没有提交的事务也可能持久化到磁盘:
- InnoDB 有一个后台线程,每隔 1 秒,就会把redo log buffer ->page cache->磁盘。
- innodb_log_buffer_size的占用空间达到一半(只write没有fsync)
- 并行的事务提交,顺带将这个事务的 redo log buffer 持久化到磁盘
-
-
双1配置:sync_binlog 和 innodb_flush_log_at_trx_commit 都 = 1,意味着一个事务完整提交前等待两次刷盘
-
组提交:幅度降低磁盘的 IOPS 消耗
- log sequence number(LSN)日志逻辑序列号:单调递增,对应redo log的写入点,每次值加rodo log长度,写盘的时候会导致前面的全部持久化到磁盘
- 提升binlog组提交效果:
- binlog_group_commit_sync_delay:延迟多少微秒后才调用 fsync
- binlog_group_commit_sync_no_delay_count:累积多少次以后才调用 fsync
- 两个满足一个调用fsync
-
小结:提升MySQL在IO上的瓶颈限制:
- binlog_group_commit_sync_delay、binlog_group_commit_sync_no_delay_count减少写盘次数
- sync_binlog设置大一些
- innodb_flush_log_at_trx_commit = 2
24.MySQL是怎么保证主备一致的?
-
主备切换流程:M-S结构
- readonly 设置对超级 (super) 权限用户是无效的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YOVAxTSP-1637156130497)(https://i.loli.net/2021/08/26/jCrfN5SGE67ITvK.png)]
-
主备流程图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rLCuDLhI-1637156130498)(https://i.loli.net/2021/08/26/O3Ex2IPC8yeM9w4.png)] -
binlog的三种格式:
- statement:SQL 语句的原文
- row:非常清楚的记录下每一行数据修改的细节,非常容易理解
- mixed:MySQL 自己会判断这条 SQL 语句是否可能引起主备不一致,如果有可能,就用 row 格式,否则就用 statement 格式。
-
双M结构:多了一条线
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H7lZXf7g-1637156130499)(https://i.loli.net/2021/08/26/m9yA1PNFEWH4iob.png)]
-
解决循环复制问题:
- 规定两个库的 server id 必须不同,如果相同,则它们之间不能设定为主备关系;
- 一个备库接到 binlog 并在重放的过程中,生成与原 binlog 的 server id 相同的新的 binlog;
- 每个库在收到从自己的主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志。
25.MySQL是怎么保证高可用性的?
-
主备延迟
- seconds_behind_master:表示当前备库延迟了多少秒
- 表现:备库消费中转日志(relay log)的速度,比主库生产binlog的速度要慢
-
主备延迟的原因:
- 备库性能比主库差
- 备库压力大
- 大事务影响:一次性用delete语句删太多;大表DDL
- 备库的并行复制能力
-
可靠性优先策略:SBM(seconds_behind_master),延迟太高两边都只读
- 缺点:可能导致不可用时间太长
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3UFCSBop-1637156130500)(https://i.loli.net/2021/08/27/FzMHdOBLDEt96vj.png)]
-
可用性优先策略
- 不等主备数据同步,直接切换到B库
- 缺点:可能会导致数据不一样(使用row格式更容易被发现,mixed或者statement不容易被发现)
26.备库为什么会延迟好几个小时?
- 影响主备复制的两个因素:主库各种锁、备库单线程复制影响
- 多线程复制的两个要求:
- 更新同一行的两个事务,必须分发到一个worker中
- 同一个事务不能被拆开,必须放到同一个事务中
- MySQL5.5不支持并行复制
- 按表分发策略:
- 如果事务涉及的表和所有worker里的表都不冲突,分配给最闲的worker
- 如果和多于一个workder冲突,进入等待
- 如果只和一个worker冲突,分配给存在冲突的worker
- 按行分发策略:
- 如果两个事务没有更新相同的行,在备库上可以并行执行
- 要求binlog格式必须是row
- 相比按表分发,要消耗更多的计算资源
- MySQL5.6的并行复制策略
- 按库并行:只需要库名、不要求binlog格式
- MariaDB的并行复制策略
- 每一组相同的事务有一个相同的commit_id,commit_id直接写到binlog里面,备库中相同commit_id的事务分发到多个worker执行,全部执行完再取下一批
- 系统吞吐量不够、容易被大事务拖后腿
- MySQL5.7的并行复制策略
- 同时处于prepare状态的事务,在备库执行时可以并行
- 处于prepare状态的事务,与处于commit状态之间的事务,也可以并行的
- slave-parallel-type:DATABASE(5.6版本策略) LOGICAL_CLOCK(MariaDB基础上再优化)
- MySQL5.7.22的并行复制策略
- binlog-transaction-dependency-tracking:
- COMMIT_ORDER:根据同时进入prepart和commit判断是否可以并行
- WRITESET:计算哈希值,判断事务没有操作相同的行,可以并行
- WRITESET_SESSION:多了一个约束,备库必须和主库的执行顺序相同
- 优点:节省计算量不需要解析binlog;省内存不需要扫全部的binlog;不要求binlog格式
- binlog-transaction-dependency-tracking:
27.主库出问题了,从库怎么办?
-
一主多从
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bzaStZIf-1637156130501)(https://i.loli.net/2021/08/28/6wbECuvHMkXftUn.png)]
-
基于位点的主备切换
-
把B设置成A’的从库时需要执行change master:
CHANGE MASTER TO # A'信息 MASTER_HOST=$host_name MASTER_PORT=$port MASTER_USER=$user_name MASTER_PASSWORD=$password # 同步位点 MASTER_LOG_FILE=$master_log_name 主库对应的文件名 MASTER_LOG_POS=$master_log_pos 主库的日志偏移量
-
同步方法:
- 等待新主库 A’把中转日志(relay log)全部同步完成;
- 在 A’上执行 show master status 命令,得到当前 A’上最新的 File 和 Position;
- 取原主库 A 故障的时刻 T;
- 用 mysqlbinlog 工具解析 A’的 File,得到 T 时刻的位点。
-
存在的问题:A执行完一个insert,binlog也已经给了A和B,传完瞬间A掉电,从这个位点同步,B会发生主键冲突
- 方法1:主动跳过一个事务。set global sql_slave_skip_counter=1; start slave;
- 方法2:通过设置 slave_skip_errors 参数,直接设置跳过指定的错误。
-
-
GTID(MySQL5.6)
-
概念:Global Transaction Identifier、全局事务 ID;一个事务在提交的时候生成的,是这个事务的唯一标识。
-
格式
GTID=server_uuid:gno GTID=source_id:transaction_id
-
启动方式:加上gtid_mode=on 和 enforce_gtid_consistency=on
-
每个事务都对应一个GTID,每个 MySQL 实例都维护了一个 GTID 集合
-
-
基于GTID的主备切换
-
设置从库语法:
CHANGE MASTER TO MASTER_HOST=$host_name MASTER_PORT=$port MASTER_USER=$user_name MASTER_PASSWORD=$password #1表示用的是GTID协议 master_auto_position=1
-
从库会把自己的GTID集合传给切换的主库,主库会计算差集,然后把不同步的给从库
-
在基于 GTID 的主备关系里,系统认为只要建立主备关系,就必须保证主库发给备库的日志是完整的。因此,如果实例 B 需要的日志已经不存在,A’就拒绝把日志发给 B
-
基于位点的协议,是由备库决定的,备库指定哪个位点,主库就发哪个位点,不做日志的完整性判断。
-
-
GTID 和在线 DDL
-
在实例 X 上执行 stop slave。
-
在实例 Y 上执行 DDL 语句。注意,这里并不需要关闭 binlog。
-
执行完成后,查出这个 DDL 语句对应的 GTID,并记为 server_uuid_of_Y:gno。
-
到实例 X 上执行以下语句序列:
set GTID_NEXT="server_uuid_of_Y:gno"; begin; commit; set gtid_next=automatic; start slave;
-
保证了既可以让实例 Y 的更新有 binlog 记录,同时也可以确保不会在实例 X 上执行这条更新。
-
28.读写分离有哪些坑?
-
过期读:由于主从可能存在延迟,客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,就有可能读到刚刚的事务更新之前的状态。
-
强制走主库方案:顾名思义
-
sleep 方案:读从库前sleep一下
-
判断主备无延迟方案:
- 判断 seconds_behind_master == 0?
- 对比位点确保主备无延迟
- 对比 GTID 集合确保主备无延迟
- 问题:判断的是“备库收到的日志都执行完成了”,但有部分备库还没收到,导致过期读
-
配合 semi-sync 方案:
- 设计方案:事务提交的时候,主库把 binlog 发给从库;从库收到 binlog 以后,发回给主库一个 ack,表示收到了;主库收到这个 ack 以后,才能给客户端返回“事务完成”的确认
- 问题:
- semi-sync + 对比位点仅适合一主一从;一主多从的时候,在某些从库执行查询请求会存在过期读的现象;
- 在持续延迟的情况下,可能出现过度等待的问题。
-
等主库位点方案:
-
语句:
select master_pos_wait(file, pos[, timeout]); # 超时返回-1,备库异常返回NULL,正常返回>=0
-
过程:
- 事务更新之后从主库上得到位点
- 选定一个从库执行语句
- 执行上述语句,根据返回值,如果>=0在从库上查询
- 否则在主库上查询
-
问题:压力会到主库上。
-
-
等 GTID 方案:
-
语句:
select wait_for_executed_gtid_set(gtid_set, 1); # 超时返回1,正常返回0
-
过程:
- 事务更新之后从返回包直接获取这个事务的 GTID,记为 gtid1
- 选定一个从库执行查询语句
- 在从库上执行 上述语句;如果返回值是 0,则在这个从库执行查询语句
- 否则,到主库执行查询语句
-
29.如何判断一个数据库是不是出问题了?
- select 1判断
- select 1 成功返回,只能说明这个库的进程还在,并不能说明主库没问题
- innodb_thread_concurrency:nnoDB 的并发线程上限,通常为64-128,0表示没有上限
- 并发连接和并发查询,应该关注并发查询(影响CPU)
- ,在线程进入锁等待以后,并发线程的计数会-1
- 查表判断
- 建一个表,只放一行数据,定期执行
- 问题:更新事务要写 binlog,binlog 磁盘的空间占用率达到 100%,所有的更新语句和事务提交的 commit 语句就都会被堵住。但是,系统这时候还是可以正常读数据的。
- 更新判断
- 建一个表,常见的是放一个timestamp字段,表示最后一次执行检测的时间
- 问题:机器的I/O已经100%,但刚好健康检查的sql拿到了资源,成功返回了
- 内部统计
- performance_schema 库里的file_summary_by_event_name 表统计了每次IO请求的时间
- 通过 MAX_TIMER 的值判断是否出问题,设置阈值,超过属于异常
- 推荐方案:优先考虑 update 系统表,然后再配合增加检测 performance_schema 的信息
30.答疑文章(二):用动态的观点看加锁
31.误删数据后除了跑路,还能怎么办?
-
误删数据分类:
- 使用 delete 语句误删数据行
- 使用 drop table 或者 truncate table 语句误删数据表
- 使用 drop database 语句误删数据库
- 使用 rm 命令误删整个 MySQL 实例
-
误删行:
- 用 Flashback 工具通过闪回把数据恢复回来(原理:修改binlog内容,拿回原库重放),确保 binlog_format=row 和 binlog_row_image=FULL
- 不建议在主库上执行这些操作
- 事前预防:
- sql_safe_updates = on
- 代码上线前,必须经过 SQL 审计
-
误删库/表:
-
取全量备份,全量备份时间之后的binlog恢复
-
方法1:mysqlbinlog 工具解析出的 binlog 文件应用到临时库:
-
加速数据恢复:在使用 mysqlbinlog 命令时,加上一个–database 参数,用来指定误删表所在的库
-
跳过 12 点误操作的那个语句的 binlog:非GTID模式:先用–stop-position 参数执行到误操作之前的日志,再用–start-position 从误操作之后的日志继续执行;GTID模式:执行 set gtid_next=gtid1;begin;commit
-
还是不够快的原因:mysqlbinlog 工具并不能指定只解析一个表的日志;只能是单线程
-
-
方法2:在用备份恢复出临时实例之后,将这个临时实例设置成线上备库的从库(让临时库只同步误操作的表,可以用上并行复制技术)
-
-
延迟复制备库:
- CHANGE MASTER TO MASTER_DELAY = N :指定备库持续保持跟主库有N 秒的延迟
-
预防误删库 / 表的方法:
- 账号分离。这样做的目的是,避免写错命令
- 制定操作规范。这样做的目的,是避免写错要删除的表名
-
rm 删除数据:
- 只是删掉了其中某一个节点的数据的话,HA 系统会选出一个新的主库,从而保证整个集群的正常工作。在这个节点上把数据恢复回来,再接入整个集群
- 尽量把你的备份跨机房,或者最好是跨城市保存。
32.为什么还有kill不掉的语句?
-
两个kill命令:
- kill query + 线程 id:终止这个线程中正在执行的语句
- kill connection + 线程 id:表示断开这个线程的连接,show processlist显示killed,后面的执行流程还是要走kill query
-
收到kill后,线程怎么做:
- 把 session B 的运行状态改成 THD::KILL_QUERY(将变量 killed 赋值为 THD::KILL_QUERY)
- 给 session B 的执行线程发一个信号,session处于锁等待状态,信号让session退出等待处理1中状态
- 注:一个语句执行过程中有多处“埋点”,在这些“埋点”的地方判断线程状态,如果发现线程状态是 THD::KILL_QUERY,才开始进入语句终止逻辑;如果处于等待状态,必须是一个可以被唤醒的等待,否则根本不会执行到“埋点”处
-
kill无效情况:
- 线程没有执行到判断线程状态的逻辑
- 终止逻辑耗时较长(三种场景:超大事务执行期间;大查询回滚;DDL命令执行到最后阶段删除临时文件,受IO资源影响)
-
Ctrl+C也不能直接终止,MySQL 是停等协议,启动另一个连接,发送kill query 命令
-
关于客户端的两个误解
- 如果库里面的表特别多,连接就会很慢
- 原因:构建本地哈希表时候慢
- 解决:在连接命令中加上 -A/-quick,跳过自动补全
- –quick参数:
- 加上采用不缓存服务器返回结果的方式,读一条处理一条,会让服务端被阻塞,变慢
- 让客户端变得更快
- 如果库里面的表特别多,连接就会很慢
33.我查这么多数据,会不会把数据库内存打爆?
-
全表扫描对Server层的影响:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ydEpvZV2-1637156130502)(https://i.loli.net/2021/08/31/QeAghKSqcwauN6d.jpg)]
- 过程(防止一次性读一整张表内存爆炸):获取每行写到nex_buffer中(大小由net_buffer_length定义),写满发送出去;如果发送成功清空next_buffer;如果发送函数返回 EAGAIN 或 WSAEWOULDBLOCK,就表示本地网络栈(socket send buffer)写满了,进入等待。
- MySQL 是“边读边发的”
- mysql_store_result:把结果保存在客户端本地,直接把所有结果读过来;
- mysql_use_result:客户端一行一行的从Server读取数据,如果每行数据都有业务处理逻辑的话Server就要等待,会造成长事务。
- Sending to client:表示服务器端的网络栈写满了,仅当一个线程处于“等待客户端接收结果”的状态才显示
- Sending data:意思只是“正在执行”
-
全表扫描对InnoDB的影响:
-
WAL 机制里 Buffer Pool 起到了加速更新的作用,实际上还能加速查询(事务提交的时候内存数据页是最新的,查询可以直接读内存页)
-
内存命中率:
-
Buffer Pool 对查询的加速效果,依赖于一个重要的指标,即:内存命中率
-
Buffer pool hit rate:一个系统当前的 BP 命中率
-
innodb_buffer_pool_size (控制InnoDB Buffer Pool 的大小):建议设成可用物理内存的 60%~80%
-
-
InnoDB 内存管理用LRU算法(淘汰最久未使用的数据):
-
InnoDB改进按照 5:3 的比例把整个 LRU 链表分成了 young 区域和 old 区域
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lWEcsAx8-1637156130504)(https://i.loli.net/2021/08/31/XMeawJHkbihrgn4.png)]
-
具体方法:如果存在于链表,放到head;如果不存在于链表,先放入old区域,如果存在时间超过了1秒(第一次被访问和最后一次被访问的时间间隔大于1秒,说明被频繁访问)移动到thead,小于1秒位置不变
-
-
34.到底可不可以使用join?
- Index Nested-Loop Join:
- 可以使用被驱动表的索引的前提下:驱动表是走全表扫描,而被驱动表是走索引搜索
- 读取驱动表中一行数据,到被驱动表中去查询
- 使用 join 语句,性能比强行拆成多个单表执行 SQL 语句的性能要好;
- 如果使用 join 语句的话,需要让小表做驱动表。
- Simple Nested-Loop Join:
- 被驱动表上没有索引,两个表都走全表扫描
- Block Nested-Loop Join:
- 被驱动表上没有索引
- 把驱动表数据读入join_buffer,再把被驱动表的每一行取处理,和join_buffer中的数据对比,满足条件的返回
- 判断操作在内存中,所以速度快性能好
- join_buffer_size:join_buffer的大小,如果放不下驱动表,就分段放
- 总结1:到底用不用join
- Index Nested-Loop Join算法,可以用上被驱动表的索引,没有问题
- Block Nested-Loop Join算法,扫描行数过多,尤其在大表上的join,扫描被驱动表占用系统资源,这种join尽量不要用
- 总结2:驱动表选大表还是小表
- 小表
35.join语句怎么优化?
-
Multi-Range Read 优化:
- 尽量使用顺序读盘
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CY5Mkn13-1637156130505)(https://i.loli.net/2021/09/01/3UjZHauLQXGPWDd.jpg)]
- read_rnd_buffer_size:read_rnd_buffer 的大小
- set optimizer_switch=“mrr_cost_based=off”:稳定地使用 MRR 优化
-
Batched Key Access:
-
对 NLJ 算法的优化:多了join_buffer可以用上MRR优化
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HumWhQFX-1637156130506)(https://i.loli.net/2021/09/01/AMPF5diOpL6aQ7D.png)]
-
set optimizer_switch=‘mrr=on,mrr_cost_based=off,batched_key_access=on’:使用 BKA 优化算法
-
-
BNL 算法的问题:
- 可能会多次扫描被驱动表,占用磁盘 IO 资源;
- 判断 join 条件需要执行 M*N 次对比(M、N 分别是两张表的行数),如果是大表就会占用非常多的 CPU 资源;
- 导致 Buffer Pool 的热数据被淘汰,影响内存命中率(InnoDB对LRU的优化中,执行BNL的语句超过1s,old和yound区域的内容没有合理的处理)
-
BNL 转 BKA:
- 可以在被驱动表上建索引(建索引也浪费资源,有时候不必要)
- 被驱动表中符号条件的数据放到临时表中,给临时表加上索引,让临时表和驱动表做join
-
扩展 -hash join:
- MySQL还不支持哈希索引
- 实现思路:取驱动表中数据,存入hash结构,获取被驱动表中符号条件的数据,到hash结构的数据表中匹配
36.为什么临时表可以重名
- 内存表和临时表:
- 内存表:使用 Memory 引擎的表,建表语法是 create table … engine=memory。这种表的数据都保存在内存里
- 临时表:可以使用各种引擎类型 。如果是使用 InnoDB或MyISAM 引擎的临时表,写数据写到磁盘上。临时表也可以使用 Memory 引擎。
- 临时表的特性:由于临时表只能被创建它的 session 访问,所以在这个 session 结束的时候,会自动删除临时表
- 临时表的应用:
- 分库分表系统的跨库查询就是一个典型的使用场景[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Aqjfy0N1-1637156130507)(https://i.loli.net/2021/09/02/ey7XId4YpELU1uW.jpg)]
- 没有用到分区字段,去所有的分区里查找满足条件的索引行的思路:在 proxy 层的进程代码中实现排序;把各个分库拿到的数据,汇总到一个 MySQL 实例的一个表中,然后在这个汇总实例上做逻辑操作
- 为什么临时表可以重名?
- 每个表都对应一个 table_def_key
- 普通表: table_def_key 的值是由“库名 + 表名”得到的
- 临时表:table_def_key 在“库名 + 表名”基础上,又加入了“server_id+thread_id”。
- 临时表和主备复制
- 如果binlog_format=row,跟临时表有关的就不会记录到 binlog 里
- 只在 binlog_format=statment/mixed 的时候,binlog 中才会记录临时表的操作
- MySQL 在记录 binlog 的时候,会把主库执行这个语句的线程 id 写到 binlog 中,在备库利用线程 id 来构造临时表
- session A 的临时表 t1,备库的table_def_key:库名 +t1+“M 的serverid”+“session A 的 thread_id”
- session B 的临时表 t1,备库的table_def_key:库名 +t1+“M 的serverid”+“session B 的 thread_id”
37.什么时候会使用内部临时表?
-
union执行流程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZkkVPVc6-1637156130508)(https://i.loli.net/2021/09/02/SmdnweUpbfF4Hkl.jpg)]
-
group by执行流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U7XY3sCn-1637156130509)(https://i.loli.net/2021/09/02/2eXpaEQGIlcrqsx.jpg)]
-
group by 优化方法–索引
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-14C6P9wQ-1637156130510)(https://i.loli.net/2021/09/02/wKshpb3GnFSMr1V.png)]
- 如果确保输入数据有序,计算 group by 的时候,就只需要从左到右,顺序扫描,依次累加:
- 当碰到第一个 1 的时候,已经知道累积了 X 个 0,结果集里的第一行就是 (0,X);
- 当碰到第一个 2 的时候,已经知道累积了 Y 个 1,结果集里的第二行就是 (1,Y);
- InnoDB 的索引,就可以满足这个输入有序的条件。
-
group by 优化方法–直接排序
- 在 group by 语句中加入 SQL_BIG_RESULT 这个提示(hint);告诉优化器:这个语句涉及的数据量很大,请直接用磁盘临时表
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8do9mgf9-1637156130511)(https://i.loli.net/2021/09/02/sVPfgSWNILnor2a.jpg)]
-
MySQL什么时候会使用内部临时表:
- 如果语句执行过程可以一边读数据,一边直接得到结果,是不需要额外内存的,否则就需要额外的内存,来保存中间结果;
- join_buffer 是无序数组,sort_buffer 是有序数组,临时表是二维表结构;
- 如果执行逻辑需要用到二维表特性,就会优先考虑使用临时表。比如我们的例子中,union 需要用到唯一索引约束, group by 还需要用到另外一个字段来存累积计数。
-
group by使用的指导原则:
- 如果对 group by 语句的结果没有排序要求,要在语句后面加 order by null;
- 尽量让 group by 过程用上表的索引,确认方法是 explain 结果里没有 Using temporary 和 Using filesort;
- 如果 group by 需要统计的数据量不大,尽量只使用内存临时表;也可以通过适当调大 tmp_table_size 参数,来避免用到磁盘临时表;
- 如果数据量实在太大,使用 SQL_BIG_RESULT 这个提示,来告诉优化器直接使用排序算法得到 group by 的结果。
38.都说InnoDB好,那还要不要使用Memory引擎?
- 数据组织方式:
- InnoDB 引擎把数据放在主键索引上,其他索引上保存的是主键 id。这种方式,我们称之为索引组织表(Index Organizied Table)
- Memory 引擎采用的是把数据单独存放,索引上保存数据位置的数据组织形式,我们称之为堆组织表(Heap Organizied Table)
- 两个引擎的一些典型不同:
- InnoDB 表的数据总是有序存放的,而内存表的数据就是按照写入顺序存放的;
- 当数据文件有空洞的时候,InnoDB 表在插入新数据的时候,为了保证数据有序性,只能在固定的位置写入新值,而内存表找到空位就可以插入新值;
- 数据位置发生变化的时候,InnoDB 表只需要修改主键索引,而内存表需要修改所有索引;
- InnoDB 表用主键索引查询时需要走一次索引查找,用普通索引查询的时候,需要走两次索引查找。而内存表没有这个区别,所有索引的“地位”都是相同的。
- InnoDB 支持变长数据类型,不同记录的长度可能不同;内存表不支持 Blob 和 Text 字段,并且即使定义了 varchar(N),实际也当作 char(N),也就是固定长度字符串来存储,因此内存表的每行数据长度相同
- hash索引和B-Tree索引:
- 内存表也是支 B-Tree 索引的
- Memory引擎支持hash索引,内存表的所有数据都保存在内存,而内存的读写速度总是比磁盘快
- 不建议在生产环境上使用内存表:内存表的锁、数据持久性问题
- 内存表的锁:内存表不支持行锁,只支持表锁
- 数据持久性问题:
- 数据库重启的时候,所有的内存表都会被清空
- M-S主从模式下:从库掉电重启收到主库请求会找不到行
- M-M双主模式下:一台掉电重启会发送delete到另一台清空数据
- 建议普通内存表都用InnoDB代替
- 例外场景:在数据量可控,不会耗费过多内存的情况下,你可以考虑使用内存表
39.自增主键为什么不是连续的?
-
表的结构定义存放在后缀名为.frm的文件中,但是并不会保存自增值
-
不同的引擎对于自增值的保存策略不同:
- MyISAM 引擎的自增值保存在数据文件中
- InnoDB 引擎保存在内存里,MySQL 8.0之后才有了“自增持久化“的能力(也就是说如果重启,表的自增值可以恢复为MySQL重启前的值)
- MySQL 5.7:第一次打开表的时候,都会去找自增值的最大值 max(id),然后将 max(id)+1 作为这个表当前的自增值
- MySQL 8.0:将自增值的变更记录存在redo log中,重启时依靠redo log恢复
-
自增值修改机制
- 如果字段 id 被定义为 AUTO_INCREMENT:
- 如果插入数据时 id 字段指定为 0、null 或未指定值,那么就把这个表当前的 AUTO_INCREMENT 值填到自增字段;
- 如果插入数据时 id 字段指定了具体的值,就直接使用语句里指定的值。
- 插入值和当前自增值的大小关系,自增值的变更结果也会不同:
- 如果 X<Y,那么这个表的自增值不变
- 如果>=Y,就需要把当前自增值修改为新的自增值
- auto_increment_offset:自增值的开始
- auto_increment_increment:步长
- 如果字段 id 被定义为 AUTO_INCREMENT:
-
自增值的修改时机
- 自增主键不连续原因1:唯一键冲突
- 自增主键不连续原因2:事务回滚
- 冲突或者回滚回退的话会导致性能问题,所以InnoDB放弃了这个设计
-
自增锁的优化:
- innodb_autoinc_lock_mode:
- 默认值是 1
- 0:语句执行结束后才释放锁
- 1:普通 insert 语句,自增锁在申请之后就马上释放;类似 insert … select 这样的批量插入数据的语句,自增锁还是要等语句结束后才被释放
- 2:所有的申请自增主键的动作都是申请自增主键的动作都是申请后就释放锁
- 如果有insert … select、replace … select 和 load data 这种批量插入数据的场景时,建议设置:innodb_autoinc_lock_mode=2 ,并且 binlog_format=row
- 普通insert语句,即使 innodb_autoinc_lock_mode 设置为 1,也不会等语句执行完成才释放锁。因为在申请自增 id 的时候,是可以精确计算出需要多少个 id 的,然后一次性申请,申请完成后锁就可以释放了。
- 自增主键不连续原因3:同一个语句去申请自增 id,每次申请到的自增 id 个数都是上一次的两倍(多申请了id也会导致自增主键 id 不连续)
40.insert语句的锁为什么这么多?
-
insert…select语句:
- insert…select是常见的两个表之间拷贝数据的方法
- 可重复读隔离级别下,这个语句会给select的表里扫描到的记录和间隙加读锁
-
insert循环写入:
-
insert和select的对象是同一个表,可能造成循环写入
-
优化方案:引入用户临时表
insert into t(c,d) (select c+1, d from t force index(c) order by c desc limit 1); create temporary table temp_t(c int,d int) engine=memory; insert into temp_t (select c+1, d from t force index(c) order by c desc limit 1); insert into t select * from temp_t; drop table temp_t;
-
-
insert唯一键冲突:
- 如果出现唯一键冲突,会在冲突的唯一值上加共享的next-key lock(S锁)
- 因此碰到由于唯一约束导致报错后,要尽快提交或者回滚事务,避免加锁时间过长
-
insert into … on duplicate key update
-
insert into t values(11,10,10) on duplicate key update d=100;
-
语义的逻辑:插入一行数据,如果碰到唯一键约束,就执行后面的更新语句
-
如果有多个列违反了唯一性约束,就会按照索引的顺序,修改跟第一个索引冲突的行
-
41.怎么最快地复制一张表
-
如果可以控制对源表的扫描行数和加锁范围很小的话,我们简单地使用 insert … select 语句即可实现
-
mysqldump 方法:
-
mysqldump -h$host -P$port -u$user --add-locks=0 --no-create-info --single-transaction --set-gtid-purged=OFF db1 t --where="a>900" --result-file=/client_tmp/t.sql
把结果输出到临时文件
-
希望生成的文件中一条 INSERT 语句只插入一行数据,执行 mysqldump 命令时,加上参数–skip-extended-insert
-
然后,可以这条命令,将这些 INSERT 语句放到 db2 库里去执行
mysql -h127.0.0.1 -P13000 -uroot db2 -e "source /client_tmp/t.sql"
-
-
:导出 CSV 文件
-
将查询结果导出到服务端本地目录
select * from db1.t where a>900 into outfile '/server_tmp/t.csv';
select …into outfile 方法不会生成表结构文件
mysqldump 提供了一个–tab 参数,可以同时导出表结构定义文件和 csv 数据文件
mysqldump -h$host -P$port -u$user ---single-transaction --set-gtid-purged=OFF db1 t --where="a>900" --tab=$secure_file_priv
-
将数据导入到目标表 db2.t 中
load data infile '/server_tmp/t.csv' into table db2.t;
load data 命令有两种用法:不加“local”,是读取服务端的文件;加上“local”,读取的是客户端的文件
-
-
物理拷贝方法:
- 在 MySQL 5.6 版本引入了可传输表空间(transportable tablespace) 的方法,通过导出 + 导入表空间的方式,实现物理拷贝表的功能
42.grant之后要跟着flush privileges吗?
-
grant 语句是用来给用户赋权的
-
在 MySQL 里面,用户名 (user)+ 地址 (host) 才表示一个用户,因此 ua@ip1 和 ua@ip2 代表的是两个不同的用户。
create user 'ua'@'%' identified by 'pa';
-
全局权限:
grant all privileges on *.* to 'ua'@'%' with grant option;
-
回收上面的 grant 语句赋予的权限:
revoke all privileges on *.* from 'ua'@'%';
-
db 权限:
grant all privileges on db1.* to 'ua'@'%' with grant option;
-
表权限和列权限:
grant all privileges on db1.t1 to 'ua'@'%' with grant option; GRANT SELECT(id), INSERT (id,a) ON mydb.mytbl TO 'ua'@'%' with grant option;
-
如果内存的权限数据和磁盘数据表相同的话,不需要执行 flush privileges。而如果我们都是用 grant/revoke 语句来执行的话,内存和数据表本来就是保持同步更新的。
-
flush privileges 使用场景:
- 当数据表中的权限数据跟内存中的权限数据不一致的时候,flush privileges 语句可以用来重建内存数据,达到一致状态
- 这种不一致往往是由于直接用 DML 语句操作系统权限表导致的,所以我们尽量不要使用这类语句
43.要不要使用分区表?
- 分区表是什么?
- 一个表包含一个.frm文件和4个.ibd文件,每个分区对应一个.ibd文件;对于引擎层来说,这是 4 个表;对于 Server 层来说,这是 1 个表
- 分区表的引擎层行为:
普通表的加锁范围
分区表的加锁范围
- MyISAM的表锁是在引擎层实现的
- 分区表和手动分表:
- 分区表和手工分表,一个是由 server 层来决定使用哪个分区,一个是由应用层代码来决定使用哪个分表
- 从引擎层看,这两种方式也是没有差别的
- 两个方案的区别,主要是在 server 层上
- 分区策略
- open_files_limit:打开分区表的最大值,超过上限将报错
- MyISAM 分区表使用的分区策略,称为通用分区策略
- MySQL 5.7.9 开始,InnoDB 引擎引入了本地分区策略
- 分区表的server层行为:
- MySQL 在第一次打开分区表的时候,需要访问所有的分区;
- 在 server 层,认为这是同一张表,因此所有分区共用同一个 MDL 锁;
- 在引擎层,认为这是不同的表,因此 MDL 锁之后的执行过程,会根据分区表规则,只访问必要的分区
- 分区表的应用场景:
- 对业务透明,相对于用户分表来说,使用分区表的业务代码更简洁
- 很方便的清理历史数据:alter table t drop partition:直接删除分区文件,效果跟 drop 普通表类似。与使用 delete 语句删除数据相比,速度快、对系统影响小
44.答疑文章(三):说一说这些好问题
45.自增id用完怎么办?
-
表定义自增值id:达到上限后值不会改变,再写入会报主键冲突
-
InnoDB系统自增row_id:达到上限后归0再递增,会覆盖数据
-
Xid:
- global_query_id:每次执行语句的时候将它赋值给 Query_id,然后给这个变量加 1。如果当前语句是这个事务执行的第一条语句,那么 MySQL 还会同时把 Query_id 赋值给这个事务的 Xid
- global_query_id 是一个纯内存变量,重启之后就清零
- MySQL 重启之后会重新生成新的 binlog 文件,保证,同一个 binlog 文件里,Xid 一定是惟一的
- global_query_id 达到上限后,就会继续从 0 开始计数
- global_query_id 定义的长度是 8 个字节,上限是 264-1,264这个值太大了,大到你可以认为这个可能性只会存在于理论上
-
Innodc trx_id(事务 id:transaction id):
- Xid 由 server 层维护的。InnoDB 内部使用 Xid,为了能在 InnoDB 事务和 server 之间做关联。但是,InnoDB 自己的 trx_id,是另外维护的。
- max_trx_id 全局变量;会持久化存储,重启也不会重置为 0
- 通过事务的一致性视图与这行数据的 trx_id 做对比,判断这个数据是否可见
- 对于只读事务(在 select 语句后加上 for update,也不是只读事务),InnoDB 并不会分配 trx_id;把当前事务的 trx 变量的指针地址转成整数,再加上 2^48(所以有时出现一个很大值)
- 减小事务视图里面活跃事务数组的大小
- 减少 trx_id 的申请次数
-
thread_id:
-
thread_id_counter:每新建一个连接,就将 thread_id_counter 赋值给这个新连接的线程变量
-
大小是 4 个字节,因此达到 232-1 后,它就会重置为 0,然后继续增加
-
不会在 show processlist 里看到两个相同的 thread_id
do { new_id= thread_id_counter++; } while (!thread_ids.insert_unique(new_id).second);
-