文章目录
- mysql实战45讲
- 第一讲:一条查询语句是怎样执行的
- MySql总的架构
- 结构
- 过程
- 分别介绍各个部分
- 一:连接器
- 任务:
- 过程
- 连接管理
- 二:查询缓存
- 形式
- 三:分析器
- 编译原理知识
- 四:优化器
- 介绍
- 五:执行器
- 介绍
- 第二讲:一条更新语句是怎样执行的(日志系统)
- 恢复MYSQL到半个月内任意一秒
- 表创建语句 / update语句
- redo log
- bin log
- 为什么需要两份日志
- 执行update语句的内部流程(引擎和执行器配合)
- 两阶段提交
- 设置相关参数,保证数据不丢失
- 什么情况下:一天一备比一周一备份更有优势?
- 第三讲:事务隔离
- 隔离性 / 隔离级别
- 读未提交
- 读提交
- 可重复读
- 串行化
- 事务隔离中 级别的实现(通过视图)
- 读未提交
- 读提交
- 可重复读
- 串行化
- 事务隔离的实现
- 回滚操作
- 回滚日志
- 不建议使用长事务
- 事务的启动方式
- 显示启动事务语句
- set autocommit=0
- 解决
- 如何避免和处理长事务
- 第四讲:索引(上)
- 索引的常见模型
- 哈希表
- 有序数组
- 搜索树
- 总结
- InnoDB的索引模型
- InnoDB索引使用的是B+树索引模型
- 索引维护
- 自增主键
- 新增一个自增字段作为主键
- 业务字段直接做主键
- 问题
- 第五讲:第五讲:索引(下)
- 回表
- 避免回表——覆盖索引
- 是否需要联合索引
- 最左前缀原则
- 利用B+树的最左前缀原则性质
- 建立联合索引时,如何安排字段顺序
- 索引下推
- 使用最左前缀加速之后
- 问题
- 第六讲:锁——全局锁和表锁
- 全局锁
- 使用场景
- 表级锁(两种)
- 表锁
- 元数据锁
- 第七讲:锁——行锁(InnoDB)
- 两阶段锁
- 死锁和死锁检测
- 问题
- 第八讲:事务到底是隔离的还是不隔离的?
- 快照
- 事务ID
- 快照概念
- 可重复读
- 更新逻辑
- 当前读
- 串联一致性读、可重复读、行锁
- 可重复读的能力是如何实现的
- 可重复读和读提交的区别
- 问题
- 第九讲:普通索引和唯一索引,该怎么选择
- 查询过程
- 更新过程
- change buffer
- change buffer在InnoDB的处理流程
- change buffer的使用场景
- 索引选择和实践
- 普通索引和唯一索引该如何选择
- change buffer和redo log
- 问题:
- 第十讲:MySQL为什么有时候会选错索引?
- 优化器的逻辑
- 判断标准
- 扫描行数是如何判断的
- 索引选择异常和处理
- 方法一
- 方法二
- 方法三
- 问题:
- 第十一讲:怎么给字符串字段加索引
- 前缀索引
- 前缀索引对覆盖索引对影响
- 其他方式
- 问题
- 第十二讲:为什么我的MySQL会抖一下
- 为什么SQL语句变慢了
- flush
- 为什么会抖一下
- 什么情况下会引发flush过程
- 分析性能影响
- InnoDB刷脏页的控制策略
- 考虑因素
- 刷脏页的一个策略
- 问题
- 第十三讲:为什么表数据删掉一半,表文件大小不变
- 表数据
- 参数innodb_file_per_table
- 数据删除流程
- 删除记录 / 删除数据页
- 重建表
- 重建表方法
- MySQL对此流程的优化
- online和inplace(有疑问没听懂)
- 概念
- online和inplace的区别
- 问题
- 第十四讲:count(*)这么慢,我该怎么办
- count(*)的实现方式
- 当有一个页面需要经常显示总数
- 用缓存系统保存计数
- 用Redis服务来保存
- 在逻辑上依然不准确
- 用数据库保存计数
- 不同的count用法
- 问题
- 第十五讲:答疑
- 第十六讲:“orderby是怎么工作的”
- 全字段排序
- 排序过程
- 备注
- rowid排序
- 单行太大时
- 变化
- 排序过程
- 变化
- 全字段排序 vs rowid排序
- 内存
- orderby
- 进一步简化
- 问题
- 第十七讲:如何正确地显示随机消息
- 内存临时表
- order by rand()
- 执行流程与扫描行数
- MySQL是靠什么定义“一行数据”
- 磁盘临时表
- 不是所有临时表都是内存临时表
- 随机排序方法
- 随机排序方法1
- 随机排序方法2
- 随机排序方法3
- 问题
- 第十八讲:为什么这些SQL语句逻辑相同,性能却差异巨大
- 案例一:条件字段函数操作
- 案例二:隐式类型转换
- 数据类型转换规则是什么
- 为什么有数据类型转换就要走全索引扫描
- 案例三:隐式字符编码转换
- 字符集不同,用不上索引
- 问题
- 第十九讲:为什么我只查一行的语句,也执行这么慢
- 第一类:查询长时间不返回
- 等MDL锁
- 等flush
- 等行锁
- 第二类:查询慢
- 问题
- 第二十讲:幻读是什么,幻读有什么问题
- 幻读是什么
- 幻读有什么问题
- 语义上
- 数据一致性
- 如何解决幻读
- 解决办法
- 间隙锁
- next-key lock
- 带来的困扰
- 问题
- 第二十一讲:为什么我只改一行的语句,锁这么多
- 前提
- 加锁规则
- 原则
- 优化
- bug
- 案例一:等值查询间隙锁
- 案例二:非唯一索引等值锁
- 案例三:主键索引范围锁
- 案例四:非唯一索引范围锁
- 案例五:唯一索引范围锁bug
- 案例六:非唯一索引上存在“等值”的例子
- 案例七:limit语句加锁
- 案例八:一个死锁的例子
- 问题
- 第二十二讲:MySQL有哪些“饮鸩止渴”提高性能的方法
- 短连接风暴
- 先处理掉那些占着连接但是不工作的线程
- 减少连接过程的消耗
- 慢查询性能问题
- 索引没有设计好
- 语句没写好
- 选错了索引
- 总结
- QPS突增问题
- 总结
- 第二十三讲:MySQL是怎么保证数据不丢的?
- binlog的写入机制
- 写入逻辑
- binlog cache
- binlog cache的保存问题
- redo log的写入机制
- redolog的三种状态
- 三种状态
- 通过参数控制
- redolog的写入策略
- 补充说明
- 组提交机制
- 日志逻辑序列号(LSN)
- 保证不会执行重复redolog
- binglog也可以组提交
- 通过设置参数提升效果
- 总结
- WAL是如何提升性能的
- MySQL在IO上出现性能瓶颈,如何解决
- 补充
- 第二十四讲:MySQL是怎么保证主备一致的?
- MySQL主备的基本原理
- 概念
- 尽管备库没有被直接访问,但是要设置成只读
- 设置成只读了,还怎么和主库一致?
- 从主库到备库的内部流程
- binlog三种格式对比
- binlog有三种格式
- binlog为statement情况下
- binlog为row的情况下
- 为什么会有mixed格式的binlog
- 为什么现在普遍要求设为row
- delete
- insert
- update
- 总结
- 如果是mixed?
- 恢复方式
- 循环复制问题
- 结构
- 双M存在循环复制的问题
- 问题
- 第二十五讲:MySQL是怎么保证高可用的?
- 最终一致性
- 主备延迟
- 概念
- seconds_behind_master是如何计算的
- 主备延迟的来源
- 原因一
- 原因二
- 原因三
- 原因四
- 切换策略
- 可靠性优先策略
- 可用性优先策略
- 概念
- 例子
- 把binlog_format设为row
- 什么情况下使用可用性优先策略
- 可靠性优先策略异常切换时是什么样
- 第二十六讲:备库为什么会延迟好几个小时
- 备库的并行复制能力
- MySQL5.5版本的并行复制策略
- 按表分发策略
- 按行分发策略
- MySQL5.6版本的并行复制策略
- MariaDB的并行复制策略
- MySQL5.7版本的并行复制策略
- MySQL5.7.22版本的并行复制策略
- 第二十七讲:主库出问题了,从库怎么办?
- 第二十八讲:读写分离有哪些坑
- 第二十九讲:如何判断一个数据库是不是出问题了
- 第三十讲:
- 第三十一讲:误删数据后除了跑路,还能怎么办
- 第三十二讲:为什么还有kill不掉的语句
- 第三十三讲:我查这么多数据,会不会把数据内存打爆
- 第三十四讲:到底可不可以使用join?
- 第三十五讲:join语句怎么优化
- 第三十六讲:为什么临时表可以重名
- 第三十七讲:什么时候会使用内部临时表
- 第三十八讲:都说InnoDB好,那还要不要使用Memory引擎
- 第三十九讲:自增主键为什么不是连续的
- 第四十讲:Insert语句的锁为什么这么多
- 第四十一讲:怎么最快的复制一张表
- 第四十二讲:grant之后要跟着flushprivileges吗?
- 第四十三讲:要不要使用分区表
- 第四十四讲:
- 第四十五讲:自增ID用完怎么办
mysql实战45讲
慢查询日志
第一讲:一条查询语句是怎样执行的
通过一条语句的执行过程,将MySql的内部架构串联一下
MySql总的架构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Aga6rjUw-1655861353126)(./FILES/mysqlshi-zhan-45jiang.md/MySql逻辑架构.png)]
结构
包括Server层 和 存储引擎层 的上下两层,
其中
1. Server层
对上:处理用户传来的数据(词法分析语法分析)& 用户权限验证
对下:生成给引擎的指令 & 操作引擎,返回数据 & 协调各个引擎,统揽全局
2. 存储引擎层
由多个存储引擎构成,负责具体数据的存储
多个存储引擎共用同一个上层(Server层)
过程
1. 建立 **连接**
—— 连接器(确认权限,维持/管理这个连接)
2. 进入查询缓存查询(类似于快表)
3. 没有命中,真正开始查询
Server层:
确认从客户端传来的命令没有问题
—— 分析器
对语句进行预处理,方便传给下一层
—— 优化器
引擎层:
一行一行执行代码
分别介绍各个部分
一:连接器
任务:
建立连接,获取权限,维持和管理连接
过程
1. 客户端和数据库三次握手
2. 用户名密码正确,则查询该用户所拥有的权限
连接管理
空闲连接:不在操作的,可立即使用的连接
长时间没有动静:连接会自动断开
长连接:每次查询都用同一个连接
短连接:每次查询时,都先建立一个;查询完就断开。
注意事项
1. 尽量使用长连接
2. 全部使用长连接后,容易积累内存导致内存占用太大被系统强行杀掉
解决办法:
a. 定期断开长连接
b. 每次执行一个大操作后,附带执行一个操作reset一下资源(非先断开再重新连接)
二:查询缓存
形式
以key-value保存之前查询过的结果
往往利大于弊, 8.0版本后已删去此功能
三:分析器
编译原理知识
1. 词法分析
2. 语法分析
四:优化器
介绍
有多个方案可以得出同一个结果时,选择一个最优方案
五:执行器
介绍
确认权限
调用引擎的接口
第二讲:一条更新语句是怎样执行的(日志系统)
恢复MYSQL到半个月内任意一秒
表创建语句 / update语句
- 基本的执行链路(上一讲)走一次
- 日志模块(redo log和bin log两个日志模块)
redo log
内存、redo log、磁盘,是三样东西
类比:孔乙己中的账本与粉板
WAL技术:先写日志,再写磁盘
(先写粉板,不忙时再写账本)
有记录需要更新时
- InnoDB引擎先先记录到redo log/内存中
- 系统空闲时再写入磁盘中
特殊情况:当redo log写满时:先腾到磁盘中,再写redo log
- 什么样叫满:
- redo log的大小
- 循环空间存储/擦除 (crash-safe)
- 两个指针:write和check(头尾指针/追击)
- write 写的位置
- check 数据存储的位置(届时从此处开始擦除数据)
redo log的crash-safe技术保证数据库异常重启时之前提交的数据不会丢失。
bin log
redo log为InnoDB引擎独有
而server层独有的为bin log
为什么需要两份日志
- 背景
- redo log是InnoDB引擎独有,最初并不是InnoDB引擎
- 不同点
- Server层的binlog所有引擎都能用,redo log只有InnoDB能用
- redolog是物理日志:记录方式
- binlog是逻辑日志
- redo log是循环写
- binlog是追加写
执行update语句的内部流程(引擎和执行器配合)
执行器对数据的逻辑进行操作(取数据,加减,写入)
引擎对数据的物理进行操作(内存/磁盘/读取/返回)
- 初始
- prepare阶段
- 写入redo log之后,写入binlog之前
- commit阶段
- 写入binlog之后
两阶段提交
- 为什么需要两阶段提交
- 两份日志的逻辑要一致
- 数据恢复的过程:重放binlog
- 反证法(假如有先后),以事务(0+1)为例
- 先redolog再binlog,在redolog写完之后异常重启了
- binlog中无数据,用binlog重放时会出现问题
- 先binlog在redolog
- 在binlog写完之后出问题,redolog没写
- 由于redolog中无数据,所以该事务(+1)无效
- 但binlog中数据已经变成值1了
会在需要数据恢复时/扩容(备用库)时出错
主从数据库不一致
设置相关参数,保证数据不丢失
redolog:innodb_flush_log_at_trx_commit=1
binlog:sync_binlog=1
什么情况下:一天一备比一周一备份更有优势?
第三讲:事务隔离
事务:相当于操作系统中的多个步骤组成的一个原子操作。性质:ACID
隔离性 / 隔离级别
隔离的越严实,效率越低。故通常要找一个平衡点
读未提交
在commit之前就被别的事务看到
读提交
在commit之后才能被别的事务看到
可重复读
整个执行期间,数据都是不变的
串行化
同一行记录(相当于os中一个文件),有读写锁
事务隔离中 级别的实现(通过视图)
读未提交
无视图,就是当时记录的值
读提交
每个,SQL语句执行时创建
可重复读
在启动时创建,整个过程都用同一个
串行化
直接对记录加锁
默认:读提交
配置方式:transaction- isolation:READ-COMMITTED
查看:show variables
事务隔离的实现
回滚操作
数据在update时可以通过回滚操作,恢复成上一个值
回滚日志
系统自动判断何时删除
不建议使用长事务
导致回滚日志必须保留,大量占用存储空间
事务的启动方式
避免无意识的使用长事务
显示启动事务语句
begin/start transaction 搭配 commit / rollback
set autocommit=0
会关闭自动提交,事务会一直存在到你主动commit/rollback或者断开连接
有的框架会默认执行:set autocommit=0
解决
- 通过set autocommit=1,用显式语句启动事务
- 多一次交互的问题
- commit work and chain
- 在提交本事务时会自动启动下一事务
- 查询长事务
- 在information_schema库的innodb_trx中查询
- 查询语句
如何避免和处理长事务
第四讲:索引(上)
索引:相当于书的目录。数据库表的目录,为了提高数据查询效率。
索引的常见模型
实现索引的方式有很多种
哈希表
- key:value键值对
- 数组:用哈希函数计算出一个位置
- 冲突处理方法之一:拉链法
- 适用于:等值查询
有序数组
- 等值查询/范围查询的性能都优秀(二分法)
- 查询效率高,但是更新数据时麻烦(因为是数组)
- 只适用于静态存储引擎(不再修改)
搜索树
树根总在内存中
- 二叉树
- 左小于父,右大于父
- 平衡二叉树(加快效率)
- 大多数数据库不使用二叉树:因为索引要写在磁盘上。
- 查询数据要来回访问内存和磁盘
- N叉树
- N取决于数据块大小,差不多是1200
- 2^N=能存的数据量来看,一般最多访问3次内存
总结
数据库的适用场景,先看数据模型
InnoDB的索引模型
不同引擎的索引方式不同
InnoDB索引使用的是B+树索引模型
- 数据都是存在B+树中
- 索引分为主键索引和非主键索引
- 主键索引:整行数据
- 非主键索引:主键的值
- 查询时的区别
- 主键索引:直接查询ID(主键)这棵B+树
- 非主键:需要多扫描一颗索引树
- 所以,应尽量使用主键查询
索引维护
在插入新值时需要做必要的维护
- B+树算法
- 直接插入一条新的记录
- 挪动数据,空出位置
- 页分裂
- 数据页满了,需要申请新页再挪动部分数据
- 影响性能、影响空间利用率
- 页合并
- 页分裂的逆过程
自增主键
新增一个自增字段作为主键
- 自动+1
- 方便追加操作,不挪动
- 有业务逻辑时,不能保证有序插入,
- 数据成本高
- 性能受影响
- 存储空间(身份证号:32位,整型4字节,长整型8字节)
- 自增主键往往更合理
业务字段直接做主键
- 只有一个索引
- 该索引是唯一索引
- 可以避免每次都需要搜索两棵树
问题
InnoDB两个重建主键索引的做法,哪个不合适,为什么?
第五讲:第五讲:索引(下)
回表
没有用主键作为关键词来搜索,但还是回到主键索引树来搜索
避免回表——覆盖索引
通过改变查询语句?
是否需要联合索引
- 有两个字段的索引表(通过身份证号来查姓名)
- 假如该请求比较高频,那么有必要建立联合索引
- 用冗余索引来实现覆盖索引,成本比较高需要专人维护
最左前缀原则
用身份证号查家庭住址
- 既不能全表扫描
- 又不能为一个不频繁的请求创建一个(身份证号,地址)索引
利用B+树的最左前缀原则性质
只要满足最左端的和所查询的相符,就可以加速检索
建立联合索引时,如何安排字段顺序
- 索引的复用能力
- 通过调整顺序,可以少维护一个索引
- 具体案例
- 通过一个高频索引来支持某个低频请求
- 既有联合查询,又有a,b各自的查询
- 同时维护两个索引
- 这时需要考虑空间
- (name,age)还是(age,name) + (age)
索引下推
不符合最左前缀时
使用最左前缀加速之后
- 把一条条合适的数据先取出来
- 再用索引中包含的其他字段来比较,去掉某一些不符合
- 能够减少回表次数
问题
a,b是联合主键。是否需要创建(ca),(cb),©共三个索引
第六讲:锁——全局锁和表锁
初衷:处理并发事件,有冲突时制定访问规则
分类:全局锁、表级锁、行锁(根据加锁的范围)
全局锁
对整个数据实例加锁,整个数据库都是只读的状态。
数据更新语句,数据定义语句(表),提交语句都会阻塞
使用场景
- 全库逻辑备份
- 是通过视图
- 既然通过视图,那只要在可重复读的级别下不行吗?
- 有的引擎不支持
- 为什么不通过命令set global readonly=true设置只读
- 修改global变量影响面更大
- 异常处理机制不一样
- 出现异常时,锁会自动释放
- 出现异常时,global会一直保持
表级锁(两种)
表锁
- 语法 lock tables……read/write
- 不仅限制读写(别的线程),也限制本线程的操作对象
- lock tables命令控制并发的影响面还是比较大,用得少
- 需要更细粒度的锁
元数据锁
- 系统会自动加上
- 增删改查(读锁)——不互斥
- 表结构变更时(写锁)——互斥
- 注意点:长事务
- 语句结束后不会释放,整个事务提交后才释放
- DDL变更的表有长事务在执行:暂停或者kill
- 注意点:数据量不大的热点表
- kill未必管用:新的请求马上就来了
- 设置等待时间,时间到了先放弃锁,免得阻塞后面的业务语句
第七讲:锁——行锁(InnoDB)
针对于某一条行记录的锁
提高并发度,由各个引擎自己实现
两阶段锁
- 在commit之后才会释放
- 要把有可能造成冲突,影响并发度的锁往后放
- 影响某一行的锁,时间最少
- 不会等待很长时间
死锁和死锁检测
- 资源+等待别人
- 策略:设置超时时间
- 如果是在线服务,不能时间不能太长
- 不能设置时间太短,容易误伤
- 这一种方法使用的少
- 策略:发起主动死锁检测
- 主动回滚某一个事务(退回前一个状态)
- 让别的事务执行
- 这也是默认状态
- 主动死锁检测消耗CPU太大
- 每个加进来的进程都要检测一遍
- 假设有1000个并发线程更新同一行
- 解决方法
- 确保这个业务一定不会死锁——主动关闭
- 临时使用,且有很大风险。
- 控制并发度:同一行最多只有十个进程在执行。
- 在客户端做,不太可行。在服务端做(MySQL源码)
- 在设计上优化:把一行记录的东西分散到十行来记录
问题
删除一万行数据,选择哪种方法+理由?
第八讲:事务到底是隔离的还是不隔离的?
快照
事务ID
- 每次更新数据时,会生成一个新的数据版本
- 每个数据版本有对应对应的事务id
快照概念
- 可重复读情况下,事务在启动时就会拍下“快照”
- 并不是拷贝一份数据,而是拿到了这个数据的某个版本
- 某一行数据可能有多个数据版本
- 数据版本是通过undo log(回滚日志)和当前版本计算出来的
可重复读
- 在事务启动时,能够看到所有已经提交的事务
- 启动瞬间的活跃事务数组:启动但还没提交的所有事务
- 数组中id最小值:低水位,最大值加1:高水位
- 该视图数组和高水位:一致性视图
- 秒级别创建快照
- 可重复读实现就是判断是否处于不可见的区域
- 处于,即向前退,一直到数据版本ID小于当前的ID
更新逻辑
当前读
- 更新数据update指令都是先读再写
- 别的指令先执行了(由1->2),然后本指令再update
- 就是在2的基础上update
- 当前读:读当前值
- 加锁的select也是当前读
串联一致性读、可重复读、行锁
- 事务A没有commit
- 事务B读时是当前读,并且加了行锁,读的值不会改变
- 实现了一致性读
可重复读的能力是如何实现的
- 核心是一致性读
可重复读和读提交的区别
- 可重复读,一开始创建的致性视图一直用
- 每条语句执行时重新计算视图
- 读提交的数组是在执行语句时创建的
- 而可重复读是在commit时创建的
问题
第九讲:普通索引和唯一索引,该怎么选择
普通索引:只加快了对数据的访问
唯一索引:不允许有重复数据
两种索引对查询语句和更新语句的影响
查询过程
- 在B+树上查找,按层从树根找到叶子
- 普通索引:找到第一个记录后需要继续向下查找直至不满足的数据
- 唯一索引:找到第一个满足的记录后就停止
- 没有太大影响:因为在内存中是整个数据页全读取进来了
更新过程
change buffer
- 数据在内存:直接更新
- 数据不在内存:
- 不读数据,而把操作放在buffer中,该buffer可以持久化(写入磁盘)
- 这个操作称为merge
- 在下次读取数据进入内存时会触发merge
- 有线程定期merge
- 数据库正常关闭也会执行merge
- 能够减少读磁盘
- 能够避免从磁盘读进内存,占内存
- 什么条件下使用change buffer
- 唯一索引需要判断是否存在重复
- 所以必须要读入内存
- 所以唯一索引不能使用
- 补充
- change buffer用的是buffer pool中的内存
- 不能无限增大
- 可以通过设置参数来设置允许占用pool的最高比例
change buffer在InnoDB的处理流程
- 已经在内存(不使用change buffer)
- 唯一索引:判断是否有冲突,然后插入
- 普通索引:直接执行
- 不在内存
- 唯一索引:读入内存,判断没有冲突,插入
- 普通索引:写进change buffer,结束
- 总结
- 减少的磁盘访问次数,极大提升了效率
- 而唯一索引需要读入内存,使用不了change buffer,还占用内存
change buffer的使用场景
- 能够实现加速,但并不是所有使用场景都加速
- 对于写多读少的业务,change buffer将操作缓存下来
- 记录的变更数越大,收益就越大
- 而对于写入之后马上就查询的业务,
- 不仅没有加速
- 反而增加了change buffer的维护成本
索引选择和实践
普通索引和唯一索引该如何选择
- 在查询上没有差别
- 主要考虑在更新上的性能差别
- 尽量选择普通索引,甚至把buffer pool中change buffer容量调大一点
- 但是对于更新之后立即查询的业务
- 应关闭change buffer
change buffer和redo log
不要混淆redo log和change buffer
- redo log会把整个过程都记录下来
- 包括更新内存 + change buffer中的操作
- redo log节约了写磁盘的IO消耗
- change buffer是读磁盘的IO消耗(减少了读的次数,在执行update语句当时不读数据,而是下次读数据时update数据)
问题:
突然断电是否会丢失change buffer
第十讲:MySQL为什么有时候会选错索引?
选错索引,导致更长的执行时间
优化器的逻辑
选择索引时优化器的工作,目的是为了一个最优的执行方案以最小的代价执行语句。
判断标准
- 扫描行数,是否使用临时表,是否排序等
扫描行数是如何判断的
- 通过统计信息(区分度)来估算
- 统计信息也就是不同值的个数/基数
- 基数是如何得到的
- 针对N个数据页面
- 使用采样统计(全部统计代价太高)
- 得到的平均值 * 页面数
- 当行数超过1/M,会在做一次索引统计
- 可以通过设置参数来选择两种统计方式
- 对于具体语句,还有判断这个语句本身要扫描多少行
- 普通索引时,还要把“回表”的时间算上去
- 可以修正统计信息:analyze table命令
索引选择异常和处理
原本可以执行的很快的SQL语句却比预期慢很多
方法一
- 采用 force index强行选择一个索引
- MySQL回根据词法分析,提供可用的索引作为候选项
- 自己判断,然后强行选择
- force index 采用矫正的作用
- 使用的少
- 不优美
- 索引改名后语句也得改
- 迁移到别的数据库时,语法可能不兼容
- 不够敏捷
- 数据库的问题最好在数据库内部解决
方法二
- 修改语句,引导MySQL使用我们期望的索引
- 基于具体语句来写
方法三
- 新建一个更合适的索引
- 或删掉一个误用的索引
- 使用的少
这个是MySQL的BUG,需要修改MySQL代码才行
问题:
第十一讲:怎么给字符串字段加索引
以邮箱登录的的系统,如果email上没有索引,就只能全表扫描
前缀索引
- 可以定义字符串的一部分作为索引
- 如果不能指定前缀长度,则索引会包含整个字符串
- 使用整个字符串作为索引
- 从索引树上查找到‘xx@126.com’,取得它的主键ID2
- 从主键上查主键为ID2的行,判断email的值是正确的
- 加入结果集,再向下查
- 发现不满足email=‘@126.com’了,循环结束
- 只需要回主键扫描一行
- 使用前6个字符作为索引
- 占用的空间更小,但是可能会增加额外扫描次数
- 可能会多次去主键树中验证
- 只要长度定义得好,比如取前7个数据时,也只查一次
- 确定前缀长度最重要是区分度,即重复越少越好
- 通过语句计算
- select count()
- count() as L4;
- 可能会损失区分度,要预先确定损失比例
前缀索引对覆盖索引对影响
- 前缀索引不仅增加扫描行数影响性能
- 并且使用前缀索引就使用不了覆盖索引了
- 必须要再回主键树查一次
其他方式
- 当前缀长度不够好的情况
- 身份证号,同一地区前六位总是相同的
- 再取前缀为前12位,效率就更低了
- 解决方法
- 使用倒序存储
- 语法
- 实践中还需要验证下
- 使用hash字段
- 在表上再创建一个int字段存校验码
- 校验码由crc32()函数计算得出
- 但是crc32()函数可能会得出两个相同值
- 所以语句的where部分要判断身份证是否精确相同
- 相同点
- 都不支持范围查询(大于xx,小于xx),都是字符串
- 不同点
- 倒序不增加字段,但是前缀长度更长
- cpu消耗
- 倒序每次都要调用reverse()函数
- hash每次都要调用crc32()函数
- reverse()函数消耗cpu资源更小
- 稳定性
- crc32()也会冲突(相同值),但是概率很小,扫描行数接近1
- 倒序毕竟还是前缀索引,会增加扫描行数
问题
针对这个特定数据表,如何设计索引
第十二讲:为什么我的MySQL会抖一下
一条SQL语句,正常时很快,但有时会特别慢
这种情况不仅随机,而且持续时间很短,很难复现
为什么SQL语句变慢了
flush
- WAL中,掌柜总要找时间把粉板内容写进账本,即flush
- 脏页:内存数据页和磁盘数据页内容不一致
- 干净页:内存数据页和磁盘数据页内容一致
为什么会抖一下
- 运行的很快,就是在写内存和redo log
- “抖一下”,可能就是在刷脏页(flush)
什么情况下会引发flush过程
- “粉板”满了,redo log写满了(环形)
- 系统内存不够用了,要淘汰了脏页时需要flush
- 系统空闲时
- MySQL正常关闭时
分析性能影响
- 后两种情况下不需要太关注性能
- 第一种情况,InnoDB要尽量避免
- 会导致更新堵塞,不再update
- 第二种情况,是常态
- InnoDB用buffer pool管理内存
- 缓冲池中的内存页有三种状态
- 还没使用(很少)
- 干净页(可以正常淘汰,不影响性能)
- 脏页(需要flush才能用)
- 脏页影响性能的情况
- 要淘汰的脏页太多
- redo log写满了
InnoDB刷脏页的控制策略
避免上面两种两种情况影响性能,故要控制脏页比例
考虑因素
- 主机的IO能力(全力刷脏页能力)
- innodb_io_capacity参数
- 可以设置成磁盘的IOPS
- 磁盘不能全用来刷脏页,按照“全力”的百分比来刷脏页
- 刷脏页速度太慢,会导致
- 内存脏页太多
- redo log写满了
- 所以,考虑因素有
- 脏页比例
- redo log写盘速度
- 计算速度
- 脏页比例上限:参数innodb_max_dirty_pages_pct
- checkpoint计算出另一个值
- 两个值可通过一个复杂公式算出
- 取大者所为刷脏页速度
- 平时 就需要多关注脏页比例
刷脏页的一个策略
- 在刷脏页时,刚好本页旁边的一页也是脏页
- 顺带flush自己的“邻居”
- innodb_flush_neighbors
- 参数取0/1可设置是否使用“连坐”机制
- 在机械硬盘时代,能够提升性能
- SSD时代,只刷自己就能够更快
- 该参数在MySQL默认值是0
问题
redo log容量设置得太小会发生什么情况
第十三讲:为什么表数据删掉一半,表文件大小不变
数据库占用空间太大,把表删了之后文件大小不变?
表数据
- 表结构只能用空间很小,不讨论表结构
参数innodb_file_per_table
- 控制表数据存在共享表空间还是单独的文件
- 参数设置为off
- 在共享表空间
- 和数据字典放在一起
- 参数设置为on
- 在一个以.ibd为后缀的文件中
- 是默认值和推荐做法
- 更容易管理
- drop table可以直接删掉文件
- 删除表的具体某些行时
- 非drop table删整张表的操作
- 表中数据被删除了,表空间却没被回收
数据删除流程
删除记录 / 删除数据页
- InnoDB的数据都是用B+树结构组织的
- 删除某个记录时,只会把这个位置标记为删除,文件大小不会缩小
- delete命令只是把空间标记为“可复用”,没有回收空间
- 而留下了一个“空洞”,没有数据却占用了空间
- 类似os中的固定分区大小
- 插入数据时,当某一页数据不够用,只能插入新的一页
- 也会在那一页中留下空洞,浪费空间
重建表
能够收缩空间,去掉空洞。
重建表方法
- 类似于复制数组,将数据一行一行读取并插入到一个新表
- 命令:alter table A engine=InnoDB
- 其中插入数据过程比较花时间
- 在此过程中如果有新数据插入会导致数据丢失
MySQL对此流程的优化
引入Online DDL
- 将原来的数据存储到临时文件中
- 将对原表的操作记录到一个日志文件中 (row log)
- 将操作应用到临时文件上
- 用临时文件替换原数据
alter语句执行时,加了锁
- 一开始时写锁,然后在真正拷贝数据之前就退化为读锁
- 为什么要读锁,而不干脆解锁?
- 防止其他线程同时做DDL
- 比较浪费时间,想要比较安全的操作的话,可以使用GitHub开源的gh-ost
online和inplace(有疑问没听懂)
概念
- 没有优化之前的重建表
- 表A的临时数据导出是一个临时表,在server层
- 优化后的重建表
- 表A重建出来的数据是临时文件
- 没有把数据挪动的过程
- 是一个原地操作
online和inplace的区别
- inplace会堵塞增删改操作,是非Online的
- DDL过程如果是Online就一定是inplace
- 反过来未必
问题
执行表收缩之后,空间不但没小而且还变大了?
第十四讲:count(*)这么慢,我该怎么办
计算表的行数,记录数越来越多,这条语句越来越慢
count(*)的实现方式
- MyISAM:把总行数存起来
- InnoDB:一行一行读出来,然后计数
- InnoDB为什么不存起来
- 与事务设计有关系,可重复读是默认级别
- 是用多版本并发控制MVCC实现
- 每一行都要判断对这个会话是否可见
- “可见的行”才能用于计算总行数
- InnoDB其实也进行了优化
- 数据存在B+树中
- 普通索引树比主键索引树小很多
- 优化器会找到最小那棵树来遍历
- show table status很快,但是是估算出来的
当有一个页面需要经常显示总数
基本思路:找个地方把行数存起来
用缓存系统保存计数
用Redis服务来保存
- 缓存系统可能会丢失更新
- 需要定期的持久化,但是异常重启时可能会丢失
- 解决办法,再单独执行一下count(*)
在逻辑上依然不准确
- 有可能在数据插入和更新Redis值的间隙
- 进来一个查询,该查询的结果就会不准确
用数据库保存计数
- 把这个计数单独放到一个计数的表中
- 能够解决数据丢失的问题
- 能够解决数据不准确的问题
- 利用事务的特性
- 只要commit中,既插入数据,又行数+1
- 只要commit了,两个操作就同步完成了
不同的count用法
原则:
- count的语义
- 有个累计值,符合要求就+1
- 返回总计数
- count(主键)——主键为ID
- 对每个ID,判断不为空,则+1
- count(1)
- 对于每一行,判断不为空,则+1
- 比count(主键)快,避免了解析
- count(字段)
- 这个字段是否允许为null
- 不允许为null
- 不为null,+1
- 为null
- 再把值取出来判断一下,不是null才累加
- count(*)
- 不取值
- 结论
- 效率:count(字段) < count(主键) < count(1)约等于 count(*)
- 尽量多使用count(*)
问题
当有一个页面需要经常显示总数,事务中应该先插入记录还是先更新计数表
第十五讲:答疑
第十六讲:“orderby是怎么工作的”
需要按照指定字段排序来显示结果
全字段排序
- 为了避免全表扫描,需要加上索引
- explain命令查看执行情况
排序过程
- 初始化一个sort_buffer用来存结果
- 找到第一个满足字段排序条件的主键
- 从主键取出整行,放入buffer中
- 下一个,直至不满足
备注
- 排序过程可能在内存或外部排序(磁盘临时文件)中
- 取决于是否大于为排序开辟的内存sort_buffer_size
- 可使用语句,判断排序是否使用了临时文件
- 使用的是归并排序
- 语句中查到的参数
- 参与排序的行数/是否做了紧缩处理/扫描了的行数
rowid排序
全字段排序中,只对数据读了一遍,其余操作都在sort_buffer或者临时文件中执行的。
加入查询的字段太多,要分成很多个小文件,性能太差。
单行太大时
- 单行长度太大会使用rowid排序(总定义长度)
- 可通过某个参数,控制长度
- max_length_for_sort_data
变化
- 原本要排序三个字段
- 排序时只剩下主键和要排序的列两个字段
- 返回时少了两个字段的值,就不能直接返回了
- 不能返回详细的一整行
排序过程
- sort_buffer中放入两个字段
- 取要排序字段中排在第一个的主键:id1
- 取主键为id1的另两个字段
- 放入sort_buffer
- 取下一个主键
变化
- rowid排序多访问了一次主键
- 返回的结果集是直接返回的,不需要再耗费内存
- 语句中查到的参数
- 参数1
- 在排序完成后还要去原表取值
- 多读了1000行
- 参数2
- 参与排序的只剩两个字段了
- 参数3
- 行数不变,但是每一行都变小了
- 所需要的临时文件也变小了
rowid排序三个字段,把第一个字段排好序,然后依次取主键来排另外两个字段
全字段排序 vs rowid排序
内存
- MySQL是担心内存太小影响排序效率,才会使用rowid排序
- 内存足够就会优先选全字段排序,不用去原表再取数据
- 设计思路:尽量减少磁盘访问
- rowid排序回表次数多,不会被优先选择
orderby
- 不是所有的orderby都需要排序操作
- 只有当表是无序表时才会排序
- 只要保证从某个字段索引上取出来的行天然就是有序
- 使用联合索引(两个字段) (city,name)
- 不需要临时表
- 也不需要排序
进一步简化
- 覆盖索引:不需要回主键的索引,内容足够
- 创建需要排序的三个字段都在的联合索引
- 但是要注意索引是有维护代价的
问题
这个语句执行过程有排序过程吗?
第十七讲:如何正确地显示随机消息
MySQL中另外一种排序需求。
从单词表中随机选三个单词
内存临时表
order by rand()
- 使用explain命令查看一下
- 需要临时表
- 需要排序
- 即在临时表上排序
- 会选择全字段排序 还是 rowid排序
- 回表只是内存操作,不会导致多访问磁盘
- 会选择rowid排序
执行流程与扫描行数
- 生成临时表,
- 对主键使用rand()函数,生成随机小数,存入临时表
- 扫描行数是10000
- 按照新生成的某个字段排序
- 取前三个
- 扫描行数增加了一倍,为20000个
- 从慢查询日志(slow log)来验证
MySQL是靠什么定义“一行数据”
- 主键删掉仍然可以回表
- InnoDB会自动生成长度为6个字节的rowid
- 作为唯一标识
- 对于有主键的表,rowid就是主键
order by rand()使用了内存临时表,其排序时rowid排序方法
磁盘临时表
不是所有临时表都是内存临时表
- 文件大小大于当tmp_table_size的配置
- 内存临时表就会转为磁盘临时表
- 使用的是没有显示索引的排序
- 没有用到临时文件,使用的是新引入的排序算法
- 优先队列算法
- 之前的排序,明明只需要前三个有序,
- 使用的是归并排序
- 可是却排序了10000个,其中9997个是浪费的计算量
- 解决办法是,使用堆排序
- 由三个元素构成的堆
随机排序方法
随机排序方法1
- 取ID最大值M,最小值N
- rand()函数随机出一个M~N之间的数X
- 第一个不小于X的行,就是随机出来的结果
- 效率很高
- max(ID)和min(id)是不需要扫描的
- 但是并不是严格意义上的随机
- 因为ID并不一定是均匀分布的
- ID之间有空洞
随机排序方法2
- 解决概率不均匀的问题
- 总行数*rand(),然后取整,就可以得到一随机行
- 使用floor()函数,取整
- 然后取一行:limit Y,1
- limit Y,1
- 一个一个扫描
- 然后丢掉前Y个
- 要扫描的行数变多,执行代价比方法1高
随机排序方法3
- 使用方法2的思路,
- 取三个
- 相当于使用三次 随机排序方法2
问题
随机排序方法3中,如何减少扫描行数
第十八讲:为什么这些SQL语句逻辑相同,性能却差异巨大
案例一:条件字段函数操作
- 如果对字段作函数计算,不能使用索引
- where ‘modi’ =‘2018-7-1’ 可以用索引
- where month(modi) = 7 不可以用索引
- 快速定位的能力来源于B+树,同一层兄弟节点的有序性
- 但是month()函数,传入7之后,在树中就不知道该怎么办了
- 优化器只能放弃树搜索功能
- 优化器并不是放弃索引
- 优化器对比此索引和主键索引大小
- 选择小的索引
- 加了month()函数,优化器只能使用全索引扫描
案例二:隐式类型转换
字段类型是varchar(32),输入参数是整型
数据类型转换规则是什么
- 简单方法
- 看 select “10” > 9 的结果
- 如果是“字符串转成数字”,就是数字比较,结果应该是1
- 如果是“数字转成字符串”,就是字符串比较,结果应该是0
- 如果是字符串和数字做比较,是将字符串转换成数字
为什么有数据类型转换就要走全索引扫描
- 对于优化器来说
- 类型转换相当于调用函数
案例三:隐式字符编码转换
字符集不同,用不上索引
- 驱动表:从驱动表中取某个字段
- 被驱动表:去被驱动表中匹配该字段
- 用不了索引
- 再次触发上述的规则,类型转换相当于调用函数
- 为什么有的也用到了函数,却可以用索引
- 函数是加在了参数上
- 可以用
- 业务上优化方法
- 修改字符集
- 修改SQL语句
问题
别的类似的性能问题
第十九讲:为什么我只查一行的语句,也执行这么慢
数据库压力大导致的语句慢不在讨论范围内
第一类:查询长时间不返回
- select 语句长时间不反悔
- 表被锁住了
- 使用show processlist查看语句状态分类解决
等MDL锁
- 有一个线程在此表 持有MDL写锁或 在请求写锁。
- 而查询需要读锁
- 解决办法
- 使用命令:
- 找到进程id
- 找到谁持有MDL写锁,kill掉
等flush
- 但是flush命令执行都很快(两种)
- 说明有一个flush命令被别的命令堵住了,然后它又来堵住本select命令
- 同上的解决办法
等行锁
- 以上是表级锁,此处select命令进入引擎中
- 也是占有写锁导致的结果
- kill可以自动回滚,要找到kill的对象
- kill这个查询会自动会滚这个连接里的正在执行的线程
第二类:查询慢
- 当前读的查询,被别的线程影响了。
问题
该语句序列是如何加锁的,加的锁又是什么时候释放的
(加锁规则)
select 语句结束后,加写锁
两阶段锁:commit语句时候释放
第二十讲:幻读是什么,幻读有什么问题
InnoDB默认是可重复读:即前后一致
幻读是什么
- select加了for update使用的是当前读
- 同一个事务,两次查询,
- 后一次查询看到前一次查询没有看到的行
- 可重复读没有幻读,当前读才有
- 幻读专指,新插入的行。
- 同一行两次结果不一致不能称为幻读
幻读有什么问题
语义上
- for update时应该加锁了,破坏了加锁
数据一致性
- binlog和数据对不上
- 到底是怎么引入的数据不一致
- “执行”一条语句时,只给那一行加了锁
- 但是binlog中,只会算commit那个时间节点
- 如何改
- 在扫描过程中碰到的行也都加上写锁
- 解决了一行
- 全部加锁
- 依然阻止不了新插入的行
如何解决幻读
原因:行锁只能锁住行,不能锁住新加的记录
解决办法
- 引入间隙锁
- 锁住了两个值之间的间隙? 只能5个一组?
间隙锁
- 是大间隙 ,比如行0和行5之间,0~5都是间隙
- 和行锁有冲突的是别的行锁
- 和间隙锁有冲突关系的是“往间隙中插入一个数据”的动作
- 间隙锁之间没有冲突关系
- 修改/插入一行
- 修改/插入另一行
- 可以正常执行
next-key lock
- 间隙锁和行锁合称next-key lock
- 每个next-key lock都是前开后闭区间
带来的困扰
- 有并发,就容易碰到死锁
- 逻辑是:先加锁,再操作
- Session A 先加上 5,10的间隙锁
- Session B 也加上 5,10的间隙锁
- Session A 操作,被Session B挡住
- Session B 操作,被Session A挡住
- 把级别设为读提交,就不用考虑间隙锁了,但是要解决数据不一致的问题
- binlog格式设为row
- 如果读提交隔离级别够用,业务不需要可重复读的保证
- 读提交锁的范围更小(没有间隙锁),这么操作是可以
问题
你的MySQL配置的是什么隔离级别,为什么这么配置?
有没有碰到什么场景必须使用可重复读级别?
+
分析一个状态
第二十一讲:为什么我只改一行的语句,锁这么多
等值查询: id=7 这种
前提
- MySQL后期版本可能会改动
- 可能有bad case。别的情况
- 一共2+2+1 : 两个原则,两个优化,一个bug
加锁规则
原则
- 加锁基本单位都是next-key lock:前开后闭区间
- 查找过程中访问到的对象才会加
优化
- next-key lock退化为行锁
- 索引上等值查询,给唯一索引加锁时
- next-key lock退化为间隙锁
- 索引上等值查询,向右遍历且最后一个值不满足等值条件
bug
- 唯一索引上的范围查询会访问到不满足条件的第一个值为止
案例一:等值查询间隙锁
where id=7
- 加锁范围
- 根据原则1:(5,10]
- 根据优化2:退化为间隙锁,
- 最终范围(5,10)
- 别的事务,向其中插入id=8不行
- 修改id=10,可以
案例二:非唯一索引等值锁
关于覆盖索引的锁,“该锁的不锁,不该锁的乱锁”的感觉
- 普通索引,仅访问一条记录不会马上停下来
- 需要向右遍历,知道c=10
- 根据原则2,访问到的都要加锁,给(5,10]加锁
- 根据优化2:退化为间隙锁,(5,10)
- 使用了覆盖索引,没有访问主键,主键索引上没锁
- 但是要在7插入数据时,就会被间隙锁锁住
- lock in share mode,只锁覆盖索引
- for update时,主键索引会被锁上
- 说明锁是加在索引上
- 主键也会被锁上
- lock in share mode,加锁是,必须绕开覆盖索引
- 方法是加上不存在的字段
案例三:主键索引范围锁
关于范围查询
- 两个语句逻辑上等价,但是加锁规则不等价
案例四:非唯一索引范围锁
案例五:唯一索引范围锁bug
案例六:非唯一索引上存在“等值”的例子
案例七:limit语句加锁
案例八:一个死锁的例子
问题
第二十二讲:MySQL有哪些“饮鸩止渴”提高性能的方法
不管用什么方案,先让系统跑起来
短连接风暴
- 建立连接的成本很高
- 业务高峰期连接数暴涨
- 除了三次握手还有登录权限判断和获取数据读写权限
- 数据库压力小点时候,这些成本不明显
- 数据库压力过大,超过max_connections限制
- 若方法是调高max_connections的值
- 可能会导致线程太多
- CPU资源不够分
- 故不能如此操作
先处理掉那些占着连接但是不工作的线程
- kill_connection
- 和设置wait_timeout一样
- 一个线程空闲超过多少秒就会被断开连接
- 如何判断哪些事务外空闲连接
- show processlist
- 其中的sleep连接
- 事务具体状态
- 查information_schema库的innodb_trx表
- trx_mysql_thread_id=4
- 表示id=4的线程,还在事务中
- 断开哪些
- 事务外空闲太久的连接
- 事务内空闲太久的连接
- 命令 kill connection + id
- 被服务端主动断开
- 客户端不会马上知道
- 在客户端发起下一个请求的时候会报错
- 可能是有损的
- 不重新连接的话会一直有问题
- 客户端会以为服务端一直没有恢复
减少连接过程的消耗
- 有的业务,短时间大量申请数据库作为备用
- 让数据库跳过权限验证阶段
- 方法
- 重启数据库
- 使用-skip-grant-tables参数启动
- 风险极高
慢查询性能问题
索引没有设计好
- 紧急创建索引来解决
- 高峰期数据库被语句打挂了
- alter table语句
- 比较理想是先在备用库执行
- 在备用库上执行set sql_log_bin=off
- 不写binlog
- 执行alter table语句加上索引
- 执行主备切换
- 再去备库(主现在被切换成备库了),执行一遍
语句没写好
- 犯了错误,导致语句没有使用上索引
- 改写SQL语句, 使用query_rewrite功能
- 增加一个语句改写功能
- 增加方式(查询充血)
- 确认是否生效
选错了索引
- 应急方案
- 加上:force index
- 使用查询重写功能,给原来语句加上force index
总结
- 索引没有设计好和语句没写好可以避免
- 上线前,在测试环境,把慢查询日志打开
- 把long_query_time设为0
- 在测试表模拟线上数据,做一遍回归测试
- 观察每类语句的输出
- 特别是Rows_examined字段是否与预期一直
- 如果语句不多,手动泡一下
- 语句多,使用工具 pt-query-digest
QPS突增问题
- 某个语句QPS突然暴涨/MySQL压力过大
- bug
- 业务高峰
- 处理办法:下掉这个功能
- 由全新的业务的bug导致
- 直接把加的白名单曲调
- 使用的是单独的数据库用户
- 删掉这个用户
- 断开现有连接
- 和主题部署在一起
- 处理语句
- 使用查询重写功能,把压力最大的语句直接重写成 “select 1” 的结果返回
- 风险很高,存在副作用
- 别的功能也使用了这个SQL语句模板,会误伤
- 单独改了这一个语句,可能导致很多业务一起失败
总结
- 方案3用于止血,优先级最低
- 方案1、2依赖规范的运维体系:很多功能(虚拟化、白名单机制、业务账号分离等)
第二十三讲:MySQL是怎么保证数据不丢的?
和数据可靠性相关
前面章节:WAL(redo log、binlog)
binlog的写入机制
写入逻辑
- 事务执行时:先把日志写到binlog cache
- 事务提交时:把binlog cache写到binlog文件
binlog cache
- 同一个事务,binlog不能被拆开,一次性写入
- binlog cache的保存问题
- 系统给binlog cache分配了一片内存
- 由参数binlog_cache_size控制
- 超过这个大小,就要暂存到磁盘
- 事务提交时,执行器把binlog cache完整事务写入binlog
- 并清空binlog cache
binlog cache的保存问题
- 每个线程都有自己的binlog cache
- 但是共用一份binlog文件
- write和fsync
- write是写入内存
- fsync是持久化到磁盘
- write和fsync的时机,由sync_binlog参数控制
- sync_binlog=0
- 每次提交事务
- 只write
- sync_binlog=1
- 每次提交事务
- 只fsync
- sync_binlog=N
- 每次提交事务
- 都write,积累N个时fsync
- IO瓶颈时,把sync_binlog设置大一点
- 常见为:100~1000
- 但异常重启可能会丢失近N个事务的binlog
redo log的写入机制
- redo log要先写到redo log buffer
- redo log buffer也不是立即持久化到磁盘
- 没有提交时,因为事务没有提交
- 即使redo log丢失,也没有影响
- redo log buffer中部分日志有可能被立即持久化
- 三种状态
redolog的三种状态
三种状态
- 存放在redo log buffer中(内存中)
- 快
- 写在磁盘(page cache)中,但没有持久化
- 快
- 被持久化了
- 慢
通过参数控制
- 通过 innodb_flush_log_at_trx_commit参数
- 值为0
- 只留在内存(redo log buffer)
- 值为1
- 直接持久化到磁盘
- 值为2
- 只提交到磁盘(page cache)
redolog的写入策略
- 通过轮询来持久化
- InnoDB的后台线程,每隔一秒
- redo log buffer写到page cache
- 然后持久化
- 所以,没有commit也会有部分持久化
- 空间过1/2
- 达到innodb_log_buffer_size一半
- 主动写盘,(page cache)
- 但是没有调用fsync,没有持久化
- 事务提交
- B事务提交也可能带上A事务没提交的一起持久化
- 多个事务共用一个buffer
补充说明
- 时序上
- redolog:prepare
- binlog
- redolog:commit
- 例外
- 如果innodb_flush_log_at_trx_commit=1
- redolog:prepare阶段就要持久化
- 因为有个崩溃恢复逻辑用到了
- 加上每秒一次的轮询
- commit时就不需要fsync持久化了
- 只写入磁盘(page cache)
- 双“1”配置
- sync_binlog=1
- innodb_flush_log_at_trx_commit=1
- 一个事务完整提交前,需要等两次刷盘
- redolog prepare阶段
- binlog
- 但是为什么时间上为什么不是*2?
- 组提交
组提交机制
日志逻辑序列号(LSN)
- 单调递增
- 用来对应redo log的写入点
- 长度为改redolog的length
保证不会执行重复redolog
- 三个并发事务,长度为50+120+160
- 第一个到达的被选为leader
- LSN为最后一个事务的长度
- leader返回时,最后一个事务也得完成持久化
- leader顺带着后面两个事务一起返回
- 组提交:即几个事务一起fsync
binglog也可以组提交
- 实质上binlog是两步
- 先写到磁盘上
- 再持久化
- 多个binlog也可以一起组提交
- binglog组提交效果不如redolog效果好
- 因为redolog持久化(fsync)更快
- 组长不需要等组员,轮流着上就行
- binlog能够集合成一组的组员比较少
通过设置参数提升效果
- binglog_group_commit_sync_delay
- 延迟多少秒后调用fsync
- binglog_group_commit_sync_no_delay_count
- 积累多少次后调用fsync
- 两者是或的关系
- 达成其一就可以调用fsync
总结
WAL是如何提升性能的
- 尽管磁盘读写次数也挺多的,
- 但是这些读写都是顺序写
- 更快
- 组提交机制的优越
MySQL在IO上出现性能瓶颈,如何解决
- 设置参数
- binglog_group_commit_sync_delay
- binglog_group_commit_sync_no_delay_count
- 减少写盘次数
- 来减少故意等待
- 将sync_binlog设置为大于100~1000
- 可能异常断电会损失日志
- innodb_flush_log_at_trx_commit
- 设置为2
- 可能异常断电会损失日志
补充
- 不建议innodb_flush_log_at_trx_commit
- 设置为0,表示只存在内存中
- 写到page_cache也挺快的
- 异常断电也会损失日志
- 设置成2和设置成0性能差不多
- 但是安全性增加了
第二十四讲:MySQL是怎么保证主备一致的?
binlog可以用来归档和做主备同步。但是它是怎么做到的?
MySQL主备的基本原理
概念
- 客户端的读写都是对主库
- 但是备库将主库的所有更新都同步过来
- 在备库的本地执行
尽管备库没有被直接访问,但是要设置成只读
- 有时也需要去查,设置成只读防止误操作
- 防止切换时出错,双写
- 可以方便判断是哪个主库/备库
设置成只读了,还怎么和主库一致?
- 只读对超级用户无效
- 用与更新的线程就是超级用户权限
从主库到备库的内部流程
- 原理
- 主库A 备库B,一个update语句在主库执行流程
- 就是A执行时写binlog,B拿着binlog直接执行
- A和B之间维持着一个长连接
- A中有个线程服务于该长连接
- 在B中设置好内容
- change master命令
- 包括A的IP、端口、用户名密码、binlog位置
- 在备库B上和主库A建立连接
- start slave命令
- 备库会启动两个线程
- io_thread和sql_thread
- A按照B发来的binlog位置,读取binlog
- 发给B
- B拿到binlog
- 写本地日志,称为中转日志(relay log)
- B中的sql_thread解析日志,并执行
binlog三种格式对比
备库B可以直接执行binlog,那么binlog究竟是怎么写的
binlog有三种格式
- statement
- row
- mixed(前两种的混合)
binlog为statement情况下
- 记录的是SQL原文,包括注释
- delete带了limit,是unsafe的
- 主库和备库使用了不同的索引
- 可能会选择到不同的行
binlog为row的情况下
- 不会主备删除不同行
- binlog中没有SQL语句的原文
- 需要用解析工具解析(mysqlbinlog)
- 记录了
- 不同的表及对应参数
- 会记录被操作行的
- 所有字段信息
- 或者必要字段信息(主键)
- 取决于参数 binlog_row_image
为什么会有mixed格式的binlog
- 如果只设置为statement
- 有出错风险
- 如果只设置为row
- 太占空间
- IO时间也长
- 设为mixed,MySQL会自己判断
- 是否有可能引起主备不一致
- 选择statement或者row
- 结论
- 如果被设置成statement,一定不合理
为什么现在普遍要求设为row
方便恢复数据
delete
- 如果出错
- row格式下
- 直接insert
- 会连行数都会被记录下来
- 精准定位
- 直接恢复
insert
- 如果insert错了
- 精准定位
- 直接删除
update
- 如果update错了
- 会记录下操作前的一行
- 和操作后的一行
- 直接调换
总结
- MariaDB的Flashback工具就是基于这个原理
如果是mixed?
- 用的不多
- 可能会记录错,把row记录成statement
- 并且很多语句的执行依赖于上下文
恢复方式
- 用mysqlbinlog解析出来
- 发给MySQL执行
- 命令:
循环复制问题
结构
- M-S结构
- 一个为主库一个为备库
- 双M结构
- 互为主备
- 用的更多
- 切换时不需要修改主备关系
双M存在循环复制的问题
- 描述
- 主库A执行时写下binlog交给B执行
- B执行完又写binlog,再给A执行
- 解决办法
- 给每个库定义一个server id
- 如果server id和本库相同,丢弃
问题
除了循环复制,还有什么情况下会出现死循环?
第二十五讲:MySQL是怎么保证高可用的?
最终一致性
- 主备关系中
- 主库更新生成的索引binlog都被传到备库执行
- 备库达到和主库一致的状态
主备延迟
概念
- 主库执行完一个事务,写入binlog为T1时刻
- 备库接受完这个binlog为T2时刻
- 备库执行完这个事务,为T3时刻
- 主备延迟了T3-T1的时间
- 可以在备库上看到
- 执行show slave status
- 会显示seconds_behind_master
- 表示延迟了多少秒
seconds_behind_master是如何计算的
- binlog上有个时间字段,记录主库写入的时间T1
- 备库取出该事务的时间字段T3
- T3-T1,精确度为秒,就是主备延迟的值
- 主库的系统时间和备库的系统时间不一致
- 不会影响
- 有函数
- 会帮助把时间的差值扣掉
- 网络正常时,binlog在主备之间传输很快
- 延迟主要来自于备库的执行时间
- 主备延迟的表现通常就是
- 备库的relay log比主库binlog生产的快
主备延迟的来源
原因一
- 备库所在机器的性能比主库差
- 同一台机器上放了多个主库的备库
- 备库必须开“非双1”模式
- 各个备库都在读操作,争抢资源
原因二
- 现在这种情况比较少,因为需要主备切换,所以要对称部署
- 但还是会有延迟的现象
- 备库的压力大
- 主库直接影响业务,提供了写能力,大家用起来比较克制
- 备库的查询时,耗费了大量CPU资源
- 解决办法
- 除了备库,多接几个从库,分担读读压力
- 从库可以理解为不会主备切换的备库
原因三
- 采用了一主多从,还可能主备延迟
- 大事务
- 原因
- 主库必须等事务完成才会写入binlog
- 再传给备库
- 如果主库语句执行了10分钟
- 从库就会延迟10分钟
- 场景1
- 快满了,一次性删除太多数据
- 换成少量多次删
- 场景2
- 大表DDL
- 使用gh-ost方案
- 见13章
原因四
- 备库的并行复制能力
- 其他情况
切换策略
主备切换时,因为有主备延迟,所以有不同的切换策略
可靠性优先策略
- 流程
- 判断备库现在的seconds_behind_master
- 小于一个值,就继续,不然重试
- 把主库A改为只读
- 判断seconds_behind_master,直到=0
- 把备库B改为可读写
- 切换业务请求
- 数据库存在不可用时间
- 比较耗时间的是
- 判断seconds_behind_master,直到=0
- 所以要尽量减少不可用时间,通过
- 判断备库现在的seconds_behind_master
- 小于一个值,就继续,不然重试
可用性优先策略
概念
- 把不可用时间降到几乎为0
- 强行把操作
- 备库B改为可读写
- 切换业务请求
- 改到前面执行
- 不等主备数据同步
- 可能会出现数据不一致
例子
- 有插入4,5两条命令
- 主库A插入了4后,开始主备切换,
- 而备库B此时有延迟,还没来得及插入4,就开始插入5
- 于是备库B插入了5,然后把binlog传给A
- 然后把延迟的4插入进了B
- B插入了5-4,A插入了4-5
- 出现了数据不一致
把binlog_format设为row
- row格式会记录全部字段
- 主备同步的线程会报错
- 所以插入 4-5 和 5-4 都不会被执行
- 结论
- 使用row格式
- 数据不一致更容易发现
- mixed和statement
- 可能数据不一致不可查
- 连带着产生更多不一致
- 大多数情况下建议使用可靠性优先策略
什么情况下使用可用性优先策略
- 某个库的作用是记录日志,数据不一致可以通过binlog补
- 短暂的不一致不影响业务
- 但是业务依赖于日志系统,库不能有不可写的状态
- 可以通过降低日志系统优先级来使用 可靠性优先策略
可靠性优先策略异常切换时是什么样
- 主库A和备库B延迟30分钟
- 主库A掉电了,此时HA要切换B作为主库
- 如果是主动切换,要等到延迟小于一个值
- 但现在是被动切换,别无选择
- 不能直接切换到备库B,保持B只读
- 中转日志没有应用
- 直接主备切换
- 客户端会任认为有数据丢失
- 对于某些业务,后补是不能接受的
- 在数据可靠的情况下,必须主备延迟小,才能达到高可用性
第二十六讲:备库为什么会延迟好几个小时
用多线程,防止备库一致追不上主库
备库的并行复制能力
MySQL5.5版本的并行复制策略
按表分发策略
按行分发策略
MySQL5.6版本的并行复制策略
MariaDB的并行复制策略
MySQL5.7版本的并行复制策略
MySQL5.7.22版本的并行复制策略
第二十七讲:主库出问题了,从库怎么办?
一主多从的主备切换流程
第二十八讲:读写分离有哪些坑
做读写分离是为了分担压力,主库写,从库读。
但是存在延迟,可能正在更新时请求读了。
就会读到非最新的数据
第二十九讲:如何判断一个数据库是不是出问题了
几种检查方法
第三十讲:
第三十一讲:误删数据后除了跑路,还能怎么办
几种可能误删的情况,误删后的处理
第三十二讲:为什么还有kill不掉的语句
不是强制的,还需要到埋点,本身也需要时间,只能等
第三十三讲:我查这么多数据,会不会把数据内存打爆
不会,是边算边发的,有淘汰策略
第三十四讲:到底可不可以使用join?
join,多表查询,有驱动表和被驱动表
驱动表是全表查,被驱动表是树搜索
所以数据量小的表作为驱动表
第三十五讲:join语句怎么优化
算法
第三十六讲:为什么临时表可以重名
因为是每个线程自己的
第三十七讲:什么时候会使用内部临时表
group by的实现,从中获得的指导原则
第三十八讲:都说InnoDB好,那还要不要使用Memory引擎
Memory引擎的特性,InnoDB的适用情况
第三十九讲:自增主键为什么不是连续的
自增值的存储,在内存上,没有被指久化
可以回退,但是需要判断,影响性能,所以主动放弃了
第四十讲:Insert语句的锁为什么这么多
几种Insert语句的使用场景
第四十一讲:怎么最快的复制一张表
讨论三种拷贝表的方式,最快,最灵活……
第四十二讲:grant之后要跟着flushprivileges吗?
MySQL用户权限的存在形式,grant的执行逻辑
第四十三讲:要不要使用分区表
Server层和引擎层对分区表的处理方式
MDL锁
第四十四讲:
第四十五讲:自增ID用完怎么办
MySQL 7*24小时全年无休,这是边界情况
讨论几种处理方式