前言
MySQL 数据库进阶 0611 完成 0620
串起来的内容简述。
MySQL 事务中 redo log、undo log 理解,undo log 实现 AC,同时 MVCC(RC、RR)下基于版本链创建规则实现 I,redo log 实现 D;索引对组合索引最左前缀研究,基本掌握索引优化思路;引擎 InnoDB buff pool 中脏页、空闲页、LRU 链表与 checkpoint 联系;隔离级别 RR 下 X 锁 SQL 分析,并与索引类型、是否命中三者组合;调优方案中慢查询、explain 不同列、profile 性能实操;分库分表、集群等具有实战意义的概念。
没有想象中简单,甚至一时不知从何开始笔记,难熬的几天。参考书籍,从 MySQL 必知必会 -> 高性能 MySQL,即基础篇 -> 进阶篇。加锁问题多 session 事务跑 SQL,其设计逻辑清晰后可解决(博主水平有限,若有纰漏,欢迎探讨);再者包括 SQL 语句性能调优,从慢查询日志、explain 到 profile;以及对集群主从复制,中间件实现分库分表、读写分离的理解(由于没在虚拟机,必要时候再学,以补充 MyCAT 集群实战,包括一主一从、高可用双主集群),内容如下:
MySQL 数据库基础 0606 完成 0610
出发点是先有体系,再理解底层。0606 搭建环境,熟悉 MySQL 语法及规范,本地连接云服务器,解决 DataGrip 关键字默认 upper、console 无法中文输入等问题。紧接着 down 例子,练手过基础内容,最终同步的整理笔记完成。
除数据库以外,知识体系完整,较夯实。包括接触数据库锁概念,发现其等同进程间通信 IPC cond 与 mutex 对数据加锁,故死锁问题考虑,初步判断,底层实现逻辑相同(理解相同点、不同点)。进度较快,但计划时间有限,提醒自己务必注重学习效率。
计划——下一阶段 MySQL 进阶(包括 MySQL 架构、引擎&事务&索引、锁、锁&分库分表、集群、高可用),深入探索底层原理,以及之后 Redis、MongoDB 等择其一 Redis,理解非关系型数据库,与 MySQL 关系型数据库对比。数据库整体目标,首先具备实战能力,其次重点理解、掌握知识点:
- MySQL 架构篇
- 事务篇
- 索引篇
- InnoDB 内存篇
- 锁篇
- 性能调优篇
- 分库分表篇
- 集群篇
第一章 MySQL 架构篇
-
MySQL 物理文件
-
数据文件
- datadir /var/lib/mysql
- 索引文件
- 记录文件
-
日志文件
- 慢查询日志
- bin log 日志
- 错误日志
- 通用查询日志
-
-
MySQL 软件架构
- server 层
- 连接器
- 分配连接
- 管理连接池
- 分析器
- 词法分析
- 语法分析
- 优化器
- 优化我们的 SQL 语句(优化索引)
- 选择合适的查询方式(考虑性能,即索引)
- 执行器
- 连接器
- 存储引擎层(可插拔式)
- MyISAM
- InnoDB(默认)
- server 层
-
MySQL 执行流程
-
MySQL 存储引擎
- MyISAM(无事务、无行锁、插入效率高)
- InnoDB(事务、行锁)
-
InnoDB 存储引擎
- 物理结构
- 系统表空间(ibdata1 文件)表空间 -> 段 -> 区(1M)-> 页(16K)-> 行(每页至少存储两行)
- 索引段
- 数据段
- 回滚段(undo log)
- 双写
- 词典
- 用户表空间(InnoDB).ibd 文件
- 索引
- 数据
- redo log(持久化)ib_logfile0、ib_logfile1文件
- 主要目的:为了保证事务的持久性。只有发生了数据,redo log 才发生作用,否则,它一直只是一个保底方案
- 系统表空间(ibdata1 文件)表空间 -> 段 -> 区(1M)-> 页(16K)-> 行(每页至少存储两行)
- 内存结构
- buffer pool
- data page
- index page
- change buffer
- 自适应哈希
- 词典
- redo log buffer
- buffer pool
- 物理结构
第二章 事务篇
-
事务概念
- 原子性(atomicity):事务最小工作单元,要么全成功,要么全失败
- 一致性(consistency):事务开始和结束后,数据库的完整性不会被破坏
- 隔离性(isolation):不同事务之间互不影响,四种隔离级别 RU(读未提交)、RC(读已提交)、RR(可重复读)、SERIALIZABLE(串行化)
- 持久性(durability):事务提交后,对数据是永久性的,即使系统故障页不会丢失
-
隔离级别
- 未提交读(read uncommitted,RU)
- 并发导致的问题,脏读:一个事务读取到另一个事务未提交的数据
- 已提交读(read committed,RC)
- 并发导致的问题,不可重复读:一个事务因读取到另一个事务已提交的 update。导致对同一条记录读取两次以上的结果不一致
- 可重复读(repeatable read,RR)
- 并发导致的问题,幻读:一个事务因读取到另一个事务已提交的 insert into 数据或者 delete 数据。导致对同一张表读取两次以上的结果不一致
- 串行化(serializable)
- 以上 3 种隔离级别都允许对同一条记录进行“读-读”、“读-写”、“写-读”的并发操作,如果我们不允许“读-写”、“写-读”的并发操作,可以使用 serializable 隔离级别。理解成 LBCC(lock based concurrency control),读加排他锁,故读为当前读,而非快照读。
- 未提交读(read uncommitted,RU)
-
持久性是如何实现的?
- 使用 redo log 文件,通过 WAL 机制 + force-log-at-commit 机制
- 内存数据落盘
- 脏页落盘
- checkpoint 机制
- 清晰
- 模糊
- 定时
- redo log 不够用(异步、同步)
- 空闲数据页不够
- 脏页太多
- double write 双写机制
- InnoDB 和磁盘交互的最小数据单位是 page,page 是 16K
- 操作系统和磁盘交互的最小数据单位也是 page,但是这个 page 的大小是 4K
- 在 InnoDB 和磁盘交互的环节中,肯定涉及到操作系统,也就是说一次 InnoDB 写入操作,会伴随 4 次操作系统写入。在这个过程中,很有可能出现“部分页丢失”
- 为了解决以上问题,才加上了 double write 机制
- 该日志是利用了日志文件顺序写入比数据文件随机写入效率更高的原因
- redo log buffer 落盘
- force-log-at-commit 机制
- 0
- 1(事务提交时,将重做日志写入文件系统缓存,内核态调用 fsync,将文件系统缓存的数据真正写入磁盘存储。默认,舍弃性能但安全)
- 2
- force-log-at-commit 机制
- checkpoint 机制
- 脏页落盘
-
一致性和原子性是如何实现的?
- undo log(既能完成事务的回滚操作,还能完成事务的隔离性实现,比如使用 MVCC 机制去解决隔离性问题)
-
MVCC 机制如何实现?(讨论的 MVCC 是在 RC、RR 隔离级别下,执行 select 操作后访问记录版本链过程)
-
undo log 版本链
-
read view(活跃事务 ID,按照顺序存放,最低事务 ID(up),最高事务 ID(low))
- RC:每次 select 语句,都会生成 read view,该视图都是取的此时的活跃事务 ID
- RR:只有在事务中第一次调用 select 语句时,才会生成对应的 read view
-
MVCC 下的读操作
- 快照读:简单的 select 操作,属于快照读,不加锁
- 当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。加行写锁,读当前版本
第三章 索引篇
-
索引分类
- 主键索引
- 普通索引
- 唯一索引
- 组合索引
- 覆盖索引
- 前缀索引
-
索引使用
-
索引的数据结构
- hash 索引
- B+ 树
- 二叉树
- 平衡二叉树
- B 树(多叉树)
-
B+ 树的特点
- 叶子节点存储数据
- 非叶子节点只存储索引数据,也就是索引的键值
-
InnoDB 的索引结构(B+ 树)
- 聚簇索引(主键索引)
- 索引和数据在一棵树
- 每张 InnoDB 表有且只有一个聚集索引,那它是怎么选择索引的呢
- 一般来说,将通过 PRIMARY KEY 作为聚簇索引的索引列,也就是通过 PRIMARY KEY 聚集数据(转自《高性能MySQL》)
- 如果没有定义 PRIMARY KEY,将会用第一个 UNIQUE 且 NOT NULL 的列来作为聚集索引
- 如果表没有合适的 UNIQUE 索引,会内部根据行 ID 值生成一个隐藏的聚簇索引 GEN_CLUST_INDEX
- 非聚簇索引
- 索引和数据不在一棵树
- 主键索引
- 次要索引(辅助索引)
- 聚簇索引(主键索引)
-
InnoDB 每张表必须有一个主键索引
-
覆盖索引
- 辅助索引树上包含所有 select 语句需要展示的列信息,此时就是完成了覆盖索引
- 覆盖索引,是为了解决回表查询的问题
-
组合索引
- 多列组成一个组合索引
- 列与列之间是有顺序的
- 建议建立组合索引,因为按照最左前缀匹配原则(使用组合索引查询时,MySQL 回一直向右匹配直至到范围查询【>、<、is null、between、in、like】就停止匹配),一个组合索引,相当于建立了多个索引树
-
索引下推 ICP(index condition pushdown)
-
大家有没有一个疑问,MySQL 在使用组合索引在检索数据时是使用最左前缀原则来定位记录,那不满足最左前缀的索引列,MySQL 会怎么处理?
- 不使用 ICP,不满足最左前缀的索引条件的比较在 server 层进行,非索引条件的比较在 server 层进行
- 使用 ICP,所有的索引条件的比较是在存储引擎层进行的,非索引条件的比较在 server 层进行
- 对比使用 ICP 和不使用 ICP,可以看到使用 ICP 可以有效减少回表查询次数和返回给服务层的记录数,从而减少了磁盘 IO 次数和服务层与存储引擎的交互次数
-
使用 explain 工具,查看执行计划,extra 列中的“Using index condition”执行器表示使用了索引条件下推 ICP,若只有“Using where”执行器表示没有使用索引条件下推 ICP。具体实现,见下一篇章——InnoDB 内存篇
-
-
LRU(least recently used)
-
索引优化建议
第四章 InnoDB 内存篇
buffer pool
page(页):数据页、索引页、undo 页
buffer pool:内存初始化的时候,pool 初始化大小:8192 个页(空闲页)
-
total pages:总页数(默认 8192 页)
-
空闲页链表(free page):未被使用的页
-
LRU 链表(data page):已使用的页(从磁盘中加载出来的数据,都会占用空闲页)
-
脏页链表(dirty page):已使用的页里面,有些页面数据已经被修改,所以产生的脏页也会单独存储
fuzzy checkpoint:
- 空闲页不足 100(free page),出发 LRU 链表(data page)的清除工作
- 脏页太多(dirty page)
LRU 链表:young(八分之五)、old(八分之三),为了解决劣币驱逐良币的现象
where 条件(结合组合索引的最左前缀匹配原则来理解)
- index key: InnoDB 存储引擎
- index filter: ICP 开启,则下推到 InnoDB 中过滤;ICP 未开启,则在 MySQL server 层过滤。5.6 之后才支持 ICP
- table filter: MySQL server 层
- explain 计划表中,有一列(filtered),就是显示从 InnoDB 中返回到 MySQL server 层数据在 MySQL server 层进行过滤的比例
- 例子:在 RR 隔离级别下,针对一个复杂的 SQL,首先需要提取其 where 条件
- index key 确定的范围,需要加上 gap 锁
- index filter 过滤条件,视 MySQL 版本是否支持 ICP,若支持 ICP,则不满足 index filter 的记录,不加 X 锁,否则需要 X 锁
- table filter 过滤条件,无论是否满足,都需要加 X 锁
- 结论,最后选取出满足条件的记录,但是加锁数量,远远大于满足条件的记录数量
第五章 锁篇
全局锁
表级锁
读锁(共享锁):S
写锁(排他锁):X
S + S 不堵塞,其它都堵塞
是由 MySQL server 层实现的,不是存储引擎层实现的
MDL:元数据锁,MySQL 自动加的,我们人为不能加 MDL
- 读锁:CRUD 操作,也就是对于记录进行操作
- 写锁:ALTER 等操作,对于结构进行操作
行级锁
- 存储引擎实现,基本上 MySQL 自带的存储引擎里面,只有 InnoDB 实现了行锁
- 行级锁,通过对索引项加锁实现,如果没有主键索引,也没有辅助索引,也就是相当于加上了全表的表级锁
- 行锁总结——行锁由存储引擎实现,更进一步地理解,通过对索引项加锁实现
- 增删改(insert into、delete、update)默认加上行级 X 锁
- 查询(select)默认加上行级 S 锁
- 加锁规则(默认隔离级别 RR 才可能加间隙锁,RC 不会加间隙锁。下述讨论除了特别标识,均 RR 隔离条件)
- 记录锁(record lock):对索引树上的一条记录加锁
- 间隙锁(gap lock):对于一条记录的前后间隙加锁(左开右闭区间)
- 临键锁(next-key lock):记录锁 + 间隙锁,其中的间隙锁(左开右闭区间)
- 默认情况下,InnoDB 使用 next-key lock 来锁定记录。select ··· for update 加行级 X 锁
- 但当查询的索引包含唯一属性(包括主键索引、唯一索引)的时候,next-key lock 会进行优化,将其降级为 record lock,即仅锁住索引本身,不是范围。其他场景如下:
- 场景 1:使用 unique index 精确匹配(=),且记录存在 -> 退化成的锁类型 record lock
- 场景 2:使用 unique index 精确匹配(=),且记录不存在 -> 退化成的锁类型 gap lock
- 场景 3:使用 unique index 范围匹配(< 和 >)-> 退化成的锁类型 record lock + gap lock
- 辅助索引(不包含唯一属性,即非唯一索引)
- 等值查询(=)命中记录场景:命中记录的辅助索引加记录锁、两侧加间隙锁(即 record lock + gap lock),对应主键索引只加记录锁
- 其他场景同上,参考场景 2、场景 3
- 非主键索引、非辅助索引
- 相当于加上了全表的表级锁,RR 锁所有记录和间隙(此处补充 RC,锁所有记录)
- 行锁分析实战——以下 SQL 语句加什么锁?(给定大前提为命中,且 server 层不做优化)
- SQL 1: SELECT * FROM t WHERE id = 10;
- 非 serializable 不加锁,因为 MySQL 是使用多版本并发控制的,读不加锁。serializable 下加锁。
- SQL 2: DELETE * FROM t WHERE id = 10;
- 对 id = 10 的记录加写锁(主键索引)。
- 分析(答案不完全):
- 前提一:id 列是不是主键?
- 前提二:当前系统的隔离级别是什么?
- 前提三:id 列如果不是主键,那么 id 列上有索引吗?
- 前提四:id 列上如果有二级索引,那么这个索引是唯一索引吗?
- 前提五:两个 SQL 的执行计划是什么?索引扫描?全表扫描?
- 分析组合:
- 组合一:id 主键 + RC
- 组合二:id 唯一索引 + RC
- 组合三:id 非唯一索引 + RC
- 组合四:id 无索引 + RC
- 组合五:id 主键 + RR
- 组合六:id 唯一索引 + RR
- 组合七:id 非唯一索引 + RR
- 组合八:id 无索引 + RR
- 组合九:serializable
- 多 session 验证后,简述如下:情况比较复杂,给定大前提为命中(其实还有未命中、范围的组合,此处不考虑)。先说默认隔离级别 RR 情况,组合五和组合六均为记录锁,组合七辅助索引记录锁 + 间隙锁(左开右闭)、对应主键索引加记录锁,组合八全表记录锁、间隙锁。再说隔离级别 RC 情况,去掉所有间隙锁即可。
- SQL 1: SELECT * FROM t WHERE id = 10;
死锁
-
死锁场景 1:单主键索引。会话 1 锁定记录,会话 2 锁定另一个记录。之后会话 1 操作会话 2 锁定的记录,会话 2 操作会话 1 锁定的记录,造成互相等待。
-
死锁场景 2:主键索引 + 两个辅助索引。辅助索引树 1 本应锁定两条,辅助索引树 2 本应锁定两条。对应主键索引为相同记录,锁定记录的先后顺序情况,辅助索引树 1 和辅助索引树 2 的第二条锁定互相等待。(RC 替代 RR 来简化场景,帮助理解)
如何避免死锁?
- 注意程序的逻辑
- 根本的原因是程序逻辑的顺序,最常见的是交叉更新
- 保持事务的轻量
- 越是轻量的事务,占有越少的锁资源,这样发生死锁的几率就越小
- 提高运行的速度
- 避免使用子查询,尽量使用主键
- 尽量快提交事务,减少持有锁的时间
- 越早提交事务,锁就越早释放
第六章 性能调优篇
性能优化的思路
- 首先需要使用【慢查询日志】功能,去获取所有查询时间比较长的 SQL 语句
- EXPLAIN 查看执行计划,查看有问题的 SQL 的执行计划
- 针对查询慢的 SQL 语句进行优化
- 使用【show profile[s]】查看有问题的 SQL 的性能使用情况
- 调整操作系统参数优化
- 升级服务器硬件
慢查询日志(重要)
-
慢查询日志介绍
- 数据库查询快慢是影响项目性能的一大因素,对于数据库,我们除了要优化 SQL,更重要的是得先找到需要优化的 SQL。MySQL 数据库有一个“慢查询日志”功能,用来记录查询时间超过某个设定值的 SQL 语句,这将极大成都帮助我们快速定位到问题所在
- 至于查询时间的多少才算慢,每个项目、业务都有不同的要求
- MySQL 的慢查询日志功能默认是关闭的,需要手动开启
-
开启慢查询功能
-
查询是否开启慢查询功能
-
SHOW VARIABLES LIKE '%slow_query%'; # 查看慢查询是否开启,以及文件所在位置 SHOW VARIABLES LIKE '%long_query_time%'; # 查看慢查询当前时间阈值
-
-
临时开启慢查询功能
-
在 MySQL 执行 SQL 语句设置,但是如果重启 MySQL 的话将失效
-
SET GLOBAL SLOW_QUERY_LOG = on; # 当前采用的开启方式 SET SESSION LONG_QUERY_TIME = 1;
-
-
永久开启慢查询
-
这里的配置重点是我找了好久,在 /etc/ 目录下并没有找到 my.cnf 文件(其他在 /etc/my.cnf 的操作,与该步骤同),也没找到替代文件 my-default.cnf 文件。由于下载的 5.7 最新版本(5.7.34),就在想是不是版本变动,后来才发现,从 5.7.18 开始官方不再二进制包中提供 my-default.cnf 文件。具体参考:https://dev.mysql.com/doc/refman/5.7/en/binary-installation.html,总结如下,以前的需要配置 /etc/my.cnf 的操作,配置 /etc/mysql/mysql.conf.d 即解决
-
所以修改 /etc/mysql/mysql.conf.d/mysqld.cnf 配置文件,重启 MySQL,这种永久生效
-
# 可见文件注释语句,取消注释并按自己的需求配置 # Here you can see queries with especially long duration # slow_query_log = 1 # slow_query_log_file = /var/log/mysql/mysql-slow.log # long_query_time = 2 # log-queries-not-using-indexes
-
# 重启 MySQL 数据库 sudo service mysql restart
-
-
-
慢查询日志格式
-
SELECT sleep(3); # 创建大于阈值 1s 的查询语句
-
# Time: 2021-06-17T05:54:05.120219Z # User@Host: root[root] @ [115.236.71.98] Id: 450 # Query_time: 3.000247 Lock_time: 0.000000 Rows_sent: 1 Rows_examined: 0 use information_schema; SET timestamp=1623909245; /* ApplicationName=DataGrip 2021.1.3 */ SELECT SLEEP(3);
-
格式说明
- 第一行,SQL 查询执行的具体时间
- 第二行,执行 SQL 查询的连接信息,用户和连接 IP
- 第三行,记录了一些我们比较有用的信息,如下解析
- Query_time,这条 SQL 执行的时间,越长则越慢
- Lock_time,在 MySQL 服务器阶段(不是在存储引擎阶段)等待表锁时间
- Rows_sent,查询返回的行数
- Rows_examined,查询检查的行数,越长就当然越费时间
- 第四行,使用 information_schema 数据库表
- 第五行,设置时间戳,没有实际意义,只是和第一行对应执行时间
- 第六行及后面所有行(第二个 # Time 之前),执行的 SQL 语句记录信息,因为 SQL 可能会很长
-
-
分析慢查询日志的工具
-
使用 mysqldumpslow 工具,mysqldumpslow 是 MySQL 自带的慢查询日志工具
-
# -s:表示按照何种方式排序 # -t:表示 top n,即返回前面多少条的数据 # -g:后面可以写一个正则匹配模式,大小写不敏感 mysqldumpslow -s t -t 10 -g 'SLEEP' /var/lib/mysql/aliyun-slow.log # 终端执行
-
# mysqldumpslow 工具查询结果 Reading mysql slow query log from /var/lib/mysql/aliyun-slow.log Count: 1 Time=3.00s (3s) Lock=0.00s (0s) Rows=1.0 (1), root[root]@[115.236.71.98] /* ApplicationName=DataGrip N.N.N */ SELECT SLEEP(N) Died at /usr/bin/mysqldumpslow line 167, <> chunk 1.
-
EXPLAIN 查看执行计划(重要)
-
EXPLAIN 命令介绍
- MySQL 提供了一个 EXPLAIN 命令,它可以对 SELECT 语句的执行计划进行分析,并输出 SELECT 执行的详细过程,以供开发人员针对性优化
- 使用 EXPLAIN 这个命令来查看一个 SQL 语句的执行计划,查看该 SQL 语句有没有使用索引,有没有做全表扫描,这些都可以通过 EXPLAIN 命令来查看
- 可以通过 EXPLAIN 命令深入了解 MySQL 的基于开销的优化器,还可以获得很多可能被优化器考虑到的访问策略的细节,以及当运行 SQL 语句时哪种策略预计会被优化器采用
-
EXPLAIN 命令使用
- EXPLAIN 命令用法十分简单,在 SELECT 语句前加上 EXPLAIN 就可以了,例如 EXPLAIN SELECT * FROM account;
-
EXPLAIN 各列的含义
- id:SELECT 查询的标识符,每个 SELECT 都回自动分配一个唯一的标识符
- select_type(重要):SELECT 查询的类型
- table:查询的是哪个表
- partitions:匹配的分区
- type(重要):join 类型
- possible_keys:此次查询中可能选用的索引
- key:此次查询中确切使用到的索引
- key_len:查看组合索引,用了哪几个列
- ref:哪个字段或常数与 key 一起被使用
- rows:显示此查询以供扫描了多少行,这个是一个估计值
- filtered:表示 server 层对存储引擎层返回记录进行筛选的估计百分比,即有效记录数百分比
- Extra(重要):额外的信息
-
例子脚本
-
CREATE TABLE tuser ( id int PRIMARY KEY AUTO_INCREMENT, name varchar(100), age int, sex char(1), address varchar(100) ); ALTER TABLE tuser ADD INDEX idx_name_age(name(100), age); ALTER TABLE tuser ADD INDEX idx_sex(sex(1)); INSERT INTO tuser(id, name, age, sex, address) VALUES (1, 'laohu', 20, '1', '北京'); INSERT INTO tuser(id, name, age, sex, address) VALUES (2, 'xuance', 16, '1', '上海'); INSERT INTO tuser(id, name, age, sex, address) VALUES (3, 'hanxin', 34, '1', '杭州'); INSERT INTO tuser(id, name, age, sex, address) VALUES (4, 'yuji', 26, '2', '广州'); INSERT INTO tuser(id, name, age, sex, address) VALUES (5, 'daji', 18, '2', '上海'); # 例如对上表跑以下 SQL,查看结果 EXPLAIN SELECT * FROM tuser WHERE id = 1; # SELECT * FROM tuser a; # SELECT * FROM tuser b WHERE b.id = a.id; EXPLAIN SELECT * FROM tuser a LEFT JOIN tuser b ON a.id = b.id; # 如上 EXPLAIN SELECT * FROM tuser WHERE name = 'laohu' AND age = 20; EXPLAIN SELECT * FROM tuser WHERE sex = '1'; EXPLAIN SELECT * FROM tuser; EXPLAIN SELECT id, name, age FROM tuser; EXPLAIN SELECT * FROM tuser WHERE name = 'laohu' ORDER BY sex; # Using filesort 需要优化
-
SQL 语句优化(重要)
-
索引优化
- 未搜索字段(where 中的条件)、排序字段、select 查询列、创建合适的索引、不过要考虑数据的业务场景:查询多还是增删多?增删多面临索引被修改 -> 影响性能,查询多加索引
- 尽量建立组合索引并注意组合索引的创建顺序,按照顺序组织查询条件,尽量将筛选粒度大的查询条件放到最左边
- 尽量使用覆盖索引,select 语句中尽量不要使用 *
- order by、group by 语句要尽量使用到索引
- 索引长度尽量短,短索引可以节省索引空间,使查找的速度得到提升,同时内存中也可以装载更多的索引键值。太长的列,可以选择建立前缀索引
- 索引更新不能频繁,更新非常频繁的数据不适宜建索引,因为维护索引的成本大,同上增删多
- order by 的索引生效,order by 排序应该遵循最佳左前缀查询,如果是使用多个索引字段进行排序,那么排序的规则必须相同(同时升序或者降序),否则索引失效
-
LIMIT 优化(MySQL 特有的关键字)
-
如果预计 select 语句的查询结果是一条,最好使用 LIMIT 1,可以停止全表扫描
-
SELECT * FROM tuser WHERE name = 'laohu'; # username 没有建立唯一索引 SELECT * FROM tuser WHERE name = 'laohu' LIMIT 1;
-
-
处理分页会使用到 LIMIT,当翻页到非常靠后页面的时候,偏移量会非常大,这时 LIMIT 的效率会非常差
-
SELECT * FROM (SELECT * FROM tuser WHERE id > 1000000 AND id <1000500 ORDER BY id) t LIMIT 0, 20
-
-
-
其他查询优化
- 小表驱动大表,建议使用 left join 时,以小表关联大表,因为使用 join 的话,第一张表是必须全扫描的,以少关联多就可以减少这个扫描次数
- 避免全表扫描,MySQL 在使用不等于【!=、<>】的时候无法使用索引导致全表扫描。在查询的时候,如果对索引使用不等于的操作将会导致索引失效,进行全表扫描
- 避免 MySQL 放弃索引查询,如果 MySQL 估计使用全表扫描要闭使用索引快,则不使用索引(最典型的场景,数据量少的时候)
- join 两张表的关联字段最好都建立索引,而且最好字段类型是一样的
- where 条件中尽量不要使用 not in 语句(建议使用 not exists)
- 合理利用慢查询日志、explain 执行计划查询、show profile 查看 SQL 执行时资源使用情况
profile 分析语句
-
介绍
- Query Profiler 是 MySQL 自带的一种 query 诊断分析工具,通过它可以分析出一条 SQL 语句的硬件性能瓶颈在什么地方
- 通常我们是在使用 explain,以及 slow query log 都无法做到精确分析,但是 Query Profiler 却可以定位出一条 SQL 语句执行的各种资源消耗情况,比如 CPU、IO 等,以及该 SQL 执行锁耗费的时间等。该工具在 MySQL 5.0.37 以及以上版本中实现
- 默认情况下,MySQL 的该功能没有打开,需要自己手动开启
-
开启 profile 功能
-
profile 功能由 MySQL 会话变量:profiling 控制,默认是 OFF 关闭状态
-
查看是否开启了 profile 功能
-
SELECT @@profiling; # 或者 SHOW VARIABLES like '%profil%';
-
-
开启 profile 功能
-
SET profiling = 1; # 1 是开启, 0 是关闭
-
-
-
profile 使用
-
SHOW PROFILE; SHOW PROFILES; SHOW PROFILE CPU, BLOCK IO; SHOW PROFILE ALL;
-
-
performance_schema 性能分析(当前发展,进阶选择)
- 用于监控 MySQL server 在一个较低级别的运行过程中的资源消耗、资源等待等情况
服务器层面优化
-
缓冲区优化(较重要)
-
将数据保存在内存中,保证从内存读取数据
-
设置足够大的 innodb_buffer_pool_size,将数据读取到内存。建议设置为总内存的 3/4 或者 4/5
-
怎么确定 Innodb_buffer_pool_size 足够大。数据是从内存读取而不是硬盘?
-
SHOW GLOBAL STATUS LIKE 'innodb_buffer_pool_pages_%'; # 查询结果 # Innodb_buffer_pool_pages_data, 594 # Innodb_buffer_pool_pages_dirty, 0 # Innodb_buffer_pool_pages_flushed, 5326 # Innodb_buffer_pool_pages_free, 7597 # 为 0 则表示 buffer pool 已用完 # Innodb_buffer_pool_pages_misc, 1 # Innodb_buffer_pool_pages_total, 8192
-
-
-
-
降低磁盘写入次数(较重要)
- 对于生产环境来说,很多日志是不需要开启的,比如:通用查询日志、慢查询日志、错误日志
- 使用足够大的写入缓存
- 推荐 innodb_log_file_size 设置为 0.25 * innodb_buffer_pool_size
- 设置合适的 innodb_flush_log_at_trx_commit,和日志落盘有关系
-
MySQL 数据库配置优化(较重要)
- 表示缓冲池字节大小,推荐值为物理内存的 50%~80%,innodb_buffer_pool_size
- 用来控制 redo log 刷新到磁盘的策略,innodb_flush_log_at_trx_commit = 1
- 每提交 1 次事务同步写到磁盘中,可以设置为 n,sync_binlog = 1
- 脏页占 innodb_buffer_pool_size 的比例时,触发刷脏页到磁盘。推荐值 25%~50%,innodb_max_dirty_pages_pct = 30
- 后台进程最大 IO 性能指标,默认 200,如果 SSD,调整为 5000~20000,innodb_io_capacity = 200
- 指定 innodb 共享表空间文件的大小,innodb_data_file_path
- 慢查询日志的阈值设置,单位秒,long_query_time = 0.3
- MySQL 复制的形式,row 为 MySQL 8.0 的默认形式,binglog_format = row
- 调高该参数则应降低 ineravtive_timeout、wait_timeout的值,max_connections = 200
- 过大,实例恢复时间长;过小,造成日志切换频繁,innodb_log_file_size
- 全量日志建议关闭,默认关闭,general_log = 0
-
操作系统优化
- 内核参数优化
- 增加资源限制
- 磁盘调度策略
-
服务器硬件优化
-
提升硬件设备,例如选择尽量高频率的内存(频率不能高于主板的支持)、提升网络带宽、使用 SSD 告诉磁盘、提升 CPU 性能等
-
例如 CPU 的选择
- 对于数据库并发比较高的场景,CPU 的数量比频率重要
- 对于 CPU 密集型场景和频繁执行复杂 SQL 的场景,CPU 的频率越高越好
-
-
第七章 分库分表篇
为什么要分库分表
- 关系型数据库以 MySQL 为例、单机的存储能力、连接数是有限的,它自身就很容易会成为系统的瓶颈。当单表数据量在百万以内时,我们还可以通过添加从库、优化索引提升性能。一旦数据量朝着千万以上趋势增长,再怎么优化数据库,很多操作性能扔下降严重。为了减少数据库的负担,提升数据库相应速度,缩短查询时间,这时候就需要进行分库分表。
- 当【表的数量】达到几百上千张时,众多的业务模块都访问这个数据库,压力会比较大,考虑对其进行分库。
- 当【表的数据】达到几千万级别,做很多操作都比较吃力,考虑对其进行分库或者分表。
数据切分(sharding)方案
-
数据的切分(sharding)根据其切分规则的类型,可以分为两种切分模式
- 垂直切分:按照业务模块切分,将不同模块的表切分到不同的数据库中
- 水平切分:将一张大表按照一定的切分规则,按照行切分成不同的表或者切分到不同的库中
-
如何分库分表
- 分库分表就是要将大量数据分散到多个数据库中,使每个数据库中数据量小响应速度快,以此来提升数据库的整体性能。核心理念就是对数据进行划分(sharding),以及切分后如何对数据的快速定位与整合。针对数据切分类型,大致可以分为:垂直(纵向)切分和水平(横向)切分两种。
- 垂直切分
- 垂直切分又分为垂直分库(分库为主,因为分表在设计层面已解决)和垂直分表
- 垂直分库是基于业务分类的,和我们常听说的微服务治理观念很相似,每一个独立的服务都拥有自己的数据库,需要不同业务的数据需借口调用。而垂直分库也是按照业务分类进行划分,每个业务有独立数据库,这个比较好理解。 如交易系统,订单 DB、支付 DB、商品 DB 以不同数据库(一般在不同服务器上)分开。
- 垂直分表是给予数据库的列为依据切分的,是一种大表拆小表的模式。如一个 order 表有很多字段,把长度较大(describe)且访问不频繁的字段,拆分出来创建一个单独的扩展表 work_extend 进行存储。因为数据库是以行为单位将数据加载到内存中,这样拆分以后核心表大多是访问频率较高的字段,而且字段长度也都较短,可以加载更多数据到内存中,增加查询的命中率,减少磁盘 IO,以此来提升数据库性能。
- 优点
- 业务间解耦,不同业务的数据进行独立的维护、监控、扩展
- 在高并发场景下,一定程度上缓解了数据库的压力
- 缺点
- 提升了开发的复杂度,由于业务的隔离性,很多表无法直接访问,必须通过接口方式聚合数据
- 分布式事务管理难度增加
- 数据库还是存在单表数据量过大问题,并未根本上解决,需要配合水平切分
- 水平切分
- 上文说到垂直切分还是会存在单表数据量过大的问题,当我们的应用无法在细粒度的垂直切分时,依旧存在单库读写、存储性能瓶颈,这时就要配合水平切分一起。水平切分将一张大数据量的表,切分成多个表结构相同,而每个表只占原表一部分数据,然后按不同的条件分散到多个数据库中。如一张 order 表有 2000 万数据,水平切分后出来四个表,order_1、order_2、order_3、order_4,每张表数据量 500 万,以此类推。
- 水平切分又分为库内分表和分库分表
- 库内分表虽然将表拆分,但字表都还是在同一个数据库实例中,只是解决了单一表数据量过大的问题,并没有将拆分后的表分不到不同机器的库上,还在竞争同一个物理机的 CPU、内存、网络 IO。
- 分库分表则是将切分出来的字表,分散到不同的数据库中,从而使得单个表的数据量变小,达到分布式的效果。
- 优点
- 解决高并发时单库数据量过大的问题,提升系统稳定性和负载能力
- 业务系统改造的工作量不是很大
- 缺点
- 跨分片的事务一致性难以保证(一致性一般在库内保证)
- 跨库的 join 关联查询性能较差
- 扩容的难度和维护量较大(拆分成几千张子表想想都恐怖,rehash)
切分规则
- 常用的切分规则有以下几种
- 按照 ID 取模:对 ID 进行取模,余数决定该行数据切分到哪个表或者库中
- 按照日期:按照年月日,将数据切分到不同的表或者库中
- 按照范围:可以对某一列按照范围进行切分,不同的范围切分到不同的表或者数据库
- 分库分表以后会出现一个问题,一张表会出现在多个数据库里,到底该往哪个库的表里存呢?
- 根据取值范围
- 按照时间区间或 ID 区间来切分,举个例子:例如我们切分的是用户表,可以定义每个库的 user 表里只存 10000 条数据,第一个库 userID 从 1~10000,第二个库 10001~20000,第三个库 20001~30000 ··· 以此类推。
- 优点
- 单表数据量是可控的
- 水平扩展简单只需增加节点即可,无需对其他分片的数据进行迁移
- 能快速定位要查询的数据在哪个
- 缺点
- 由于连续分片可能存在数据热点,如果按时间字段分片,有些分片存储最近时间段的数据,可能会被频繁的读写,而有些分片存储的历史数据,则很少被查询
- hash 取模
- hash 取模 mod(hash 结果为 hash mod N)的切分方式比较常见
- 优点
- 数据分片相对比较均匀,不易出现某个库并发访问的问题
- 缺点
- 但这种算法存在一些问题,当某一台机器宕机,本应该落在该数据库的请求就无法得到正确的处理,这时宕掉的实例会被踢出集群,此时算法变成 hash mod (N -1),用户信息可能就不再在同一个库中
- 优点
- hash 取模 mod(hash 结果为 hash mod N)的切分方式比较常见
- 根据取值范围
切分原则
- 第一原则:能不切分尽量不要切分
- 第二原则:如果要切分,一定要选择合适的切分规则,提前规划好
- 第三原则:数据切分尽量通过数据冗余或表分组(table group)来降低跨库 join 的可能
- 第四原则:由于数据库中间件对数据 join 实现的优劣难以把握,而且实现高性能难度极大,业务读取尽量少使用多表 join(代码层面解耦,分成小步骤)
分库分表需要解决的问题
- 分布式事务问题
- 强一致性事务(同步,RPC)
- 最终一致性事务(推荐异步,人为本机提交事务,一致确定,非一致回滚)
- 分布式主键 ID 问题
- Redis incr 命令
- 数据库(另一张表维护生成主键)
- UUID(字符串,能做但是不建议,性能差)
- SnowFlake 算法(推荐)
- 跨库 join 问题
- 通过业务分析,将不同库的 join 查询拆分成多个 select
- 建立全局表(每个库都有一个相同的表)
- 冗余字段(不符合数据库三范式)
- E-R 分片(将有 ER 关系的记录都存储在一个库中)
- 最多支持跨两张表跨库的 join
- 跨库 count、order by、group by 问题
分库分表实现技术
- Sharding-JDBC(当当)
- TSharding(蘑菇街)
- Atlas(奇虎 360)
- Cobar(阿里巴巴)
- MyCAT(基于 Cobar)
- Oceanus(58 同城)
- Vitess(谷歌)
分布式 ID 都有那些生成方式?
- 需要考虑满足唯一性、不能太长、连续性
- 基于 UUID
- 基于数据库自增 ID
- 基于数据库的集群模式
- 基于数据库的号段模式
- 基于 Redis 模式(次推荐)
- 雪花算法(SnowFlake)(推荐)
- 滴滴出品(TinyID)
- 百度(UidGenerator)
- 美团(Leaf)
第八章 集群篇
主从复制作用
- 数据备份
- 实现高可用
实现原理
- 基于 bin log 来实现的主从复制
bin log 模式
-
三种存储格式
-
statement 模式
- 语句模式,把数据库修改的 SQL 语句写入 bin log
- 例如 insert、update、delete、alter
- 优点
- 数据量非常小。例如在全表更新时
- 缺点
- 在从库中执行相同的 SQL 语句可能得到不同的结果,如 uuid()
-
row 模式(默认模式)
- 行模式,把表中变化的数据行的内容放到 bin log 中
- 优点
- 主从复制不会出错
- 缺点
- 批量更新数据时,产生的日志量非常大
-
mixed 模式
- 混合模式,前两种模式的组合
-
bin log 开启与使用
-
查看 bin log 开启状态
-
SHOW VARIABLES LIKE 'log_bin';
-
-
开启 bin log
-
这里引用慢查询开启一样的经历,麻了,文件系统 yyds
-
这里的配置重点是我找了好久,在 /etc/ 目录下并没有找到 my.cnf 文件(其他在 /etc/my.cnf 的操作,与该步骤同),也没找到替代文件 my-default.cnf 文件。由于下载的 5.7 最新版本(5.7.34),就在想是不是版本变动,后来才发现,从 5.7.18 开始官方不再二进制包中提供 my-default.cnf 文件。具体参考:https://dev.mysql.com/doc/refman/5.7/en/binary-installation.html,总结如下,以前的需要配置 /etc/my.cnf 的操作,配置 /etc/mysql/mysql.conf.d 即解决
-
server-id = 1 # 0620 取消注释 log_bin = /var/log/mysql/mysql-bin.log # 0620 取消注释 expire_logs_days = 10 max_binlog_size = 100M # binlog_do_db = include_database_name # binlog_ignore_db = include_database_name
-
# 重启 MySQL 数据库 sudo service mysql restart
-
# 查看当前模式 SHOW VARIABLES LIKE 'binlog_format'; # 修改当前模式 SET binlog_format = row; # row 模式 SET binlog_format = statement; # statement 模式
-
-
bin log 文件
- mysql-bin.index:bin log 的索引文件,记录当前数据库使用的 binlog 列表
- mysql-bin.000001:bin log 文件,二进制文件,不能直接读取
- 生成并切换到新的 bin log 文件,MySQL 终端执行 flush logs 命令
-
查看 bin log 内容
-
mysqlbinlog 工具查看
- 由于二进制文件无法直接读取
- mysqlbinlog 简单理解成 cat 命令即可,同使用方式
-
在 MySQL 命令中使用命令查看
-
# 查看 bin log 文件列表 SHOW MASTER LOGS; # 查看 bin log 文件内容 SHOW BINLOG EVENTS IN 'mysql-bin.000002';
-
-
基于 bin log 日志点的主从复制
-
同步数据
- 有两个数据库服务器,确定一个主、一个从
- 手动完成数据同步
-
配置主服务器
-
已开启 bin log
-
# 清空主服务器的 bin log reset master; # 查看日志状态 show master status;
-
创建一个主从复制的用户,授权
-
# 创建用户,授权 mysql> create user 'slave'@'%' identified by 'Abc_123456'; mysql> grant replication slave on *.* to 'slave'@'%';
-
-
-
配置从服务器
-
配置文件中增加一个 server-id 即可,不需要开 log_bin
-
使用 change master to 命令配置主从
-
# 清除 relay log reset slave; # 停止从服务 stop slave; # change master to 连接 change master to master_host='192.168.68.132', master_port=3306, master_user='slave', master_password='Abc_123456', master_log_file='mysql-bin.000003', master_log_pos=1682, MASTER_AUTO_POSITION=0; # MASTER_AUTO_POSITION:不使用GTID
-
-
开启主从复制
-
start slave;
-
-
查看主从状态
-
show slave status \G # \G 只能在命令行,不能在可视化使用
-
-
基于 GTID 的主从复制(5.6 之后的版本才有,更方便)
-
GTID
- 全局事务 ID
- 优点:不需要知道使用的 bin log 的位置,实现主从复制使用的是 GTID
-
步骤
-
数据同步
- 有两个数据库服务器,确定一个主、一个从
- 手动完成数据同步
-
在主、从服务器配置文件中增加如下配置
-
#开启 GTID 模式(必选) gtid_mode=ON #强制 gtid 一致性(必选) enforce-gtid-consistency=true
-
-
清空主服务器的 bin log 和从服务器的 relay log
-
# 清除 bin log reset master; # 清除 relay log reset slave;
-
-
创建一个主从复制的用户,并授权
-
# 若已存在,则不需要重复创建 mysql> create user 'slave'@'%' identified by 'Abc_123456'; mysql> grant replication slave on *.* to 'slave'@'%';
-
-
从服务器上重新执行 change master to 命令
-
stop slave; change master to master_host='192.168.68.132', master_port=3306, master_user='slave', master_password='Abc_123456', MASTER_AUTO_POSITION=1;
-
-
主从同步延迟原因及优化方案
- 实际上主从同步延迟根本没有什么一招制敌的办法,因为所有的 SQL 必须都要在从服务器里执行一遍,但是主服务器如果不断的有更新操作源源不断写入,那么一旦有延迟产生,那么延迟加重的可能性就会越来越大。当然我们可以做一些环节的措施
- 保障主从服务器距离近
- 降低从服务器负载,多配几个从服务器
读写分离
-
方案一:在 APP 中创建两个数据源(可扩展性差,改源代码)
- masterDS:增删改
- slaveDS:查询
-
方案二:使用中间件实现读写分离
- MyCAT(底层 JAVA 实现)
- 读写分离
- 分库分表
- ···
- MySQL-Proxy(底层 C + Ruby 实现)
- 可以实现 MySQL 的读写分离,MySQL 官方提供,但是未提供稳定版本
- 不需要关心后台数据库之间的关系
- APP 不需要做任何修改就可以实现读写分离
- 不建议大家使用,因为是测试版本
- 建议使用 Atlas(奇虎 360)
- Atlas(底层 C 实现)
- 360 公司基于 MySQL-Proxy 进行升级改造,推出稳定版。可以应用在生产环境
- 分库分表性能较差
- https://github.com/Qihoo360/Atlas/blob/master/README_ZH.md
- MyCAT(底层 JAVA 实现)
-
Atlas 测试读写分离
-
# 如何使用,查看官方仓库 mysql -uroot -proot -P1234 --protocol=tcp -e"select @@server_id"
-
MyCAT 读写分离
- 一主一从
- 配置 MySQL 主从
- 配置 MyCAT,server.xml 和 schema.xml
基于主从复制的高可用方案
- 高可用双主一从集群
- schema.xml 其中的 balance=“1”,双主为主从双向复制,stand by writeHost 主与从负载均衡
小结
学习笔记,定期回顾,有问题留言。