优化慢查询
慢查询可能出现的情况:
- 聚合查询
- 多表查询
- 表数据量过大
- 深度分页查询
表象:页面加载过慢,接口压测响应时间过长(超过1s)
如何定位慢查询?
方案一:开源工具
可以使用相应的调试工具:
调试工具:Arthas
运维工具:Prometheus,Skywalking
工具下载地址:
- Arthas:https://arthas.gitee.io/
- Prometheus:https://prometheus.io/
- Skywalking:https://github.com/apache/skywalking
方案二:MySQL自带的慢日志
慢日志记录了所有执行时间超过指定参数(long_query_time,单位:秒,默认10秒)的所有sql语句的日志,如果要开启慢查询,需要在Mysql的配置文件(/etc/my.conf)中配置如下信息:
# 开启MySQL慢日志查询开关
slow_query_log=1
# 设置慢日志的时间为2秒,sql语句执行的时间超过2秒,就会视为慢查询,记录慢查询日志
long_query_time=2
配置完毕之后,通过以下指令重新启动Mysql服务器进行测试,查看慢日志文件中记录的信息
/var/lib/mysql/localhost-slow.log
一个SQL执行的很慢,如何分析
可以采用EXPLAIN
或者DESC
命令获取Mysql如何执行SELECT语句的信息
-- 语法
-- 直接在select语句之前加上关键字 explain或者desc
explain select 字段列表 from 表名 where 条件;
这条SQL数据的结果并不是表中的数据,而是sql语句执行的相关数据,其中:
- possible_key 当前SQL可能使用到的索引
- key 当前SQL实际使用到的索引
- key_len 索引占用的大小
通过后两者可以查看是否会命中索引
- Extra额外优化建议
Extra | 含义 |
---|---|
Using where;Using Index | 查询使用了索引,需要的数据都在索引列中能找到,不需要回表查询数据 |
Using index condition | 查询使用了索引,但是需要回表查询数据 |
- type 这条sql的连接类型,性能由好到差为NULL,system,const,eq_ref,ref,range,index,all
- system:查询系统中的表
- const:根据主键查询
- eq_ref:主键索引查询或唯一索引的查询
- ref:索引查询
- range:范围查询
- index:索引树扫描
- all:全盘扫描
如果出现后两种类型,那么这个sql的问题就很严重,需要进行索引的优化
总结:SQL语句的执行很慢,如何优化呢?
可以采用Mysql自带的分析工具:explain
- 通过key和key_len检查是否命中索引(索引本身存在是否有效的情况)
- 通过type字段查看sql是否有进一步优化的空间,是否存在全索引扫描或者全盘扫描
- 通过Extra建议判断,是否出现了回表情况,如果出现了,可以尝试添加索性或者修改返回字段来修复
索引的概念和索引的底层
什么是索引?
索引(index)是帮助MySQL高效的获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足的特定查找算法的数据结构(B+树),这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。
MySQL的索引底层使用的数据结构是B+树,那么什么是B+树呢?
数据结构的对:
二叉树容易退化成链表,时间复杂度很不稳定
如果使用红黑树的话,虽然他的时间复杂度是稳定的,但是如果数据量大的情况下,比如1000万条数据,红黑树其实也是二叉树的一种,红黑树只有两个节点,如果存入就会导致这个红黑树变得特别的高,如果去查找数据的话,就需要查找很多的层级才能找到数据,因此红黑树的效率也不高。
B-Tree,B树是一种多叉路平衡查找树,相对于二叉树,B树每个节点都可以有多个分支,即多叉。以一颗最大度数(max-degree)为5(5阶)的B-Tree为例,那这个B数每个节点最多存储4个key,B树又被称为矮胖树,所以他的查询效率很高但是MySQL的底层并没有采用这种方式。
B+Tree是在B-Tree基础上的一种优化,使得其更加适合实现外存储索引结构,InnoDB存储引擎就是使用B+Tree实现其索引结构。
B-Tree和B+Tree对比:
- 磁盘读写代价B+Tree数很低
- 查询效率B+Tree更加稳定
- B+Tree树更便于扫库和区间查询
总结:
了解过索引吗?什么是索引?
- 索引(index)是帮助mysql高效获取数据的数据结构(有序)
- 提高数据检索的效率,降低数据库的IO成本(不需要全表扫描)
- 通过索引列对数据进行排序,降低数据排序的成本,降低CPU的消耗
索引的底层数据结构了解吗?
Mysql的InnoDB引擎采用的是B+树的数据结构来存储索引的
- 阶层更多,路径更短
- 磁盘读写代码B+树更低,非叶子节点只能存储指针,叶子节点存储数据
- B+树便于扫库和区间查询,叶子节点是一个双向链表
聚簇索引和非聚簇索引
聚簇索引:将数据存储和索引放在一起,索引的叶子节点保存了行数据
特点:必须有,而且只能有一个
二级索引:将数据和索引分开存储,索引结构的叶子节点关联的是对应的主键
特点:可以存储多个
聚簇索引的选取规则:
- 如果当前表中有主键,那么主键就是聚簇索引
- 如果不存在主键,将使用第一个唯一索引(UNIQUE)索引作为聚簇索引
- 如果表中没有主键或没有合适的唯一索引,则InnoDB会自动生成一个rowid作为隐藏的聚簇索引。
聚簇索引存储的都是id值,叶子节点存储的都是每个id对应的row
非聚簇索引存储的都是当前这行的id值
什么是回表呢?
就是通过二级索引去查询数据的时候,会查询到数据对应的主键的id,然后再使用主键的id去聚簇索引中去查询id对应的数据,从而提高了查询的速度。
覆盖索引
什么是覆盖索引?
覆盖索引是指查询使用了索引,返回的列,必须在索引中全部都能找到
- 使用id查询,直接走聚簇索引查询,一次索引扫描,直接返回数据,性能高。
- 如果返回的列没有创建索引,有可能会触发回表查询,尽量避免使用select *
MYSQL超大分页的处理:
在数据量较大的时候,如果进行limit分页查询,在查询时,越往后,分页效率越低。
limit分页耗时对比:
select * from tb_sku limit 0,10;
(0.00 sec)
select * from tb_sku limit 9000000,10;
(11.05 sec)
因为,当在进行分页查询的时候,如果执行了 limit 9000000,10,此时需要mysql排序前9000010记录,仅仅返回9000000-9000010 的记录,其他记录都会丢弃,查询排序的代价非常大。
select *
from tb_sku t,
(select id from tb_sku order by id limit 9000000,10) a
where t.id = a.id;
(7.19 sec)
通过覆盖索引的方式优化查询速度,首先使用聚簇索引查询90000000,90000010的id,然后就不需要进行回表,查询到的id之后将这个查询结果设置为子查询,然后与tb_sku再次进行关联,然后用id相等的方式查询10条数据。
索引创建的原则
- 针对数据库较大,切查询比较频繁的表建立索引,单表超过10万条数据(增加用户体验)
- 针对常作为查询条件(where),排序(order by),分组(group by)操作字段建立索引。
- 尽量选择区分高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高
- 如果字符串类型的字段,字段的长度越长,可以针对字段的特点,建立前缀索引。
- 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询的效率。
- 要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价越大,会影响增删改查的效率。
- 如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它,当优化器指导每列是否包含null值时,他可以更好的确定那个索引最有效的用于查询。
索引失效
什么情况下索引会失效?
- 违反了最左前缀法则
- 范围查询右边的列,不能使用索引
- 不要再索引列上进行运算操作,索引将失效
- 字符串不加单引号,造成索引失效(类型转化)
- 以%开头的模糊查询,索引失效
SQL优化经验
表的设计
- 标的设计优化(参考阿里巴巴开发手册《嵩山版》)
- 比如设置合适的数值(tinyint,int,bigint),要根据实际情况选择
- 比如设置合适的字符串类型(char和varchar)char定长效率高,varchar可变长度,效率稍低。
SQL语句的优化
- SELECT语句务必指明字段名称(避免使用select *)
- SQL语句要避免造成索引失效的写法
- 尽量用union all 代替 union union会多一次过滤,效率低
- 避免where子句对字段进行表达式操作
- join优化 能用inner join就不要用left join 和 right join,如必须使用一定要以小表为驱动,内连接会对两个表进行优化,优化把小表放在外面,把大表放在里面,left join和right join,不会重新调整顺序。
主从复制,读写分离
如果数据库的使用场景的操作比较多的时候,为了避免写的操作性能影响,可以采用读写分离的架构,读写分离解决的是:数据的写入操作,影响了查询的效率。
事务(重要)
事务的特性是什么?数据库的ACID是什么?
事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。
ACID是什么?可以详细说一下嘛?
- 原子性(Atomicity):事务是不可分割的最小单元,要么全部成功,要么全部失败
- 一致性(Consistency):事务完成时,所有的数据必须保持一致状态。
- 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
- 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
并发事务的问题,事务的隔离级别
-
并发事务的问题:脏读,不可重复读,幻读
-
隔离级别:读未提交,读已提交,可重复读,串行化
并发事务的问题: -
脏读:一个事务读另一个事物还没有提交的数据
解释:事务1首先查询了数据库中的数据,然后事务1开始修改数据库的数据,在修改的时候,事务2进行读取数据库的数据,然后事务1才修改了数据,那么失误而读取的数据和当前数据库中的数据不一致就会出现脏读的问题 -
不可重复读:一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读
事务1手续爱你读取数据库的数据,随后事务2对数据库中的数据进行了更新,然后这个时候事务1再次读取数据库中的事务,那么两次读取到的数据就会出现不一致的问题,也就出现了不可重复读。 -
幻读:一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在,好像出现了幻影。
当事务1首先读取数据库中数据,发现数据不存在,这个时候事务2对数据进行插入,然后事务1再对数据进行插入的时候发现数据已经存在了,就会出现幻读的问题。
怎么解决并发事务的问题呢?
解决方案:对事务设置隔离级别
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
Read uncommitted 未提交读 | 不能解决 | 不能解决 | 不能解决 |
Read committed 读已提交 | 能解决 | 不能解决 | 不能解决 |
Repeatable Read(默认)可重复读 | 能解决 | 能解决 | 能解决 |
Serializable 串行化 | 能解决 | 能解决 | 能解决 |
但是Serialable 效率非常低下,一般很多时候不建议使用,也不会使用
注意:事务隔离级别越高,数据越安全,但是性能越低。
undo log 和 redo log
- 缓冲池(buffer pool):主存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改查操作时,先操作缓冲池中的数据(若缓冲池中没有数据,则从磁盘中加载并缓存),以一定的频率刷新到磁盘,从而减少磁盘的IO,加快处理速度。
- 数据页(page):是InnoDB存储引擎磁盘管理的最小的单元,每个页大小默认是16KB,页中存储的是行数据。
服务器端执行了sql之后会先去操作内存结构,然后将数据存储在内存结构的缓冲池中,然后随后再同步到磁盘结构中,这里会出现一个问题,如果数据同步到内存结构的缓冲池中的时候这个时候恰好mysql服务挂了,那么就有可能造成缓冲池中数据丢失问题。
因此针对上述问题,在数据库内存结构引入了一种文件叫做redo log
redo log
重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。
修改日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,后者是在磁盘中,当事务提交之后会把所有修改信息都存在改日志的文件中,用于刷新脏页到磁盘,防止发生错误时,进行数据恢复使用。
简单的描述redo log就是当数据库发生脏页数据的时候,这个时候就可以使用redo log 进行数据恢复,实现数据的持久性。
undo log
回滚日志,用来记录数据被修改前的信息,作用有两个:提供回滚,MVCC(多版本并发控制)。undo log 和redo log记录物理日志不一样,他是逻辑日志。
- 可以认为当delete一条记录之后,undo log中会记录一条insert记录,反之亦然
- 当update一天记录的时候,他会记录一条对应相反的update语句,当执行rollback的时候,就可以从undo log中的逻辑记录中读取对应的内容,并进行回滚。
undo log 和 redo log 区别
- redo log:记录的是数据页的物理变化,服务宕机可用来同步数据
- undo log:记录的是逻辑日志,当事务回滚的时候,通过逆操作恢复原来的数据
- redo log:保证事务的持久性,undo log保证了事务的原子性和一致性
MVCC多版本控制
事务的隔离性是如何保证的呢?
添加锁:排它锁(如果一个事务获取了一个数据行的排它锁,其他事务就不能获取该行的其他锁)
MVCC:多版本控制
全称 Multi-Version Concurrency Control,多版本控制,指维护一个数据的多个版本,使得读写操作没有冲突。
MVCC的具体实现主要依赖数据库记录中的隐式字段,undo log日志,readView。
隐藏字段 | 含义 |
---|---|
DB_TRX_ID | 最近修改事务id,记录插入这条记录或最后一次修改该记录的事务id |
DB_ROLL_PTR | 回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本 |
DB_ROW_ID | 隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段 |
undo log
- 回滚日志,在insert,update,delete的时候产生的便于数据回滚的日志。
- 当insert的时候,产生的undo log日志只在回滚时需要,在事务提交后,就会被立即删除
- 而update,delete的时候,产生的undo log日志不仅在回滚的时候需要,mvcc版本访问也需要,因此不会立即删除。
undo log版本链
在进行事务2的时候首先会在undo log中记录一下事务2开始之前数据库中的数据,并且记录DB_PRX_ID事务id为1,事务指向的地址为null,随后进行事务2的操作,然后对对应的数据加载到记录中,然后将DB_TRX_ID自增1,然后将DB_ROLL_PRX事务回滚的字段记录在undo log中保存的地址上。
在进行事务3操作的时候,首相还是将当前数据库中的记录进行备份,然后将地址指向上一次事务之前的数据的undo log文件中的地址,然后对记录中的数据进行修改,随后将事务的id(DB_TRX_ID)自增,然后将DB_ROLL_PRE的地址修改为当前事务执行之前的undo中记录的数据库的日志中。
最后进行事务4放入操作的时候同样是执行上述的操作。
不同的事务或相同的事务对同一条记录进行修改,会导致该记录的undo log生成一条记录版本链条,链条的头部是最新的旧记录,链条的尾部是最早的旧记录。
readview
ReadView(读视图)是快照读SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交)id。
-
当前读
读取当前的是记录的最新版本,读取时还要保证其他并发事务不能修改当前的记录,会对读取的记录进行加锁,我们日常操作,如select…lock in share mode(共享锁),select…for update,insert,delete(排它锁)都是一种当前读。 -
快照读
简单的select(不加锁)就是快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。 -
Read committed:每次select,都生成一个快照读。
-
Repeatable Read:开启事务后第一个select语句才是快照读的地方。
ReadView中包含的四个核心的字段:
-
m_ids: 表示当前活跃的事务id,例如事务5中的第一次查询中,当前活跃的事务的id就是3,4,5 因为事务1已经提交了。
-
min_trx_id:表示当前最小的活跃事务id,那么在活跃事务 3,4,5中最小的活跃事务就是3
-
max_trx_id:表示当前最大的活跃事务id,那么在活跃事务3,4,5中最大的活跃事务并不是5,而是预分配事务ID当前最大的事务id再+1 因此是6
-
creator:创建读视图的事务id,因为这个事务的事务5,因此id就是5.
版本链的访问规则:
- 当前事务id = 创建事务id 说明成立,说明数据是当钱这个事务更改的
- 当前事务的id 小于最小事务的id 说明成立,表示事务已经提交,可以访问
- 当前事务的id 大于最大事务的id 不可以访问,说明这个事务是readView生成后才开启的
- 当前事务是否在最小活跃事务id和最大活跃事务id之间,如果不在的是可以访问该版本的,说明数据已经提交了
- 不同的隔离级别,生成的ReadView的时机不同:
- Read Committed:在事务中每一次执行快照读生成ReadView文件。
- Repeatable Read:仅在十五中第一次执行快照读生成ReadView,后续复用ReadView。
RC隔离级别下,每一次执行快照读时生成ReadView
通过这3张表将事务依次往里面套就可以知道是否可以访问数据,根据左下角图中可以看到一共有4个事务
首先是事务4:
- 事务4 不等于 事务5 不成立
- 事务4 小于事务3 不成立
- 事务4 大于事务6 不成立
- 事务4在事务3和事务6之间 不成立
事务3:
5. 事务3 不等于事务5 不成立
6. 事务3 小于 事务3 不成立
7. 事务3 大于 事务5 不成立
8. 事务3在事务3到事务6之间 不成立
事务2:
9. 事务2 不等于 事务5 不成立
10.事务2 小于 事务3 成立 可以访问
使用想用的方法推断第二次访问得到的结果是事务3可以访问。
RR隔离级别下,仅在事务第一次执行快照读就生成ReadView,后续复用ReadView
通过上述访问规则的引用,RR隔离级别下两次访问的都是事务2提交的数据。
总结:事务中的隔离性是如何保证的,(解释一下MVCC)
MySQL中的多版本并发控制,值维护一个数据的多个版本,使得读写操作没有冲突
- 隐藏字段
- trx_id(事务的id),记录每一次操作的事务id,是自增的
- roll_pointer(回滚指针),指向上一个版本的事务版本记录地址
- undo log
- 回滚日志,存储老版本的数据
- 版本链:多个事务并行提交某一条记录,记录不同事务修改数据的版本,通过roll_pointer指针形成一个链表
- readView解决的是一个事务查询选择版本的问题
- 根据readView的匹配规则和当前的一些事务id判断该访问那个版本的数据
- 不同的隔离级别快照读是不一样的,最终的访问结果是不一样的
RC:每次执行快照读都会生成ReadView
RR:仅在事务中第一次执行快照读时生成ReadView,后续复用
MySQL主从同步的原理
MySQL主从复制的核心就是二进制日志
二进制日志(BINLOG)记录了所有的DDL(数据定义语言)语句和DML(数据操作语言)语句,但不包括数据查询(SELECT,SHOW)语句。
个人理解:
- 首先master库会将插入的数据写入到数据变化文件binlog中
- slave从库会开始IOThread去读取主库的binlog文件,然后将数据写入到从库的Relay log文件
- 随后slave从库会开启一个SQLThread将Relay log中的数据写入从库中
复制主要分为三步:
- master主库在事务提交的时候,会将数据变更记录在二进制日志文件binlog中
- 从库读取主库的二进制日志文件binlog,写入到从库的中继日志文件Relay log
- slave重做中继日志中的时间,将改变加载为自己数据库中的数据。
总结:主从同步的原理
Mysql主从复制的核心就是二进制文件binlog(DDL(数据定义语言)语句和DML(数据操作语言)语句)
- 主库在事务提交的时候,会把数据变更记录在二进制日志文件binlog中
- 从库读取主库的二进制日志文件binlog,写入到从库的中继日志Relay log中
- 从库重做中继日志中的事件,将改变反应为从库自己的数据
MySQL分库分表
分库分表的时机:
- 前提,项目业务数据逐渐增多,或业务发展比较迅速
- 优化已经解决不了问题(主从读写分离,查询索引…)
- IO瓶颈(磁盘IO,网络IO),CPU瓶颈(聚合查询,连接次数太多)
拆分策略:
垂直分库
垂直分库:以表为依据,根据不同的业务将不同的表拆分到不同的库中。
特点:
- 按照业务对数据分层管理,维护,监控,扩展
- 在并发下,提高磁盘的IO和数据量的连接数
垂直分表
垂直分表:以字段为依据,根据不同的属性将不同的字段拆分到不同的表中。
拆分规则:
- 将不常用的字段单独放在一张表中
- 把text、blob等大字段拆分出来放在附表中。
例如表中有一个商品描述,而且商品描述中存储的数据量是非常大的,因此就可以将这个字段单独分出来
特点:
- 冷热数据分离
- 减少IO过度争抢,两表互不影响。
水平分库
水平分库:将一个库的数据拆分到多个库中。
特点:
- 解决了单库大数据量,提高了并发性能瓶颈问题
- 提高了系统的稳定性和可用性
那么怎么进行查询呢?
一般情况下因为数据库的id节点是自增的
存储的id也是自增的
那么我们可以根据路由规则来查询
- 根据id节点取模
- 按id也就是范围路由,节点1(1-100万),节点2(100万-200万)
- …
水平分表
**水平分表:**将一个表的数据拆分到多个表中(可以是同一个库内)。
特点:
- 优化单一表数据量过大而产生的性能问题
- 避免IO争抢并减少锁表的几率
分库后的问题:
- 分布式事务一致性的问题
- 跨节点关联查询的问题
- 跨节点分页,排序函数
- 逐渐避重
一般项目中如果使用了分库分表的时候会增加分库分表的中间件 mycat
笔记是对黑马课程中的知识进行的个人总结,图片借鉴了课程视频中的资料,感谢黑马程序员的开源精神,哈哈,如有问题联系我删除!