MySQL
1. 优化
1.1 慢查询
页面加载过慢, 接口测试时响应时间过长
1.1.1 如何定位
##### 开源工具
1. 调试工具: Arthas
2. 运维工具: Prometheus、Skywalking
MySQL自带慢查询
MySQL在/etc/my.cnf中设置了sql慢查询语句
1.1.2 慢查询日志分析
这条SQL语句执行地很慢该如何分析?
type
type(该语句查询用了什么类型的表)
这一列有NULL、system、const、eq_ref、range、index、all
NULL: 没有用到表
system: 查询系统中的表
const: 根据主键查询
eq-ref: 主键索引查询或唯一索引查询
ref: 索引查询
range: 范围查询
index: 索引树查询
all: 全盘扫描
explain
- 通过
key和key_len检查是否命中索引 (索引本身是否存在失效的情况) - 通过
type字段查看sql是否有进一步优化的空间,是否存在全索引扫描/全盘扫描 - 通过
extra建议判断是否出现回表的情况, 出现了可以尝试添加索引/修改返回字段来修复
1.2 索引
通过减少磁盘 IO, 帮助数据库快速定位数据的数据结构。
数据库索引是一种用于快速定位数据的数据结构。
1.2.1 B+树
在 MySQL 的 InnoDB 存储引擎中,索引通常使用
B+Tree来实现。
B+Tree 是一种多叉搜索树
其节点在物理上对应一个 page,InnoDB 默认 page 大小为 16KB。
非叶子节点只存储 key 和子节点页号,用于索引导航,而真正的数据存储在叶子节点中。
1 | Page |
查找三步走
分为 B+树层级比较找到page + 页内稀疏索引查找record (slot二分) + 顺序查找(双向链表)
1 | SELECT * |
B+树和红黑树
数据库的真正瓶颈是磁盘IO, 所以谁磁盘IO消耗小, 谁才是胜者!
由于每个 page 可以存储大量 key,因此 B+Tree 的 fanout 非常大,树高度通常只有 3 到 4 层(10亿到100亿)
而红黑树本质是二叉搜索树, 在大量数据下fanout小, 则高度大, 导致磁盘IO
叶子节点之间通过双向链表连接,因此 B+Tree 非常适合范围查询。
如果没有索引,数据库需要进行全表扫描,读取所有数据页并逐行比较,这会产生大量磁盘 IO,因此查询效率较低。
1.2.2 聚簇索引和二级索引 (数据与索引)
聚簇索引
1.数据和索引 存储在同一棵 B+Tree
2.叶子节点存储完整行数据
3.一张表只能有一个聚簇索引 (因为数据在磁盘上只能按一种顺序存储)
二级索引
1.数据和索引 分开存储
2.叶子节点存主键值
回表查询
通过二级索引找到主键值, 再通过主键索引找到行数据
索引下推
索引下推 让数据库 在扫描索引时,直接在索引层面进行过滤,而不是把数据扫描出来后再通过
WHERE子句去筛选,减少了不必要的 I/O 操作。
覆盖索引(是否回表)
如果一个查询 需要的所有字段都能从索引中直接获取,数据库就不需要再访问表数据页。
比如我用了 name=amy 这个二级索引来
select id, name
name二级索引中的叶子节点是id, 所以二级索引覆盖了我要找的所有字段
深分页查询优化
MySQL 执行 LIMIT offset,size 时需要扫描 offset + size 条记录,
MySQL 无法直接定位到第 offset 条记录,
必须从索引或数据的起点开始顺序扫描。
如何优化?
1.游标分页: 借助主键/唯一字段进行翻页 游标分页vs传统分页
1 | SELECT * FROM tb_sku LIMIT 9000000, 10; |
2.覆盖索引+子查询
在大范围扫描 且需要进行排序的情况下, 子查询尽量使用覆盖索引.
1 | 原 SQL: |
1.2.3 索引创建的原则
- 数据量大且查询频繁的表要建立索引
- 尽量使用
联合索引, 而非单列索引 (覆盖索引可减少回表次数) - 索引数量不能太多 (删改数据要维护, 多则成本大)
- 用常作为查询
条件、排序、分组的字段作为索引 - 索引内容
区分度要高, 比如地区全是北京就没必要建立地区索引 - 索引列
不能存储NULL值, 在创建表时要使用NOT NULL约束
1.2.4 索引失效的情况
1.违反最左前缀法则
比如有一索引是 id_name_address, 但是却 where id=xx and address=xx,
违反最左前缀法则
2.范围查询, 右侧字段无法使用索引
where id = 1 and name > ‘Tom’ and address = ‘广州’
name范围查询, 所以address无法使用索引
3.’%xxxx’的模糊查询
where name like ‘%Tom’
因为左侧有%,无法利用 B+Tree 的有序性,所以 索引失效。
4.在索引上进行操作运算 (计算、函数、表达式)
where substring(name, 1, 3) = ‘Tom’
因为对索引列做了函数运算,MySQL 无法直接利用原索引,所以 索引失效。
5.字符串不加单引号而引起的类型转换
比如有一索引是
phone,字段类型是varchar,但是却:
where phone = 13800138000
因为字符串字段和数字比较时会发生类型转换,可能导致 索引失效。
总结
- 违反最左前缀法则, 联合索引中断
- 范围查询之后,联合索引右侧列失效
- like 以 % 开头,索引失效
- 对索引列做函数/运算,索引失效
- 隐式类型转换,索引可能失效
1.3 优化
1.3.1 表的设计优化
- 对于字段, 根据实际情况选择合适的数值
- 设置合适的字符串类型( char 和 varchar ), char是定长效率更高, varchar可变长度效率偏低
1.3.2 SQL语句的优化
- select语句务必指明字段名称, 避免直接使用select * (因为可能会产生回表)
- SQL语句要避免索引失效的写法详见
- 尽量用union all 代替union, union会多一次过滤掉重复的, 效率低
- 避免在where子句中对字段进行表达式操作
- join能用inner join就别用left right join,
如果必须用left/right, 则小表在外, 大表在内 (减少连接次数,提高效率)
1.3.3 读写分离 主从复制
主节点负责写入操作, 从节点负责读操作, 读写分离, 避免写操作影响读操作
2. 事务
2.1 事务的特性
事务具有原子性, 一致性, 持久性, 隔离性
- 原子性A: 事务是不可分割的最小操作单元, 要么成功要么失败 undo log
- 一致性C: 事务完成时, 所有数据都保持一致状态
- 隔离性I: 多个事务同时干活,彼此别乱影响。
- 持久性D: 事务一旦提交或回滚, 它对数据库中的数据的改变是永久的 redo log
例如 A 向 B 转账 500: A 扣 500,B 加 500。 原子性:两步必须一起成功或一起失败。 一致性:转账前后系统总金额不变。 隔离性:转账过程中不能被其他事务干扰。 持久性:转账提交后,即使数据库重启数据仍然存在。
2.2 并发事务问题与隔离级别
2.2.1 脏读 & 幻读 & 不可重复读
脏读: 事务B读到了事务A未提交的数据 –> 读未提交
不可重复读: 事务A先后读取同一条记录, 两次读取到的数据却不一样 –>
幻读: 事务查询数据时, 没有发现对应的数据行
插入数据时, 却发现已经存在了
2.2.2 RU & RC & RR & S
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| Read Uncommitted | √ | √ | √ |
| Read Committed | × | √ | √ |
| Repeatable Read(默认) | × | × | √ |
| Serializable | × | × | × |
| 隔离级别越高, 性能越差 |
2.2.3 undo log & redo log
redo log (持久性)
内存层的引入
由于 磁盘 I/O 的性能远低于内存,
如果每次执行 update 操作都直接将数据页写入磁盘,会产生大量随机 I/O
因此 MySQL 在 数据文件与磁盘之间加入了内存层(Buffer Pool):
1 | 数据修改 |
redo log引入
1.引入原因
如果 MySQL 在数据页刷盘之前宕机,内存中的修改就会丢失。
为了解决这个问题,MySQL 引入了 redo log(重做日志)。
redo log 的核心思想是 WAL(Write-Ahead Logging):
在真正修改数据文件之前,先将修改操作记录到日志中。
update即时的更新到磁盘是随机访问式的, 但是redolog像清单一样顺序地写入修改操作, 效率更高
| 写入方式 | IO类型 |
|---|---|
| 数据页更新 | 随机写 |
| redo log | 顺序写 |
2.redolog的组成
redolog = redo log buffer(内存) + redo log file(磁盘)
过程如下:
修改 Buffer Pool 数据页
↓
生成 redo log
↓
写入 redo log buffer
↓
刷入 redo log file
↓
事务提交
3.redolog的写入方式
checkpoint: 之前的 redo log ,对应的数据页 ,已经写入磁盘, 其前面的日志是可以覆盖的
redo log file 是固定大小,采用循环写入。当日志写满后,只能覆盖已经完成 checkpoint 的日志。
undo log (原子性)
undo log 是 InnoDB 的回滚日志,用于记录数据修改前的旧值。
它通过记录与数据页完全相反的操作实现以下两个功能:
第一是支持事务回滚,实现事务的原子性;
第二是为 MVCC 提供历史版本,实现多版本并发控制.对于insert, 产生的undo log日志, 只在回滚时需要, 一旦事务提交即可被立即删除
而update、delete, 产生的undo log日志不仅在回滚时需要, MVCC也需要, 不会立即被删除
| redo log | undo log | |
|---|---|---|
| 类型 | 物理日志 | 逻辑日志 |
| 作用 | crash recovery | rollback + MVCC |
| 保证 | 持久性 | 原子性 & 一致性 |
| 写入方式 | 顺序写 | 随数据修改生成 |
2.2.4 MVCC
MVCC详解
MySQL InnoDB 的事务隔离是通过 锁和 MVCC 实现的。
MVCC 是多版本并发控制,它通过维护数据的多个版本,使读写操作不冲突。
MVCC 的实现依赖三个核心组件:
第一是 隐藏字段,包括 trx_id 和 roll_pointer,分别记录修改事务和指向旧版本。
第二是 undo log,用于保存数据的历史版本,并通过roll_pointer形成版本链。
第三是 ReadView,用于在查询时判断当前事务可以看到哪个版本的数据。
查询时会沿着版本链查找,通过 ReadView 的规则判断版本是否可见。
如果版本不可见,就继续向旧版本查找,直到找到可见版本。
在 RC 隔离级别下,每次查询都会生成新的 ReadView
在 RR 隔离级别下,只在第一次查询生成 ReadView,之后复用,从而实现可重复读。
3. 主从同步原理
MySQL主从复制的核心就是binlog
binlog记录了所有DDL & DML语句, 但不包括数据查询(select, show)等语句
- 主库在
(DDL & DML)事务提交时, 会把数据的变更记录在binlog - 从库读取 binlog 写入到从库的中继日志relay log
- 从库重做relay log的事件, 并记录数据
4. 分库分表
分库分表
水平拆分一般指的是把数据行拆开, 而垂直拆分一般指的是把字段拆开
分表: 比如原来只有:
order
后来拆成:
order_0
order_1
order_2
order_3
通常还在同一个库里,这叫 水平分表。
垂直分表: 不同字段访问频率不同, 可以
按频率拆分字段, 经常访问的字段组合在一起例如用户表:
user(id, name, age, phone, avatar, intro, address, create_time)
你发现很多字段访问频率不一样,可以拆成:
user_base(id, name, age, phone, create_time)
user_profile(id, avatar, intro, address)
特点:
1. 冷热数据分离
2. 减少IO过度争抢, 两表互不影响
垂直分库: 把不同业务模块拆到不同库。
水平分库和水平分表都是靠%的方式来决定去哪存/找
分库后的问题
分布式事务的一致性问题
跨节点关联查询
跨节点分页、排序
主键避重
解决方式: 中间件MyCat、Sharding-sphere