什么是 Buffer Pool ?
MySQL 数据是存放在磁盘中的,如果每次操作都是对磁盘进行,那么肯定很影响性能。所以一定是先把数据从磁盘中取出来,放在内存中,再在内存中操作。 这一块区域就是 buffer pool。它由许多的数据缓存页和控制块组成。默认大小时 128M,每页大小是 16k,控制块为数据页的 5%。实际应用中,对数据的增删改 都是在 buffer pool 中操作,然后再由 IO 线程写入磁盘。同时为了避免数据库崩溃等异常情况,导致未成功写入磁盘,引入了 redo 日志,记录了之前做过的增删改操作, 重启后,读取文件内操作,重新执行一遍,即可恢复。当然,对于数据更新过程,还有一套严格的步骤,涉及到 undolog、binlog、提交事务、buffer pool 占数据刷回磁盘等。 通过这种方式,保证在内存中操作,然后顺序写回磁盘,提高性能。(内存速度高于磁盘,顺序写高于随机读写)
工作流程为:在查询的时候,先在 buffer pool 中查询对应的数据页,如果没有从磁盘读取,存在 buffer pool 中,下次查询直接从 buffer pool 中获取。
不同于查询缓存,查询缓存是以 key-value 的形式,存储相同查询 sql 的结果。buffer pool 是属于引擎层的。
buffer pool 是以页为单位进行存储的,每个缓存页都对应一个控制块,里面包含数据页所属的表空间、编号以及在 buffer pool 中的地址。
buffer pool 中页分三种, 空闲页、干净页、脏页,空闲页是未被使用的页,干净页是指内存的数据和磁盘数据一致,脏页是指内存中的数据已被修改,还没有刷回磁盘,当刷回磁盘后变为干净页。
InnoDB 如何管理 Page 页?
基于链表 + LRU + 冷热分离来管理的。
MySQL 为什么要改进 LRU 算法?
LRU 是指最近最少使用算法,原理就是将使用的元素放在队列头部,这样尾部即为最少使用的元素,当队列满了,淘汰最末尾的即可。 但是在 MySQL 使用中会出现 2 个比较常见的问题:
-
第一个是预读失效,所谓预读就是在读取一部分数据后,其相邻的数据也有可能下次被访问,所以可以提前预读提高性能,但是并不一定会被读取,所以预读后,导致淘汰了页,但是淘汰的页却是需要被访问的。
-
第二个是缓存池污染,当需要读取大量的页时,会同时加入多个页,淘汰已经存储的页,同样是不合理的,这就是缓存池污染,会导致 MySQL 性能下降。
MySQL 因此改进了 LRU 算法,将其分为2个区域,热数据区和冷数据区,当加载页时,先放在冷数据区,1s 内被再次访问就移动至热数据区,时间可设置。 为何设置成1s,是因为 1s 内如果数据不在被访问,那么再次被访问的几率就不大了。时间可以调整。
使用索引一定可以提高效率吗?
视情况而定,有好处也有坏处。
优点: - 提高检索效率,降低数据库IO成本 - 通过对索引列进行排序,降低数据排序的成本,降低cpu的消耗 缺点: - 创建和维护索引需要耗费时间,随数据量增加而增加 - 需要占用物理空间 - 数据进行增加、修改、 删除,索引也需要维护 创建索引的原则: - 在经常需要搜索的列创建索引 - 主键、外键 - 范围搜索 - 排序 不建或少建索引: - 用不到的字段查询 - 表记录比较少 - 经常需要修改的字段 - 参与列计算的 - 区分度不高的字段
介绍一下 Page 页的结构
-
File Header: 文件头,描述页的信息
-
Page Header: 页头,页的状态信息
-
Infimum/Supermum: 最小最大记录
-
User Records: 数据行
-
Free Space: 空闲空间,页中未被使用的空间
-
Page Directory: 页目录,存储用户记录的相对位置
-
File Trailer: 文件尾,校验页是否完整
说以下聚簇索引和非聚簇索引
聚簇索引,索引和数据在一起,非聚簇索引,索引和数据分开存储,索引的叶子节点指向了数据对应的位置
主键采用聚簇索引,采用B+数结构,叶子节点就是行记录。
聚簇索引的优点 - 范围查找 - 查找目标数据,不需要再次io回表查询 - 覆盖索引
缺点 - 插入速度严重依赖插入顺序 - 更新主键的代价很高,因为会导致行移动 - 二级索引访问,需要回表查询
索引有那几种类型
-
普通索引 没有任何限制
-
唯一索引 字段唯一,允许控制
-
主键索引,只能有一个,不允许控制
-
复合索引 可以在多个列创建索引
-
全文索引大数据量检索,使用全文索引会比like快很多,字符串、文本字段。
介绍一下最佳左前缀法则
根据联合索引最左边的字段进行排序,然后再按照第二个排序,依次………
什么是索引下推
没有索引下推时,会根据索引来查找记录,然后根据 where 进行数据过滤,会造成多次回表, 使用索引下推后,在取出索引的同时,判断是否可以根据where条件进行数据过滤,减少回表次数,提高性能。
什么是自适应哈希索引
InnoDB 引擎根据查询统计发现某一查询满足hash索引的数据结构特点并且达到一定次数,就建立一个hash索引,底层是散列表,存在于缓存中,只适合搜索等值的查询,范围查询是不能使用的。 其目的减少b+树路径搜索,可以根据索引快速定位到叶子节点。
为什么 like 以 % 开头索引会失效
索引的创建是有序的,如果首字母无法确认,也即%在左侧,这意味着所有的索引都要进行判断,如果在右侧,则可以根据首字母从左到右依次比较,可以过滤一部分数据,是可以用到索引的。 当然还有一种特殊情况,如果查询的字段中都在组合索引中,那么也会走索引,也是全部扫描,但是组合索引由于不会存储完整的数据行,那么检索效率还是高于全表扫描的。
自增还是UUID
-
自增长度小
-
占用空间小
-
容易排序
-
容易暴露业务量
-
并发高,竞争自增锁会增加数据库吞吐能力
-
数据迁移或分库分表场景,不再适用
UUID - 不会冲突,数据拆分合并,都能保证主键全局唯一性 - 可以在应用层生成,提高数据库吞吐能力 - 影响插入速度,与自增相比,最大缺陷就是随机io,新增的uuid,值不一定比以前的大,无法按顺序放在数据后,涉及到插入旧的数据之间,造成数据移动,影响性能。 - 会消耗更多的存储空间,操作比整型慢
推荐还是使用自增。
InnoDB 和 MyISAM 的区别
-
事务和外键
-
锁机制
-
索引结构
-
并发处理能力
-
存储文件
B 树和 B+ 树的区别
B树 - 每个节点包含索引和数据,如果数据量过多,会导致树的高度增加,增加磁盘io
B+树 - 一个节点有多个元素 - 叶子节点都冗余了非叶子节点数,有数据冗余 - 叶子节点有单项指针
为什么采用 B+ 树而不是 B 树
B+ 树在提高IO性能的同时还能解决 B 树遍历效率低的问题。B+ 树只要遍历叶子节点即可实现整颗树的遍历。
Innodb中的 B+ 树是怎么产生的?
MySQL 以页为单位进行读取存储。页包含双向指针、页目录以及用户数据区域。页目录分组缩小查询范围、提高查询效率,以空间换取时间。
一个B+树大概能存放多少条索引记录
show global status like 'Innodb_page_size'
- 16384 , 16kb
MySQL设计者将 B+ Tree 的节点大小设置为一个页,这样只需要一次 io 就能全载入,每页大小16k,所以节点也是 16k,B+Tree的根节点保存在内存中,子节点才是存储在磁盘中。
计算公式: 根节点指针数 * 单个叶子节点的记录行数
-
根节点指针数: 假设用的int,占用4个字节,指针6个字节,那么一个节点就可以存储 1638/(4+6) = 1638 个索引指针
-
计算每个叶子节点记录数,假设每行1k,那么一页就可以存储 16/1 行数据
-
一颗高度为2的树可以存放的记录数是 1638 * 16 = 26208, 同理,每增加一层高度,就乘以 1638即可。
explain
-
表的读取顺序
-
那些索引可以被使用
-
那些索引真正被使用
-
表的直接引用
-
每张表有多少行被优化器查询了
id:操作顺序 select_type: 查询类型 table:查询的表 partitions:分区 type: 表示连接类型,以什么样的方式来获取数据,简化后的从好到坏 system const eq_ref ref rang index all possible_keys: 可能使用到的索引 key:使用的索引 key_len: ref: rows: 读取行数 filtered:过滤行数 extra: 额外信息
type: - system 不进行磁盘id,查询系统表,仅返回一条数据 - const 查询主键索引,返回 1 条或 0 条数据,属于精确查找 - eq_ref 查找唯一性索引,返回数据最多一条,属于精确查找 - ref 查询非唯一性索引,返回多条数据,属于精确查找 - range 查找某个索引的部分索引,只检索给定范围的行 - index 查找所有索引树,比all快 - all 不使用任何索引,全表扫描
extra:
-
using filesort: 无法利用索引进行排序
-
using index: 直接通过索引就能获取到数据,也即覆盖索引,不需要回表
-
using index condition: 使用了索引,但是部分条件无法使用索引,会先用索引匹配一遍,在去匹配无法使用索引的条件
-
using join buffer: 使用了连接缓存 join 连接查询时,join 字段非索引字段
-
using temporary: 使用了临时表,来进行存储结果,常用于排序和分组查询
-
using where: 全表扫描
如何进行分页查询优化
一般性分页,使用 limit offset,rows, 第一个是偏移量,0 开始,rows 表示返回的行数
-
使用覆盖索引
-
利用子查询,先定位偏移位置id,然后 id 值向后查询 select * from t5 where id>=(select id from t5 order by text limit 1000000, 1) limit 10;
-
延迟关联,可以先用join进行分页,然后回表查询数据
如何做慢查询优化
开启慢查询日志: - slow_query_log 开启还是关闭 默认关闭 - slow-query-log-file 日志储存路径 - long-query-time 慢查询阈值,大于此时间就是慢查询
性能下降原因: - 等待时间长,锁表导致 - 执行时间长,查询语句烂,索引失效,join太多,服务器参数配置
慢查询优化思路 - 优先选择高并发的sql - 定位性能优化的性能瓶颈, io数据访问,看是否用到了索引,cpu,看数据预算是否花费了太多时间,网络带宽 - 明确优化目标,确定优化程度用户体验好 - 通过 explain 入手,明确 sql 的执行状态 - 永远用小的结果集驱动大的结果集,获取连接次数少,减少资源消耗 - 尽可能在索引中完成排序 - 只获取自己需要的列 - 只使用最有效的过滤条件,最短路径获取数据 - 避免复杂的 join 和子查询,建议不超过 3 张表,将复杂的 sql 拆分成小 sql,通过代码层进行封装 - 合理设计索引并利用
Hash 索引的优缺点
-
没有 hash 冲突的情况,查询数据比较快,如果有,则按链表进行逐行比较
-
只存储hash值和行指针,不存储数据
-
只支持等值比较查询
-
无法用于排序,范围查找
说一下 InnoDB 内存相关的参数优化
Buffer Pool, 增大值,可减少对磁盘的 io 操作,在有大量事务进行更新、删除、插入时,可有效提高性能。
增加依据: 根据命中率,低于90,则可以考虑增加缓冲池大小。缓存查询次数 / (缓存查询次数 + 磁盘查询次数) * 100
InnoDB 日志相关参数优化
-
默认16M
-
有过有 text/blog 大字段,
-
innodb_lob_buffer_size 缓存区大小
-
innodb_log_file_size 日志文件大小
-
设置过小,会导致频发触发检查,刷新脏页到磁盘,增加 io 次数,影响性能
-
设置过大,如果宕机,恢复时间过长
-
官网建议最少要承载一个小时业务日志量
-
可以通过抓取一段时间内的 log sequence number,这是一个全局参数(MySQL设计)
-
pager 分页工具获取一分钟获取的 log sequence number,在判断一个小时即可
-
存在业务量大小负载,导致算出比较大、比较小,实际还需要集合业务经验判断。
InnoDB io线程相关的参数优化?
-
将磁盘 io 转为内存查询
-
query_cache_size&have_query_cache 查询缓存
什么是写失效?
数据库一页 16k,操作系统一页 4k,需要分 4 次写入,如果只写了一部分,那么就会造成数据丢失。
-
双写缓冲
-
先写 DoubleWriter Buffer 内存缓存
-
写入共享表空间 2m,连续的内存空间,如果下一步失败,从这里进行恢复
-
再写入数据文件中
如何进行join优化
-
永远用小结果的表驱动大结果,本质是减少外层循环数据量
-
为匹配的条件增加索引,减少内层表的循环匹配次数
-
增大 join buffer size 的大小,缓存数据越多,内层表扫描次数就越少
-
减少不必要的字段查询。
索引那些情况会失效
-
查询条件包含 or
-
隐式类型转换
-
左 like
-
联合索引,违反最左匹配原则
-
对索引进行函数运算
-
多索引字段使用 != <> not in
-
索引使用 is null, is not null, 可能会失效
-
mysql 估计使用全表扫描会比使用索引快,则不适用索引。
什么是覆盖索引
获取的数据都在索引上,不需要回表
MySQL 事务的特性
-
原子性 事务作为一个整体被执行,那要全部执行,那么全部不执行,实现回滚是 undo log。
-
一致性 事务应确保数据库状态从一个一致性状态转变为另一个一致性状态,一致性是数据库中的数据应满足完整性约束
-
隔离性 一个事务的执行,不能被其他事务影响,有是个隔离级别,从低到高一次 读未提交,读已提交,可重复读、串行化
-
持久性 一个事务一旦提交,在数据库中的改变是永久性的,后续操作或故障都不会受影响,丢失,主要靠 redo log
事务的持久化是为了应对系统崩溃造成的数据丢失,只有保证了事务的一致性,才能保证执行结果的正确性。
MySQL 可重复读是怎么实现的?
MVCC 多版本控制器,核心就是 Undo log 多版本链 加 Read view, 通过 undo log 来保存数据的历史版本,实现多版本管理。CC 是通过 read view 来实现管理的,通过read view 原则来觉得数据是否显示。针对不同的隔离级别,read view 的生成策略不同,也就实现了不同的隔离级别。
Undo log 多版本链,每条数据都有两个隐藏字段 - trx_id: 事务id,记录最近一次更新这条数据的事务id - roll_pointer: 回滚指针,指向之前生成的 undo log
Read view: 将某个时刻的数据记录下来,查询时从 undo log 最新一条记录进行对比,如果不符合比较规则,则回滚到上一条记录进行对比,直到得到符合比较条件的查询结果
-
m_ids: 那些事务正在执行,没有提交
-
min_trx_id:
-
max_trx_id:
-
creator_trx_id:
当一个事务查询sql时,会生成一致性视图 read-view, 按照规则来查找合适的数据
规则 - trx_id < min_id: 事务已提交,可读 - trx_id > max_id: 将来事务生成的,不可读 - 中间情况,分,当前在未提交的事务中,那么不可读,否则可读。
Repeatable Read 解决了幻读问题嘛?
普通的查询时快照读,是不会看到别的事务插入的数据的, 只有在当前读时才会出现,例如 select … for update 语句。 当前读读取的是数据库最新的数据。
数据库锁的种类
按操作类型:分读锁和写锁 按操作粒度分:表级锁、行级锁、页面锁 开销、速度、粒度、死锁、冲突概率、并发读 按操作性能: 乐观锁(数据版本比较,提交时检查)、悲观锁(提前锁定,避免其他人操作)
请说下共享锁和排他锁
行级锁包含共享锁和排他锁,必须在使用 InnoDB 引擎以及开始事务隔离级别为可重复读。
共享锁,当事务对数据加上共享锁后,其他用户可以并发读取数据,但不能进行修改,直到释放 排他锁,当事务加排他锁后,其他事务不能加任何类型的锁,获取锁,既可以读,也可以写
一般情况,insert update delete 都是默认加排他锁,select 默认不加锁,但是可以显示的镜像加共享锁和排他锁,lock in share mode, for update
共享锁兼容共享锁,其他组合都不兼容。
InnoDB 的行锁时怎么实现的?
通过对索引数据页上的记录加锁的,主要算法有3种: record lock/ gap lock / next-key lock
-
readlock 锁定当个行的记录
-
gap lock 间隙锁,锁定索引记录的间隙,确保索引记录的间隙不变
-
next-key lock 记录锁和间隙锁的组合,同时锁住数据以及数据前后范围 注:只有在索引上检索数据才会使用行级锁,否则会使用表锁
在可重复读的隔离级别下,先采用 next-key lock,当操作含有唯一所有时,会进行降级,recordlock,仅锁住索引本身而非范围
并发事务会产生那些问题?
-
更新丢失 多个事务同事更新一行记录
-
脏读 一个事务读取到另一个事务修改但是未提交的数据
-
不可重复读
-
幻读
mvcc 内部细节
数据库为了高并发的数据访问,对数据进行了多版本处理,并通过事务的可见性来保证事务看到自己应该看到的数据版本,只在 Read Commited,Repeatable Read 下工作
行记录有三个隐藏字段,db_row_id/db_trx_id/db_roll_ptr - 没有显示定义主键,也没有定义唯一索引,会自动添加一个 row_id 隐藏列作为主键 - 事务进行增删改时,会将这个事务 id 插入到 trx_id 中 - roll_ptr: 回滚指针,指向 undo log
undo log 多版本链 read view
读已提交,每次都会生成一个 readview,导致每次都读到不同的数据 可重复读,一个事务中,只有第一次生成,可以解决不可重复读问题。
说一下 MySQL 死锁的原因和处理方法
表锁,一个事务锁住了a表,然后访问b表,此时b被另一个事务锁住,同时需要访问a表,互相等待释放,造成死锁,调整程序操作逻辑
行级锁 - 全表扫描,行级锁上升为表锁,多个事务执行后,很容易产生死锁和阻塞,解决办法就是 explain 分析,处理全表扫描 sql,建立索引优化 - 两个事务想拿到对方持有的锁,互相等待,产生死锁。对索引加锁的顺序不一致导致,如果可以,尽量以相同的属性来访问索引记录,在程序批量处理数据时,如果对数据进行排序,以固定的顺序来处理记录,可降低死锁可能
MySQL 体系结构
-
客户端连接器
-
连接池、sql 接口、解析器、查询优化器、缓存
-
存储引擎层,可插拔
-
系统文件层、 文件和日志
undo log、redo log、bin log
undo log 用于回滚的日志,更新之前会记录在这个里面,回滚时通过这个来回滚,在事务开始前产生,事务提交后,不会立即删除,放入待删除列表,交给后台线程处理。 - 提供回滚操作,记录回滚时需要操作的sql记录 - 提供多版本控制 mvcc
redo log:用于数据库持久性,事务提交成功,那么数据库修改就永久保存下来。 如果每执行成功一个事务,就将数据写入磁盘,会有严重的性能问题。因此设计了 redo log,来记录事务对那些数据页做了修改,就能解决性能问题,文件更小而且是顺序IO。 先将记录写入 redo log buffer,后续将多个操作写入redo log file。当发生故障、崩溃后,重启服务,可以通过此文件进行恢复。
bin log 用于记录数据库执行的写入性操作,不包含查询,以二进制的形式保持在磁盘中。 - 主从复制 - 数据恢复
说一下 redo log 和 undo log 的持久化策略
-
redo log 是 Innode 引擎独有的,binlog 是服务 server层的,其他层都可以用
-
redo logs 是物理记录,记录修改内容,binlog 是逻辑记录,记录 sql
-
redo log 是循环写,bin log 是追加写,不会覆盖以前的日志
-
redo 是事务自动恢复使用,binlog 用于主从复制以及数据恢复
MySQL 在线修改大表影响
-
时间不可预估,一般时间长
-
表级锁,影响写入操作
-
中断操作,会还原,会锁表
-
修改大表结构会导致cpu,io等性能消耗,使服务器性能降低
-
会导致主动延迟,影响业务读取
-
对表进行加锁
-
复制原表物理结构
-
修改表物理结构
-
将原表数据导入中间表
-
锁定中间表,删除原表
-
改名中间表
-
释放锁
count(列名) count(1) count(*) 区别
-
count(*) 和count(1)不会忽略null数据,count(列名) 会忽略 null 数据
-
执行效率, innodb 通过遍历最小的二级索引来处理 count(*)和count(1),性能是相同的,大于 count 列名
什么时候进行分库分表?
将同一个数据库中的数据分散存放到多个数据库上,以达到分散单台设备负载效果。主要解决因数据量过大导致数据库性能降低。
-
单机容量达到瓶颈
-
连接达到上限
说说 Mysql 的主从复制
-
实时灾备,用于故障切换
-
备份
-
读写分离
是通过将主服务器的 binlog 负责到从服务器,然后执行。从服务器会启动一个 io 线程读取日志,以及一个执行线程,执行 binlog
sql执行流程
客户端发送sql请求 -》查询缓存 -》 解析器-》解析树-》预处理器-》新解析树-》查询优化器-》执行计划-》查询执行引擎-》返回结果
Mysql 支持缓存查询嘛?
5.7 支持,8.0 之后废弃
-
管理不灵活
-
sql 必须完全一致才会 cache 命中
-
结果太大不会缓存
-
分库分表不起作用
-
有自定义函数、触发器,不起作用
-
表结果、数据变化是,关联 cache 全部失效
可在应用层进行缓存,例如 redis encached 等等。
ACID 靠什么保证?
A: 原子性由 undo log 保证,他记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql C:一致性由其他三大特性保证,程序代码要保证业务上的一致性 I: 隔离性由 MVCC 来保证 D:持久性由内存 + redo log 来保证,宕机可以从 redo log 恢复。
MySQL 主从同步原理
Master 一个线程,binlog dump thread, 当 binlog 日志有变动时,线程读取内容发送给从节点。 从节点 IO 线程接收 binlog 内容,写入 relay log 文件中,从节点SQL线程读取 relay log 来进行重放。 默认采用异步负责,主库发送日志,不关心从库是否处理。存在丢失问题、但是性能高。可以选择全同步复制,必须等待从库响应才结束。 还有一种折中方式半同步复制,一个节点确认没问题,即可视为成功,其他节点就不考虑了。