第一梯队:必考高频题(出现率 80%+)
1. InnoDB 和 MyISAM 的区别?
核心答案:
InnoDB 是 MySQL 默认存储引擎,支持事务 、行级锁 、外键 、MVCC 和崩溃恢复。
MyISAM 是早期默认引擎,不支持事务 ,使用表级锁 ,崩溃后无法安全恢复,适合只读或读多写少的场景。
详细说明:
InnoDB 通过 redo log 实现崩溃恢复,即使突然断电,重启后也能恢复已提交的事务。它的行级锁 大大提高了并发写入能力,MVCC 让读操作不阻塞写操作。
MyISAM 的表级锁 意味着任何写操作都会锁住整张表,并发写入性能差。但它有全文索引(InnoDB 在 5.6 版本后也支持了),在某些读多写少的场景仍有应用。
生产建议 :除非有特殊原因,否则都使用 InnoDB。
2. 索引的底层结构为什么用 B+ 树?
核心答案:
B+ 树矮胖 ,树高通常只有 2 到 4 层 ,磁盘 I/O 次数少 。叶子节点形成有序链表 ,范围查询高效 。非叶子节点只存键值,单节点存储更多索引项。
详细说明:
与其他数据结构对比:
二叉树 / AVL / 红黑树:数据量大时树高过高,比如 2000 万行数据,红黑树高度可能达到 20 多层,查询需要 20 多次磁盘 I/O,性能极差。
B 树:非叶子节点也存储数据,每个节点能存的索引项少,树高比 B+ 树高。而且范围查询需要回溯到父节点,效率低。
哈希表 :虽然单点查询 O(1),但不支持范围查询 ,不支持排序,无法应对 WHERE age > 18 这类查询。
B+ 树的优势 :一个节点通常 16KB,每个索引项约 16 字节,一个节点可存约 1000 个索引项。2000 万行数据,树高仅需 3 层,查询只需 3 次磁盘 I/O。叶子节点的双向链表让范围查询变成链表遍历,非常高效。
3. 聚簇索引和非聚簇索引的区别?
核心答案:
聚簇索引 的叶子节点存储完整的行数据 ,一张表只能有一个,主键索引就是聚簇索引。
非聚簇索引 (二级索引)的叶子节点存储主键值 ,查询时需要回表到聚簇索引查完整数据。
详细说明:
InnoDB 中,数据本身就是按聚簇索引组织的。如果你没有定义主键,InnoDB 会选择一个唯一非空索引作为聚簇索引;如果也没有,则隐式生成一个 6 字节的 rowid 作为聚簇索引。
回表 的过程:比如在 name 字段上建了索引,执行 SELECT * FROM t WHERE name = '张三'。先在 name 索引树上找到 '张三' 对应的主键 id,再拿着 id 到聚簇索引树上查找完整行数据。这个过程需要两次 B+ 树查找。
覆盖索引 可以避免回表:如果查询的字段全部在索引中,比如 SELECT id, name FROM t WHERE name = '张三',name 索引已经包含了 id 和 name,就直接返回,不需要回表,性能大幅提升。
自增主键 的优势:使用自增主键时,新数据按顺序插入,只会在当前页末尾追加,不会触发页分裂。如果用 UUID 作为主键,插入是随机的,会频繁导致页分裂,造成性能下降和空间浪费。
4. 事务的四大特性(ACID)及实现原理?
核心答案:
-
原子性(Atomicity) :通过 undo log 实现,事务失败时回滚到修改前状态
-
一致性(Consistency):由 A、I、D 共同保证,加上业务逻辑约束
-
隔离性(Isolation) :通过 锁 + MVCC 实现,防止事务间相互干扰
-
持久性(Durability) :通过 redo log 实现,崩溃恢复时不丢失已提交事务
详细说明:
undo log 实现原子性 :当事务修改数据时,InnoDB 会把修改前的旧值记录到 undo log 中。如果事务需要回滚,就读取 undo log 把数据恢复原状。undo log 也用于 MVCC:当其他事务需要读取旧版本数据时,通过 undo log 构建历史快照。
redo log 实现持久性 :采用 WAL(Write-Ahead Logging)预写日志 机制。修改数据时,先写 redo log(顺序写) ,再写磁盘数据文件(随机写)。顺序写的速度比随机写快很多。如果数据库崩溃,重启后通过 redo log 重放已经写入 redo log 但尚未刷盘的事务,保证数据不丢失。
WAL 的核心 :redo log 是物理日志,记录的是"在某个数据页的某个偏移量做了某某修改"。它采用循环写 的方式,有两个文件轮流写,写满一个就切换。checkpoint 机制保证 redo log 不会被覆盖掉还没有刷盘的数据。
5. MySQL 的隔离级别及解决的问题?
核心答案:
MySQL 默认隔离级别是 REPEATABLE READ(可重复读) ,通过 Next-Key Lock(行锁 + 间隙锁) 解决了幻读问题。
四种隔离级别从低到高:
-
READ UNCOMMITTED(读未提交):可能产生脏读、不可重复读、幻读
-
READ COMMITTED(读已提交):解决脏读,仍可能产生不可重复读和幻读
-
REPEATABLE READ(可重复读):解决脏读和不可重复读,InnoDB 下也解决了幻读
-
SERIALIZABLE(串行化):解决所有问题,但并发性能最差
详细说明:
脏读 :一个事务读到了另一个未提交事务修改的数据。比如事务 A 修改了某行但未提交,事务 B 读到了修改后的值,然后事务 A 回滚了,事务 B 读到的就是脏数据。READ COMMITTED 及以上级别可以避免脏读。
不可重复读 :同一事务内两次读取同一行数据,结果不一致。比如事务 A 先查询某行得到值 100,事务 B 修改该行为 200 并提交,事务 A 再次查询得到 200,两次结果不同。REPEATABLE READ 及以上级别可以避免不可重复读。
幻读 :同一事务内两次范围查询,结果集不一致。比如事务 A 查询 WHERE age > 18 得到 10 条记录,事务 B 插入一条 age=20 的新记录并提交,事务 A 再次查询得到 11 条记录,多出来的就是"幻影"行。InnoDB 在 REPEATABLE READ 级别通过 Next-Key Lock 锁住间隙,阻止其他事务插入满足条件的数据,从而解决幻读。
READ COMMITTED 与 REPEATABLE READ 的区别 :两者都通过 MVCC 实现,区别在于 Read View 的生成时机 。RC 级别下,每次查询都生成一个新的 Read View ,所以能看到其他事务已提交的修改,导致不可重复读。RR 级别下,事务第一次查询时生成 Read View,之后一直复用,所以始终看到的是事务开始时的快照,保证了可重复读。
6. MVCC 是什么?如何实现?
核心答案:
MVCC(多版本并发控制) 让读不阻塞写,写不阻塞读 ,大大提高并发性能。InnoDB 通过为每行数据维护多个版本,结合 Read View 来判断哪个版本对当前事务可见。
详细说明:
InnoDB 中每行数据都有两个隐藏字段:
-
DB_TRX_ID:最近修改(插入或更新)该行的事务 ID
-
DB_ROLL_PTR:指向 undo log 中该行旧版本的指针
当更新一行数据时,InnoDB 不会直接覆盖原数据,而是:
-
将原数据复制到 undo log 中,形成一个旧版本
-
修改原数据为新值,同时更新 DB_TRX_ID 为当前事务 ID
-
将 DB_ROLL_PTR 指向 undo log 中的旧版本
这样就形成了一个版本链:当前行 → 上一个版本 → 更早的版本。
Read View 是事务执行快照读时生成的一个"视图",包含以下关键信息:
-
m_ids:生成 Read View 时所有活跃(未提交)的事务 ID 列表
-
min_trx_id:m_ids 中的最小值
-
max_trx_id:下一个将要分配的事务 ID
-
creator_trx_id:当前事务自己的 ID
可见性判断规则:当读取一行数据时,根据该行的 DB_TRX_ID 判断:
-
如果 DB_TRX_ID < min_trx_id ,说明该行在 Read View 生成前已提交,可见
-
如果 DB_TRX_ID >= max_trx_id ,说明该行是 Read View 生成后开启的事务修改的,不可见
-
如果 min_trx_id <= DB_TRX_ID < max_trx_id,判断 DB_TRX_ID 是否在 m_ids 中:
-
在 m_ids 中,说明事务未提交,不可见
-
不在 m_ids 中,说明事务已提交,可见
-
如果当前版本不可见,就通过 DB_ROLL_PTR 沿着版本链找上一个版本,重复判断直到找到可见版本或到达链尾。
不同隔离级别的 Read View 生成时机:
-
READ COMMITTED :每次查询都生成新的 Read View,所以能看到其他事务新提交的修改
-
REPEATABLE READ :事务第一次查询时生成 Read View,之后复用,所以始终看到的是事务开始时的数据快照
7. redo log、undo log、binlog 的区别?
核心答案:
| 日志类型 | 归属 | 日志内容 | 主要作用 | 写入时机 |
|---|---|---|---|---|
| redo log | InnoDB | 物理日志(页级别修改) | 持久性、崩溃恢复 | 事务执行过程中持续写入 |
| undo log | InnoDB | 逻辑日志(记录旧值) | 原子性、MVCC | 事务执行过程中持续写入 |
| binlog | Server 层 | 逻辑日志(SQL 语句或行记录) | 主从复制、数据备份 | 事务提交时一次性写入 |
详细说明:
redo log 的详细机制:
-
采用 WAL(预写日志) 机制:先写日志,后写磁盘
-
物理日志,记录的是"在某个数据页的某个偏移量做了什么修改",比如"在 page 100 的 offset 200 处写入值 99"
-
循环写 :redo log 文件大小固定,有两个文件轮流写,写满一个就切换。checkpoint 指针指向已经刷盘的位置,保证循环写不会覆盖未刷盘的数据
-
崩溃恢复时,从 checkpoint 之后开始重放 redo log,恢复所有已提交但未刷盘的事务
undo log 的详细机制:
-
逻辑日志,记录的是"如何把数据恢复回去",比如"将 id=1 的 name 从 '李四' 改回 '张三'"
-
用于事务回滚:读取 undo log 中的旧值恢复数据
-
用于 MVCC:当其他事务需要读取历史版本时,沿着 undo log 版本链找到可见版本
-
undo log 在事务提交后不会立即删除,因为可能还有别的事务正在读取这个旧版本。只有当事务不再需要这个版本时,purge 线程才会清理
binlog 的详细机制:
-
Server 层日志,所有存储引擎都共用
-
逻辑日志,记录的是 SQL 语句(STATEMENT 格式)或行数据变化(ROW 格式)
-
追加写:binlog 文件写满后生成新的文件,不会覆盖
-
主要用途 :主从复制 (从库拉取 binlog 进行重放)和数据恢复(基于全量备份 + binlog 恢复到任意时间点)
两阶段提交保证一致性 :
InnoDB 使用内部 XA 两阶段提交来保证 redo log 和 binlog 的逻辑一致:
-
Prepare 阶段:事务执行,写入 redo log,状态为 prepare
-
Commit 阶段:写入 binlog,然后提交事务,将 redo log 状态改为 commit
崩溃恢复时,检查 redo log 中的事务状态:
-
如果 redo log 状态为 commit,或者 redo log 状态为 prepare 且 binlog 已写入,则提交事务
-
如果 redo log 状态为 prepare 但 binlog 未写入,则回滚事务
这样保证了主从数据一致性:只要 binlog 写入了,redo log 就一定提交了;反过来,如果 redo log 提交了,binlog 也一定写入了。
8. 如何分析一条慢 SQL?
核心答案:
-
开启慢查询日志,定位具体的慢 SQL
-
使用 EXPLAIN 分析执行计划
-
根据执行计划进行针对性优化
详细说明:
第一步:开启慢查询日志
sql
-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
-- 设置慢查询阈值(秒),建议设为 1
SET GLOBAL long_query_time = 1;
-- 查看慢查询日志文件位置
SHOW VARIABLES LIKE 'slow_query_log_file';
第二步:使用 EXPLAIN 分析执行计划
sql
EXPLAIN SELECT * FROM t WHERE name = '张三';
EXPLAIN 输出中需要重点关注的关键字段:
type(访问类型):从好到差排序
-
system:系统表,只有一行数据,const 的特例
-
const:通过主键或唯一索引等值查询,最多返回一行,效率最高
-
eq_ref:连接查询中,被驱动表通过主键或唯一索引访问,每个驱动行只匹配一行
-
ref:通过非唯一索引等值查询,可能返回多行
-
range:索引范围查询,如 BETWEEN、>、<、IN 等
-
index:全索引扫描,扫描整个索引树
-
ALL :全表扫描,需要重点优化
possible_keys :可能用到的索引
key :实际使用的索引,如果为 NULL,说明没走索引
key_len :使用的索引长度,可以判断联合索引用了哪几列
rows :预估需要扫描的行数,越小越好
Extra:额外信息,重点关注:
-
Using index :使用了覆盖索引,非常好
-
Using where:存储引擎返回后,Server 层再过滤,可以接受
-
Using index condition :使用了索引下推,好
-
Using filesort :需要额外的排序操作,需要优化
-
Using temporary :使用了临时表,通常出现在 GROUP BY 或 DISTINCT 中,需要优化
第三步:优化方式
**1. 避免 SELECT ***
只取需要的字段,减少数据传输和回表可能。
2. 为条件字段建立合适的索引
注意联合索引的最左前缀原则。
3. 避免在索引字段上使用函数或隐式类型转换
sql
-- 错误:函数导致索引失效
SELECT * FROM t WHERE DATE(create_time) = '2024-01-01';
-- 正确:改为范围查询
SELECT * FROM t WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02';
-- 错误:隐式类型转换,phone 字段是 varchar 类型
SELECT * FROM t WHERE phone = 13800000000;
-- 正确:用字符串匹配
SELECT * FROM t WHERE phone = '13800000000';
4. 优化分页查询
sql
-- 深分页问题:offset 越大,扫描行数越多
SELECT * FROM t ORDER BY id LIMIT 100000, 10;
-- 优化方案1:使用子查询先定位起始 id
SELECT * FROM t WHERE id >= (
SELECT id FROM t ORDER BY id LIMIT 100000, 1
) LIMIT 10;
-- 优化方案2:使用游标分页,记住上一页的 id
SELECT * FROM t WHERE id > last_id ORDER BY id LIMIT 10;
5. 使用覆盖索引
让查询的所有字段都在索引中,避免回表。
6. 合理使用 JOIN
小表驱动大表,确保 JOIN 字段有索引。
9. 什么情况会导致索引失效?
核心答案:
索引失效的常见场景:
-
对索引字段使用函数 或表达式计算
-
隐式类型转换
-
LIKE 以
%开头(左模糊匹配) -
联合索引未遵循最左前缀原则
-
OR 条件中只要有一个字段无索引
-
全表扫描比使用索引更快时(优化器选择)
详细说明:
1. 对索引字段使用函数
sql
sql
-- 失效:对 create_time 使用了 DATE 函数
SELECT * FROM t WHERE DATE(create_time) = '2024-01-01';
-- 正确:改为范围查询,可以走索引
SELECT * FROM t WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02';
2. 隐式类型转换
sql
sql
-- 假设 phone 字段是 varchar 类型,建立了索引
-- 失效:传入的是数字,MySQL 会隐式将 phone 转为数字,相当于对字段用了 CAST 函数
SELECT * FROM t WHERE phone = 13800000000;
-- 正确:传入字符串
SELECT * FROM t WHERE phone = '13800000000';
3. LIKE 以 % 开头
sql
sql
-- 失效:以 % 开头,无法定位索引起点
SELECT * FROM t WHERE name LIKE '%张三%';
-- 有效:以常量开头,可以走索引
SELECT * FROM t WHERE name LIKE '张三%';
4. 联合索引未遵循最左前缀
sql
sql
-- 假设有联合索引 (a, b, c)
-- 有效:包含最左列 a
SELECT * FROM t WHERE a = 1;
SELECT * FROM t WHERE a = 1 AND b = 2;
SELECT * FROM t WHERE a = 1 AND b = 2 AND c = 3;
SELECT * FROM t WHERE a = 1 AND c = 3; -- a 有效,c 无法用(因为跳过了 b)
-- 失效:没有 a
SELECT * FROM t WHERE b = 2;
SELECT * FROM t WHERE b = 2 AND c = 3;
5. OR 条件中有无索引的字段
sql
sql
-- 假设 a 有索引,b 无索引
-- 失效:因为 OR 需要合并结果集,b 无索引需要全表扫描,优化器可能放弃 a 的索引
SELECT * FROM t WHERE a = 1 OR b = 2;
-- 正确:将 OR 改写为 UNION
SELECT * FROM t WHERE a = 1
UNION
SELECT * FROM t WHERE b = 2;
6. 不等于操作(!= 或 <>)
不等于操作通常不走索引,因为不等于是范围查询,如果结果集太大,优化器可能选择全表扫描。
7. 优化器选择全表扫描
当表中数据量很小,或者索引选择性差(如性别字段),优化器可能认为全表扫描更快。
10. 什么是覆盖索引?什么是回表?
核心答案:
回表 :通过二级索引找到主键后,再到聚簇索引查找完整行数据的过程,需要两次 B+ 树查找。
覆盖索引 :查询所需要的所有字段都在某个索引中,不需要回表,直接从索引返回数据,性能大幅提升。
详细说明:
回表的过程:
text
假设表结构:
CREATE TABLE t (
id INT PRIMARY KEY,
name VARCHAR(50),
age INT,
INDEX idx_name(name)
);
执行查询:SELECT * FROM t WHERE name = '张三';
执行步骤:
-
在 idx_name 索引树上查找 '张三',得到对应的主键 id 值
-
拿着 id 到 聚簇索引树上查找完整的行数据(包括 name、age 等所有字段)
-
返回结果
这就是回表,需要两次 B+ 树查找。如果查询的数据量很大,回表次数就很多,性能会下降。
覆盖索引的例子:
sql
-- 查询只需要 name 和 id,idx_name 已经包含这两个字段
SELECT id, name FROM t WHERE name = '张三';
因为 idx_name 索引的叶子节点存储的是 (name, id),查询只需要这两个字段,不需要回表 ,直接从索引返回数据。这就是覆盖索引。
如何判断是否覆盖索引 :
在 EXPLAIN 的 Extra 字段中看到 Using index,就说明使用了覆盖索引。
覆盖索引的优化技巧:
sql
sql
-- 假设经常需要查询用户的 id 和 name 通过 phone
-- 创建覆盖索引
ALTER TABLE t ADD INDEX idx_phone_name_id(phone, name, id);
-- 或者(MySQL 5.6+ 自动包含主键)
ALTER TABLE t ADD INDEX idx_phone_name(phone, name);
-- 这样查询就可以直接从索引返回,避免回表
SELECT id, name FROM t WHERE phone = '13800000000';
第二梯队:高频进阶题(出现率 60%+)
11. 最左前缀原则是什么?
核心答案:
最左前缀原则是指:联合索引 (a, b, c) 在使用时,查询条件必须从索引的最左列开始,不能跳过中间的列。MySQL 会从左到右匹配索引列,直到遇到范围查询(>、<、BETWEEN、LIKE)为止。
详细说明:
假设有一个联合索引 (a, b, c),以下查询可以走索引:
sql
sql
-- 使用 a 列
WHERE a = 1
-- 使用 a, b 两列
WHERE a = 1 AND b = 2
-- 使用 a, b, c 三列
WHERE a = 1 AND b = 2 AND c = 3
-- a 和 c 查询,但跳过了 b
WHERE a = 1 AND c = 3
-- 实际只有 a 用到了索引,c 无法使用(因为 b 被跳过)
以下查询无法使用索引或只能部分使用:
sql
sql
-- 没有 a 列,完全无法使用索引
WHERE b = 2
-- 跳过了 a,无法使用索引
WHERE b = 2 AND c = 3
范围查询的影响:
sql
sql
-- 假设查询条件
WHERE a = 1 AND b > 2 AND c = 3
-- 索引使用情况:
-- a = 1 可以用索引定位
-- b > 2 可以用索引范围扫描
-- c = 3 无法使用索引,因为 b 使用了范围查询后,c 就无序了
为什么会有最左前缀原则?
B+ 树索引的排序规则是:先按 a 排序,a 相同按 b 排序,b 相同按 c 排序。所以如果查询条件中没有 a,就无法确定索引的起始位置。如果跳过 b 直接查 c,因为 b 是无序的,c 也是无序的,无法利用索引。
12. 什么是索引下推(ICP)?
核心答案:
索引下推(Index Condition Pushdown,ICP) 是 MySQL 5.6 引入的优化。它的核心是:将 where 条件中能用索引过滤的部分,下推到存储引擎层进行过滤,减少回表次数。
详细说明:
没有 ICP 之前 :
存储引擎层只根据索引找到数据,然后回表取出完整行,再交给 Server 层判断 where 条件。即使索引中包含 where 条件中的字段,也只能在 Server 层过滤,导致很多不必要的回表。
有 ICP 之后 :
存储引擎层在遍历索引时,直接在索引上判断 where 条件,只有满足条件的才回表。
举例说明:
sql
-- 假设有联合索引 (name, age)
SELECT * FROM t WHERE name LIKE '张%' AND age = 20;
没有 ICP 的执行过程:
-
存储引擎找到所有 name 以 '张' 开头的记录(可能很多条)
-
对每条记录回表取完整数据
-
返回给 Server 层,Server 层再过滤 age = 20
-
问题:很多 name 符合但 age 不符合的记录也回了表,浪费 I/O
有 ICP 的执行过程:
-
存储引擎遍历 name 索引,找到 name 以 '张' 开头的记录
-
在索引上直接判断 age = 20(因为 age 也在索引中)
-
只有 age = 20 的记录才回表取完整数据
-
大大减少了回表次数
如何确认是否使用了 ICP :
在 EXPLAIN 的 Extra 字段中看到 Using index condition,就说明使用了索引下推。
适用条件:
-
ICP 适用于二级索引
-
需要查询的字段不全在索引中(否则就直接覆盖索引了)
-
where 条件中有一部分字段在索引中
13. 什么是 Next-Key Lock?如何解决幻读?
核心答案:
Next-Key Lock = 行锁(Record Lock)+ 间隙锁(Gap Lock) 。它锁住索引记录本身,也锁住记录之间的间隙,阻止其他事务在这个间隙中插入新数据,从而在 REPEATABLE READ 级别下解决幻读问题。
详细说明:
三种锁的类型:
-
Record Lock(行锁):锁定单条索引记录
-
Gap Lock(间隙锁):锁定索引记录之间的间隙(不锁记录本身),防止插入
-
Next-Key Lock(临键锁):锁定记录本身 + 记录前的间隙
举例说明间隙锁 :
假设有一个表,id 列有索引,现有数据 id = 5, 10, 15, 20。
sql
-- 事务 A 执行
SELECT * FROM t WHERE id BETWEEN 10 AND 15 FOR UPDATE;
这个查询会锁住什么?
-
Record Lock:锁住 id = 10 和 id = 15 的记录
-
Gap Lock:锁住 (5,10) 之间的间隙和 (10,15) 之间的间隙
其他事务尝试插入 id = 12 会被阻塞 ,因为 (10,15) 间隙被锁住了。这样就防止了幻读:事务 A 两次查询 BETWEEN 10 AND 15 结果集不会变化。
Next-Key Lock 的退化规则:
-
唯一索引等值查询且记录存在:Next-Key Lock 退化为 Record Lock(只锁记录本身)
-
唯一索引等值查询且记录不存在:Next-Key Lock 退化为 Gap Lock(只锁间隙)
-
普通索引或范围查询:使用 Next-Key Lock
为什么唯一索引等值查询不需要间隙锁?
因为唯一索引保证值唯一,其他事务不可能插入相同值的数据,所以不需要锁间隙防止幻读。
14. 死锁是如何发生的?如何排查和避免?
核心答案:
死锁是两个或多个事务互相持有对方需要的锁,并且互相等待,导致都无法继续执行。
详细说明:
死锁发生的四个必要条件:
-
互斥:资源只能被一个事务持有
-
持有并等待:事务持有锁的同时等待其他锁
-
不可剥夺:已持有的锁不能被其他事务强行剥夺
-
循环等待:事务之间形成等待环
举例:
text
事务 A:UPDATE t SET age=20 WHERE id=1; -- 持有 id=1 的行锁
事务 B:UPDATE t SET age=30 WHERE id=2; -- 持有 id=2 的行锁
事务 A:UPDATE t SET age=25 WHERE id=2; -- 等待事务 B 释放 id=2
事务 B:UPDATE t SET age=35 WHERE id=1; -- 等待事务 A 释放 id=1
-- 形成死锁
排查死锁的方法:
- 查看最近一次死锁信息
sql
SHOW ENGINE INNODB STATUS;
重点关注 LATEST DETECTED DEADLOCK 部分,可以看到:
-
哪些事务参与了死锁
-
每个事务持有的锁和等待的锁
-
MySQL 选择哪个事务作为"牺牲品"回滚
- 查看当前正在运行的事务
sql
SELECT * FROM information_schema.INNODB_TRX\G
查看 trx_state、trx_started、trx_mysql_thread_id 等字段。
- 查看当前锁信息
sql
SELECT * FROM performance_schema.data_locks;
避免死锁的方法:
-
统一访问顺序
让所有事务按相同的顺序访问资源,避免循环等待。
-
保持事务简短
长事务持有锁的时间长,增加死锁概率。及时提交事务。
-
使用较低的隔离级别
比如 READ COMMITTED,减少间隙锁的使用。
-
合理设计索引
索引选择性好,锁定的行数就少,降低冲突概率。
-
使用
SELECT ... FOR UPDATE时注意顺序主动加锁时,确保按相同顺序加锁。
-
使用
INSERT ... ON DUPLICATE KEY UPDATE时注意这种语句会加 Next-Key Lock,可能造成死锁。
MySQL 的死锁处理 :
MySQL 会自动检测死锁 ,选择一个事务作为"牺牲品"回滚(通常是更新行数较少的事务),另一个事务继续执行。应用层需要重试机制来处理被回滚的事务。
15. binlog 的三种格式有什么区别?
核心答案:
binlog 有三种格式:
-
STATEMENT:记录原始 SQL 语句
-
ROW:记录每行数据的变化(前镜像和后镜像)
-
MIXED:MySQL 自动选择,STATEMENT 可能不安全时用 ROW
详细说明:
STATEMENT 格式:
-
记录的是执行的 SQL 语句
-
优点:日志量小,节省磁盘空间和网络传输
-
缺点:可能造成主从数据不一致。比如
NOW()、UUID()、RAND()等非确定性函数,在主库执行时得到一个值,从库重放时又得到另一个值,导致数据不一致
ROW 格式:
-
记录的是每行数据的变化,格式为"修改前数据 + 修改后数据"
-
优点:最安全,保证主从数据完全一致。即使是非确定性函数也能正确复制
-
缺点:日志量大,尤其是批量更新时,每条记录的变化都会记录
-
额外优势:可以通过 binlog 进行数据恢复,能精确到每一行的变化
MIXED 格式:
-
MySQL 自动判断:普通 SQL 用 STATEMENT,如果包含非确定性函数或可能不安全(如
LIMIT不带ORDER BY)时自动切换为 ROW -
优点:兼顾 STATEMENT 的日志量小和 ROW 的安全性
生产环境建议:
-
如果对数据一致性要求高(大多数业务场景),推荐使用 ROW 格式
-
MySQL 8.0 默认使用 ROW 格式
如何查看和设置 binlog 格式:
sql
-- 查看当前格式
SHOW VARIABLES LIKE 'binlog_format';
-- 设置格式(需要重启或动态设置)
SET GLOBAL binlog_format = 'ROW';
16. 主从复制的原理是什么?有哪些复制方式?
核心答案:
主从复制的核心是:主库将数据变更写入 binlog,从库拉取 binlog 并重放。
详细说明:
复制架构:
text
主库(Master) 从库(Slave)
│ │
│ binlog │
│ ───────────────────>│ I/O 线程
│ │ ↓
│ relay log
│ │
│ │ SQL 线程
│ │ ↓
│ 数据文件
三个线程的工作:
-
主库的 binlog dump 线程:当从库连接时,主库启动这个线程,负责读取 binlog 并发送给从库
-
从库的 I/O 线程:连接主库,拉取 binlog 并写入本地的 relay log(中继日志)
-
从库的 SQL 线程:读取 relay log 并重放(执行 SQL),把数据写入从库
三种复制方式:
1. 异步复制(默认)
-
主库提交事务后,不等待从库确认,直接返回客户端成功
-
优点:性能最好,对主库无影响
-
缺点:如果主库崩溃时从库还没收到 binlog,数据可能丢失
2. 半同步复制
-
主库提交事务后,等待至少一个从库确认收到 binlog 后才返回客户端成功
-
优点:数据安全性更高,至少有一个从库有完整数据
-
缺点:性能略低于异步复制,如果从库延迟或故障,主库会等待(可设置超时时间,超时后降级为异步)
-
需要安装插件:
rpl_semi_sync_master和rpl_semi_sync_slave
3. 组复制(Group Replication)
-
MySQL 5.7 引入,基于 Paxos 协议,多主或多单主模式
-
优点:高可用性、自动故障切换、数据强一致性
-
缺点:配置复杂,网络要求高
并行复制 :
MySQL 5.6 开始支持并行复制,从库可以用多个 SQL 线程并行重放 relay log,减少主从延迟。相关参数:
-
slave_parallel_workers:并行线程数 -
slave_parallel_type:LOGICAL_CLOCK(基于事务分组)或DATABASE(基于数据库)
17. 什么是主从延迟?如何解决?
核心答案:
主从延迟 是指从库的数据落后于主库的时间差,通常表现为 Seconds_Behind_Master 大于 0。主要原因是从库单线程重放跟不上主库的写入速度。
详细说明:
查看主从延迟:
sql
SHOW SLAVE STATUS\G
-- 关注字段:
-- Seconds_Behind_Master:延迟秒数
-- Relay_Log_File / Relay_Log_Pos:当前重放位置
-- Master_Log_File / Read_Master_Log_Pos:已拉取的位置
常见原因:
-
大事务:一个事务在从库重放耗时很长,期间无法处理其他事务
-
从库单线程重放:虽然主库可以并行写入,但从库 SQL 线程早期是单线程的
-
从库硬件差:从库配置低于主库,执行 SQL 更慢
-
锁竞争:从库重放时的锁等待
-
从库有其他查询负载:如报表查询占用从库资源
解决方案:
1. 开启并行复制
sql
-- MySQL 5.7+ 设置
SET GLOBAL slave_parallel_workers = 8;
SET GLOBAL slave_parallel_type = 'LOGICAL_CLOCK';
2. 拆解大事务
-
将大批量更新拆分为多个小事务
-
比如一次删除 100 万行,改为每次删除 1 万行,循环执行
3. 优化从库硬件
-
从库使用 SSD
-
调整从库的
innodb_flush_log_at_trx_commit参数
4. 使用半同步复制
- 保证主从数据实时性,但会略微影响主库性能
5. 读写分离策略
-
对延迟敏感的业务读主库
-
对延迟不敏感的业务(如报表)读从库
6. 使用组复制或 MGR
- 提供更强的一致性保证
18. 什么是分库分表?有哪些策略?
核心答案:
分库分表是当单库单表数据量过大、并发过高时,将数据分散到多个库和多个表中的技术方案。
详细说明:
分库分表的两种类型:
1. 垂直分库/分表
-
垂直分表:将一张宽表按字段拆分为多张表,比如把不常访问的大字段(如 text、blob)拆出去
-
垂直分库:按业务模块拆分到不同数据库,如订单库、用户库、商品库
2. 水平分库/分表
-
水平分表:将同一张表的数据按规则分散到多张结构相同的表中
-
水平分库:将数据分散到多个数据库实例中
常用分片策略:
| 策略 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| 取模分片 | shard_key % 库/表数量 |
数据分布均匀 | 扩容时数据需要迁移 |
| 范围分片 | 按时间、ID 范围划分 | 扩容简单,只需新增分片 | 可能产生热点(如近期数据) |
| 一致性哈希 | 哈希环算法 | 扩容时数据迁移量小 | 实现复杂 |
| 哈希 + 范围 | 先哈希,再按范围 | 兼顾均匀和扩展性 | 复杂度高 |
分库分表带来的问题:
-
跨分片 JOIN:无法直接 JOIN,需要在应用层聚合或使用全局表(广播表)
-
分布式事务:跨分片的事务需要 XA 或最终一致性方案
-
全局唯一 ID:不能使用自增主键,需要分布式 ID 生成器(如雪花算法、美团 Leaf)
-
分页查询:跨分片的排序分页需要在中间件层聚合,性能差
-
扩容困难:取模分片扩容需要数据迁移
常用中间件:
-
ShardingSphere:Apache 开源,支持 Java 应用层分片
-
MyCat:基于 Proxy 的数据库中间件
-
Vitess:YouTube 开源,支持大规模集群
19. 什么是分布式 ID?有哪些生成方案?
核心答案:
分布式 ID 是在分布式系统中生成全局唯一标识符的方案。在分库分表场景下,数据库自增 ID 无法保证全局唯一,需要独立的 ID 生成器。
详细说明:
分布式 ID 的要求:
-
全局唯一:不能重复
-
趋势递增:便于数据库索引(B+ 树插入效率)
-
高性能:生成 ID 的 QPS 要高
-
高可用:不能成为系统瓶颈
常用方案对比:
1. UUID
-
格式:32 位十六进制数,如
550e8400-e29b-41d4-a716-446655440000 -
优点:本地生成,性能高,实现简单
-
缺点:无序 ,作为主键会导致 B+ 树频繁页分裂;太长,占用存储空间大
2. 数据库自增 ID 单独表
-
创建一张专门用来生成 ID 的表,使用
REPLACE INTO或INSERT ... ON DUPLICATE KEY UPDATE获取自增 ID -
优点:实现简单,ID 趋势递增
-
缺点:单点故障风险,性能受限于数据库
3. 数据库号段模式
-
预先分配一批 ID 到应用内存,用完后再取新号段
-
优点:减少数据库访问,性能高
-
缺点:应用重启可能浪费未用完的 ID
4. Redis 生成 ID
-
使用
INCR或INCRBY命令 -
优点:性能高,可以控制步长
-
缺点:Redis 持久化可能丢失 ID,需要额外的维护成本
5. 雪花算法(Snowflake)
-
Twitter 开源的分布式 ID 生成算法,64 位整数,结构如下:
-
1 位:符号位,0 表示正数
-
41 位:时间戳(毫秒级),可以支持 69 年
-
10 位:机器 ID(5 位数据中心 + 5 位机器)
-
12 位:序列号,同一毫秒内最多 4096 个 ID
-
-
优点:趋势递增,性能高(单机每秒可达数十万),不依赖外部服务
-
缺点:依赖机器时钟,时钟回拨会导致 ID 重复
雪花算法时钟回拨解决方案:
-
等待时钟追上
-
预留序列号空间
-
使用时钟回拨检测,如果回拨小于阈值则等待,大于阈值则报错
6. 美团 Leaf
-
基于雪花算法和号段模式的双方案
-
提供高可用、高可靠的分布式 ID 服务
-
支持通过 ZooKeeper 管理机器 ID
20. CHAR 和 VARCHAR 的区别?
核心答案:
| 对比项 | CHAR | VARCHAR |
|---|---|---|
| 存储方式 | 定长,固定分配空间 | 变长,按实际长度存储 |
| 最大长度 | 255 字节 | 65535 字节 |
| 存储开销 | 无长度前缀 | 有 1-2 字节长度前缀 |
| 空间效率 | 浪费空间 | 节省空间 |
| 性能 | 读取快 | 写入有额外开销 |
详细说明:
CHAR(n):
-
固定长度,无论实际存储多少字符,都分配 n 个字符的空间
-
存储时会删除末尾空格,读取时会补上空格
-
适合存储长度固定的数据,如手机号、MD5 值、状态码等
VARCHAR(n):
-
可变长度,按实际存储的字符数分配空间
-
需要额外 1-2 字节存储长度信息(n <= 255 时 1 字节,否则 2 字节)
-
适合存储长度变化较大的数据,如用户名、地址、描述等
选择建议:
-
长度固定或变化很小:用 CHAR(如手机号、身份证号)
-
长度变化大:用 VARCHAR
-
VARCHAR(255) 是一个常用值,因为 255 以内长度前缀只需 1 字节
21. COUNT(*) 和 COUNT(1) 和 COUNT(列) 的区别?
核心答案:
COUNT(*) 和 COUNT(1) 性能相同,都统计所有行数。COUNT(列) 统计该列非 NULL 值的行数。
详细说明:
-
COUNT(*):统计所有行的数量,包括 NULL 值。优化器会优先选择最小的非聚簇索引来统计,如果没有索引就全表扫描。
-
COUNT(1):与 COUNT(*) 完全等价,优化器会做同样的优化,没有性能差异。
-
COUNT(列):统计指定列非 NULL 值的数量。如果该列有 NOT NULL 约束,优化器可以转换为 COUNT(*),否则需要逐行判断是否为 NULL。
常见误解:
-
有人说 COUNT(1) 比 COUNT(*) 快,这是错误的。MySQL 优化器已经做了优化,两者完全相同。
-
有人说 COUNT(主键) 最快,实际上优化器可能选择更小的二级索引,不一定选择主键。
性能排序(有 NOT NULL 约束时) :
COUNT(列) 如果列有索引且 NOT NULL,性能与 COUNT(*) 接近。
性能排序(可能包含 NULL 时) :
COUNT(*) ≈ COUNT(1) > COUNT(主键) > COUNT(有索引列) > COUNT(无索引列)
22. 什么是 SQL 注入?如何防范?
核心答案:
SQL 注入是攻击者通过构造恶意的 SQL 语句,欺骗数据库执行非预期的 SQL 命令,从而获取敏感数据或破坏数据库的安全漏洞。
详细说明:
SQL 注入的原理:
java
// 错误写法:直接拼接用户输入
String sql = "SELECT * FROM users WHERE name = '" + userName + "'";
// 如果 userName 输入:' OR '1'='1
// 最终执行的 SQL:SELECT * FROM users WHERE name = '' OR '1'='1'
// 这会返回所有用户数据
防范方法:
1. 使用预编译语句(PreparedStatement)
java
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE name = ?");
ps.setString(1, userName);
// 参数会被安全转义,不会作为 SQL 代码执行
2. 使用 ORM 框架
MyBatis、Hibernate 等框架默认使用预编译,注意 MyBatis 中 #{} 是预编译,${} 是字符串拼接,尽量避免使用 ${}。
3. 严格校验输入
对用户输入进行白名单校验,如枚举值、正则表达式等。
4. 最小权限原则
数据库账号只授予必要的权限,不使用 root 账号连接应用。
5. 使用 Web 应用防火墙
检测和拦截 SQL 注入攻击。
6. 隐藏数据库错误信息
生产环境不暴露详细的数据库错误信息,避免给攻击者提供线索。
23. UNION 和 UNION ALL 的区别?
核心答案:
UNION 会对结果集进行去重 ,UNION ALL 保留所有结果(包括重复行)。UNION ALL 性能更好,因为不需要去重操作。
详细说明:
sql
-- UNION:会去重,有额外排序开销
SELECT id FROM t1 UNION SELECT id FROM t2;
-- UNION ALL:不去重,直接拼接结果
SELECT id FROM t1 UNION ALL SELECT id FROM t2;
使用建议:
-
如果确定结果集不会重复,或者可以接受重复,使用 UNION ALL
-
去重操作需要排序或哈希,消耗 CPU 和内存
-
UNION ALL 不会产生临时表(某些情况),执行效率更高
24. EXISTS 和 IN 的区别?哪个性能更好?
核心答案:
EXISTS 和 IN 都可以用于子查询,但执行逻辑不同。性能取决于子查询结果集大小 和外层查询大小。
详细说明:
IN 的执行逻辑:
sql
SELECT * FROM t1 WHERE id IN (SELECT id FROM t2);
先执行子查询,将结果集(通常是 t2 的 id 列表)物化,然后遍历 t1 判断是否在结果集中。适合子查询结果集较小的场景。
EXISTS 的执行逻辑:
sql
SELECT * FROM t1 WHERE EXISTS (SELECT 1 FROM t2 WHERE t2.id = t1.id);
遍历 t1 的每一行,执行子查询判断是否存在匹配。适合外层结果集较小 或子查询有索引的场景。
性能选择原则:
-
IN 优化:子查询结果集小,可以用 IN
-
EXISTS 优化:外层结果集小,子查询大,用 EXISTS
-
MySQL 优化器会做优化,但在复杂场景下需要手动选择
注意:
-
子查询中的
SELECT 1或SELECT *不影响结果,EXISTS 只关心是否存在匹配行 -
现代 MySQL 版本中,IN 和 EXISTS 在某些情况下会被优化器重写为相同的执行计划
25. 什么是三大范式?为什么要遵守?
核心答案:
数据库三大范式 是规范数据库表设计的准则,目的是减少数据冗余 ,避免数据不一致。
详细说明:
第一范式(1NF):原子性
-
要求:列不可再分,即每个字段都是原子值
-
反例:
phone_numbers字段存储138****,139****多个号码 -
正例:拆分为多行或多列
第二范式(2NF):完全依赖
-
要求:在 1NF 基础上,非主键列必须完全依赖主键(不能部分依赖)
-
场景:联合主键表中,如果某列只依赖主键的一部分,就违反 2NF
-
解决:拆分成多张表
第三范式(3NF):无传递依赖
-
要求:在 2NF 基础上,非主键列不能传递依赖主键
-
反例:订单表中有
user_id、user_name,user_name 依赖 user_id,不直接依赖订单 id -
解决:将用户信息拆分到用户表
是否必须严格遵守?
-
生产环境不一定要完全遵守 ,有时会反范式设计以提升性能
-
适当冗余可以减少 JOIN 查询,提高读性能
-
需要在冗余带来的性能提升 和维护成本增加之间权衡
第三梯队:深度进阶题(出现率 30%+)
26. 什么是自适应哈希索引?
核心答案:
自适应哈希索引(Adaptive Hash Index,AHI) 是 InnoDB 的一个优化特性,它会根据查询频率 ,自动将热点索引页的某些值构建成哈希索引,实现 O(1) 级别的单点查询。
详细说明:
工作原理:
-
InnoDB 监控对 B+ 树索引的访问
-
如果发现某个页被频繁访问,并且满足一定条件(如同一个值被多次查询),就为该页构建哈希索引
-
哈希索引只存在于内存中,不持久化
-
下次查询可以直接通过哈希查找,避免 B+ 树遍历
优点:
-
热点数据的等值查询性能大幅提升
-
对业务透明,自动生效
缺点:
-
消耗额外的内存
-
对写入性能有一定影响(维护哈希索引有开销)
查看状态:
sql
SHOW ENGINE INNODU STATUS\G
-- 查看 INSERT BUFFER AND ADAPTIVE HASH INDEX 部分
控制参数:
sql
-- 关闭自适应哈希索引
SET GLOBAL innodb_adaptive_hash_index = OFF;
27. 什么是 change buffer?有什么作用?
核心答案:
change buffer 是 InnoDB 的一个优化机制,用于缓存对非唯一二级索引页的修改操作,减少随机 I/O,提升写入性能。
详细说明:
工作原理:
-
当更新一个非唯一二级索引时,如果目标索引页不在内存中,InnoDB 不会立即从磁盘读取该页
-
而是将修改操作缓存到 change buffer 中
-
后续当该索引页被读取到内存时,再将 change buffer 中的修改合并(称为 merge)
适用场景:
-
只适用于非唯一二级索引
-
唯一索引需要立即检查唯一性,不能缓存
-
写多读少的场景效果最明显
优点:
-
减少随机 I/O:将多次随机写合并为一次
-
提升写入性能:批量写入
控制参数:
sql
-- 查看 change buffer 大小
SHOW VARIABLES LIKE 'innodb_change_buffer_max_size';
-- 默认 25,表示最大占用 buffer pool 的 25%
监控:
sql
SHOW ENGINE INNODU STATUS\G
-- 查看 INSERT BUFFER AND ADAPTIVE HASH INDEX 部分
-- merged operations:合并次数
-- inserts:插入操作数
28. 什么是 doublewrite buffer?为什么需要?
核心答案:
doublewrite buffer(双写缓冲区) 是 InnoDB 保证数据页完整性的机制,用于解决部分写失效问题。
详细说明:
部分写失效:
-
磁盘写入的最小单位是扇区(512 字节),而 InnoDB 的数据页是 16KB
-
如果写数据页时发生断电或系统崩溃,可能只写入了部分数据(如只写 4KB)
-
导致数据页损坏,redo log 也无法恢复(redo log 记录的是页内修改,依赖页的完整性)
doublewrite buffer 的工作流程:
-
当 InnoDB 需要将脏页刷盘时,先写入 doublewrite buffer(连续写,顺序 I/O)
-
doublewrite buffer 写入成功后,再写入数据文件的实际位置(随机 I/O)
-
崩溃恢复时,如果发现数据页损坏,从 doublewrite buffer 中恢复
doublewrite buffer 的位置:
-
位于系统表空间(ibdata 文件)中,共 2MB,分为 2 个区(每个 1MB)
-
每次写入 128 个页(128 × 16KB = 2MB)
为什么能解决部分写:
-
写入 doublewrite buffer 时是顺序写,即使崩溃也不影响已写入的完整页
-
恢复时检查数据页的 checksum,如果校验失败,从 doublewrite buffer 中拷贝正确版本
性能影响:
-
每次脏页刷盘需要写两次:一次 doublewrite,一次实际位置
-
但 doublewrite 是顺序写,开销相对较小
-
SSD 场景下,可以通过
skip_innodb_doublewrite关闭(有风险)
29. 什么是页分裂?如何避免?
核心答案:
页分裂 是当 InnoDB 的 B+ 树索引页写满后,需要插入新数据时,将一页拆分为两页的过程。自增主键可以避免页分裂。
详细说明:
页分裂的过程:
-
InnoDB 数据页默认 16KB,当页写满后,新插入数据需要分配新页
-
将原页的一部分数据移动到新页,并调整父节点指针
-
这是一个开销较大的操作,需要移动数据、更新指针
页分裂的影响:
-
性能下降:需要额外 I/O 和 CPU
-
空间浪费:新页可能未写满,产生碎片
-
索引维护成本高
如何避免:
-
使用自增主键:新数据总是插入到当前页末尾,页满时分配新页追加,不会触发分裂
-
避免使用 UUID 或随机值作为主键:插入位置随机,频繁导致页分裂
-
合理设置
fill_factor(填充因子):保留一定空间
举例:
text
自增主键插入:1,2,3,4,5... 始终在最后,页满就开新页
UUID 插入:随机位置,可能插入到中间页,导致页分裂
30. 什么是脏读、不可重复读、幻读?区别是什么?
核心答案:
这三种是并发事务可能产生的数据不一致问题,严重程度递增,不同隔离级别解决不同问题。
详细说明:
| 问题 | 定义 | 产生条件 | 解决方案 |
|---|---|---|---|
| 脏读 | 读到未提交的数据 | 事务 A 修改但未提交,事务 B 读取 | 隔离级别 ≥ RC |
| 不可重复读 | 同一事务两次读同一行结果不同 | 事务 B 在事务 A 两次查询之间修改并提交了数据 | 隔离级别 ≥ RR |
| 幻读 | 同一事务两次范围查询结果集不同 | 事务 B 在事务 A 两次查询之间插入或删除了数据 | 隔离级别 = Serializable 或 RR + Next-Key Lock |
举例说明:
脏读:
text
事务 A:UPDATE 账户 SET 余额 = 余额 - 100 WHERE id = 1;(未提交)
事务 B:SELECT 余额 FROM 账户 WHERE id = 1; -- 读到减少后的金额
事务 A:ROLLBACK; -- 回滚,事务 B 读到的就是脏数据
不可重复读:
text
事务 A:SELECT 余额 FROM 账户 WHERE id = 1; -- 得到 1000
事务 B:UPDATE 账户 SET 余额 = 900 WHERE id = 1; COMMIT;
事务 A:SELECT 余额 FROM 账户 WHERE id = 1; -- 得到 900,两次结果不同
幻读:
text
事务 A:SELECT COUNT(*) FROM 账户 WHERE 余额 > 500; -- 得到 5 条
事务 B:INSERT INTO 账户 (余额) VALUES (600); COMMIT;
事务 A:SELECT COUNT(*) FROM 账户 WHERE 余额 > 500; -- 得到 6 条,多出幻影行
31. 一条 UPDATE 语句的执行流程是怎样的?
核心答案:
一条 UPDATE 语句在 MySQL 中要经过连接器、分析器、优化器、执行器、存储引擎 等多个组件,涉及 undo log、redo log、binlog 的写入。
详细说明:
text
UPDATE t SET age = 20 WHERE id = 1;
执行流程:
-
连接器:验证用户名密码,获取权限
-
分析器:词法分析识别表名、字段,语法分析检查 SQL 语法
-
优化器:选择最优执行计划,决定使用哪个索引(id 主键索引)
-
执行器:调用 InnoDB 引擎接口
InnoDB 内部流程:
-
根据 id=1 在主键索引树上找到该行数据(如果不在内存,先从磁盘读入 buffer pool)
-
记录 undo log:将修改前的数据写入 undo log(用于回滚和 MVCC)
-
修改内存数据:将 buffer pool 中的数据改为 age = 20,标记为脏页
-
写入 redo log(prepare 状态):将修改记录到 redo log buffer,状态为 prepare
-
写入 binlog:将 SQL 或行变化写入 binlog
-
提交事务:将 redo log 状态改为 commit,释放锁
两阶段提交:
-
第 8 步写入 redo log prepare
-
第 9 步写入 binlog
-
第 10 步 redo log commit
-
保证 redo log 和 binlog 逻辑一致
脏页刷盘:
-
后台线程定期将 buffer pool 中的脏页刷回磁盘
-
时机:redo log 写满、buffer pool 空间不足、MySQL 空闲时、MySQL 关闭时
32. 什么是 buffer pool?如何调优?
核心答案:
buffer pool 是 InnoDB 的内存缓冲池,用于缓存数据页和索引页,减少磁盘 I/O,是 InnoDB 性能的核心。
详细说明:
buffer pool 的作用:
-
缓存数据页和索引页
-
缓存 change buffer
-
缓存自适应哈希索引
-
缓存行锁信息
关键参数:
sql
-- buffer pool 大小(建议设置为物理内存的 70%-80%)
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
-- buffer pool 实例数(多实例减少锁竞争)
SHOW VARIABLES LIKE 'innodb_buffer_pool_instances';
调优建议:
-
合理设置大小:
-
专用数据库服务器:物理内存的 70%-80%
-
混合部署:根据实际情况调整,留足系统内存
-
-
开启多实例:
-
当 buffer pool >= 8GB 时,设置
innodb_buffer_pool_instances = 8 -
减少内部锁竞争
-
-
预热 buffer pool:
sql
-- 保存当前 buffer pool 状态 SET GLOBAL innodb_buffer_pool_dump_now = ON; -- 重启后加载 SET GLOBAL innodb_buffer_pool_load_now = ON; -
监控命中率:
sql
SHOW ENGINE INNODU STATUS\G -- 查看 Buffer pool hit rate -- 命中率应该 > 99%
33. 什么是 MySQL 的查询缓存?为什么被移除?
核心答案:
查询缓存 是 MySQL 5.7 及之前版本的一个特性,用于缓存 SELECT 语句的结果集。在 MySQL 8.0 中被彻底移除。
详细说明:
工作原理:
-
当执行 SELECT 查询时,MySQL 将 SQL 语句作为 key,结果集作为 value 存入内存
-
再次执行相同 SQL 时,直接从缓存返回结果,跳过解析、优化、执行等步骤
为什么被移除:
-
失效太频繁:只要表数据发生变化(INSERT、UPDATE、DELETE),该表的所有查询缓存都失效。对于写多读少的场景,缓存几乎无效
-
锁竞争严重:查询缓存有全局锁,多个查询同时访问时会竞争
-
缓存粒度粗:以 SQL 语句为粒度,多一个空格、大小写不同都不命中
-
维护成本高:缓存失效检查、内存管理增加复杂度
替代方案:
-
使用 Redis 等外部缓存,更灵活、可控
-
应用层自己做缓存,可以精细控制失效策略
34. 什么是 online DDL?如何减少 DDL 对业务的影响?
核心答案:
online DDL 是 MySQL 5.6 引入的特性,允许在执行 DDL 操作(如 ALTER TABLE)时,不阻塞并发的 DML 操作,减少对业务的影响。
详细说明:
online DDL 的三种类型:
| 操作类型 | 是否允许并发 DML | 是否需要重建表 |
|---|---|---|
| INSTANT | 允许,瞬间完成 | 否(MySQL 8.0+) |
| INPLACE | 允许 | 是(原地重建) |
| COPY | 不允许(阻塞) | 是(复制表) |
常用 online DDL 操作:
sql
-- 添加索引(INPLACE,允许并发 DML)
ALTER TABLE t ADD INDEX idx_name (name), ALGORITHM=INPLACE, LOCK=NONE;
-- 添加列(MySQL 8.0 支持 INSTANT)
ALTER TABLE t ADD COLUMN age INT, ALGORITHM=INSTANT;
-- 修改列类型(通常需要 COPY,会锁表)
ALTER TABLE t MODIFY COLUMN name VARCHAR(200), ALGORITHM=COPY;
减少影响的策略:
-
使用 online DDL :指定
ALGORITHM=INPLACE和LOCK=NONE -
使用 pt-online-schema-change:Percona 工具,通过触发器实现无锁 DDL
-
在业务低峰期执行:如凌晨 2-4 点
-
分批执行:大表操作拆分为多个小操作
-
监控 DDL 进度:
sql
-- MySQL 5.7+ 查看 DDL 进度 SELECT * FROM performance_schema.events_stages_current;
35. 什么是临时表?什么情况下会使用临时表?
核心答案:
临时表 是 MySQL 在执行某些 SQL 时,由于无法直接返回结果,需要在内存或磁盘中创建临时表来存储中间结果。
详细说明:
临时表的类型:
-
内存临时表:使用 MEMORY 引擎,数据量小时优先使用
-
磁盘临时表 :使用 InnoDB 或 MyISAM,当内存临时表超过
tmp_table_size或max_heap_table_size时转换为磁盘临时表
什么情况会使用临时表:
-
GROUP BY 没有使用索引
-
DISTINCT 无法使用索引去重
-
UNION(不是 UNION ALL)需要去重
-
ORDER BY 和 GROUP BY 的列不同
-
子查询的物化
-
多表 JOIN 时某些特殊情况
如何识别 :
在 EXPLAIN 的 Extra 字段中看到 Using temporary,说明使用了临时表。
优化方法:
-
为 GROUP BY、ORDER BY 的字段建立合适的索引
-
使用 UNION ALL 代替 UNION(如果不需要去重)
-
增加
tmp_table_size和max_heap_table_size的值 -
优化 SQL 结构,避免复杂的聚合
36. 什么是慢查询日志?如何分析?
核心答案:
慢查询日志 是 MySQL 记录执行时间超过指定阈值的 SQL 语句的日志,是性能优化的首要工具。
详细说明:
配置慢查询日志:
sql
-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
-- 设置慢查询阈值(秒)
SET GLOBAL long_query_time = 1;
-- 设置日志文件路径
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
-- 是否记录未使用索引的查询
SET GLOBAL log_queries_not_using_indexes = ON;
分析工具:
-
mysqldumpslow(MySQL 自带):
bash
# 按平均查询时间排序,取前 10 条 mysqldumpslow -s at -t 10 /var/log/mysql/slow.log -
pt-query-digest(Percona Toolkit):
bash
# 生成详细分析报告 pt-query-digest /var/log/mysql/slow.log > slow_report.txt
分析维度:
-
Query_time:执行时间,越长越需要优化
-
Lock_time:锁等待时间
-
Rows_examined:扫描行数,应尽量小
-
Rows_sent:返回行数,与 Rows_examined 对比判断索引效率
37. 什么是执行计划?如何读懂?
核心答案:
执行计划 是 MySQL 优化器根据 SQL 语句生成的具体执行方案 ,通过 EXPLAIN 命令查看,是优化 SQL 的核心依据。
详细说明:
EXPLAIN 输出关键字段:
| 字段 | 含义 | 重点关注 |
|---|---|---|
| id | 执行顺序 | id 越大越先执行,相同则从上到下 |
| select_type | 查询类型 | SIMPLE(简单查询)、PRIMARY(外层)、SUBQUERY(子查询)、DERIVED(派生表)、UNION 等 |
| table | 访问的表 | - |
| type | 访问类型 | system > const > eq_ref > ref > range > index > ALL,越靠左越好 |
| possible_keys | 可能使用的索引 | - |
| key | 实际使用的索引 | NULL 表示没走索引 |
| key_len | 索引长度 | 可以判断联合索引用了哪几列 |
| ref | 索引列与什么比较 | const(常量)、字段名等 |
| rows | 预估扫描行数 | 越小越好 |
| filtered | 过滤比例 | 越高越好 |
| Extra | 额外信息 | Using index (覆盖索引)、Using where 、Using temporary (需优化)、Using filesort (需优化)、Using index condition(索引下推) |
示例:
sql
EXPLAIN SELECT * FROM t WHERE name = '张三'\G
type 详解:
-
system:系统表,只有一行
-
const:主键或唯一索引等值查询
-
eq_ref:连接查询,被驱动表通过主键/唯一索引访问
-
ref:非唯一索引等值查询
-
range:索引范围查询
-
index:全索引扫描
-
ALL :全表扫描,最差
38. 什么是数据库连接池?为什么需要?
核心答案:
数据库连接池是维护一组数据库连接的缓存池,应用程序从池中获取连接,使用后归还,而不是每次操作都新建和关闭连接。
详细说明:
为什么需要连接池:
-
建立数据库连接是耗时操作(TCP 三次握手、认证、权限检查等)
-
频繁创建和关闭连接会消耗大量系统资源
-
连接池复用连接,大幅提升性能
常用连接池:
-
HikariCP:Spring Boot 默认,性能最好
-
Druid:阿里开源,功能丰富(监控、SQL 防火墙)
-
Tomcat JDBC Pool:Tomcat 内置
-
c3p0:老牌连接池
关键参数:
yaml
# HikariCP 示例
maximumPoolSize: 20 # 最大连接数
minimumIdle: 10 # 最小空闲连接数
connectionTimeout: 30000 # 获取连接超时时间(ms)
idleTimeout: 600000 # 空闲连接存活时间
maxLifetime: 1800000 # 连接最大生命周期
调优建议:
-
最大连接数根据数据库 CPU 核数、并发量设置(通常 10-50)
-
最小空闲连接数根据业务波动设置
-
监控连接池使用率,避免连接数不足或过多
39. 什么是读写分离?如何实现?
核心答案:
读写分离 是将数据库的写操作(INSERT、UPDATE、DELETE)指向主库 ,读操作(SELECT)指向从库,分摊主库压力,提升系统吞吐量。
详细说明:
架构:
text
┌─────────┐
│ 应用层 │
└────┬────┘
│
┌────▼────┐
│ 中间件 │(可选)
└────┬────┘
┌─────┴─────┐
│ │
┌───▼───┐ ┌───▼───┐
│ 主库 │ │ 从库 │
│ 写入 │ │ 读取 │
└───────┘ └───────┘
实现方式:
-
应用层实现:
-
在代码中区分数据源,写用主库,读用从库
-
Spring 中可以使用 AbstractRoutingDataSource 动态路由
-
优点:灵活可控
-
缺点:侵入代码
-
-
中间件实现:
-
ShardingSphere-JDBC:轻量级 Java 组件,支持读写分离
-
MyCat:代理层,对应用透明
-
ProxySQL:高性能 MySQL 代理
-
优点:对应用透明
-
缺点:增加架构复杂度
-
-
MySQL Router:
- MySQL 官方路由,轻量级
注意事项:
-
主从延迟:刚写入的数据可能读不到,需要业务容忍或强制读主库
-
事务一致性:事务内的读操作应该走主库,避免读到不一致数据
-
负载均衡:从库多时,需要负载均衡策略
40. 什么是分库分表的垂直拆分和水平拆分?
核心答案:
-
垂直拆分 :按业务模块 或字段拆分,解决单表列太多或不同业务耦合的问题
-
水平拆分 :按分片键将数据分散到多个表/库,解决单表数据量过大的问题
详细说明:
垂直拆分示例:
垂直分表:
sql
-- 原表:用户表(列太多)
CREATE TABLE user (
id INT PRIMARY KEY,
name VARCHAR(50),
email VARCHAR(100),
password VARCHAR(100),
bio TEXT, -- 大字段,不常访问
avatar BLOB -- 大字段
);
-- 拆分为:
CREATE TABLE user_base (
id INT PRIMARY KEY,
name VARCHAR(50),
email VARCHAR(100),
password VARCHAR(100)
);
CREATE TABLE user_ext (
id INT PRIMARY KEY,
bio TEXT,
avatar BLOB
);
垂直分库:
text
原库:一个库包含订单、用户、商品所有表
拆分为:
- 订单库:订单表、订单详情表
- 用户库:用户表、地址表
- 商品库:商品表、库存表
水平拆分示例:
sql
-- 原表:订单表,数据量巨大(2 亿行)
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT,
amount DECIMAL(10,2),
create_time DATETIME
);
-- 水平拆分:按 user_id 取模 8,分到 8 张表
orders_0, orders_1, orders_2, ..., orders_7
选择分片键的原则:
-
查询频率高:选择最常用的查询字段作为分片键
-
数据分布均匀:避免数据倾斜
-
稳定性:分片键的值不应该频繁变化
第四梯队:极客进阶题(出现率 15%+)
41. MySQL 8.0 有哪些重要新特性?
核心答案:
MySQL 8.0 是里程碑版本,引入了大量重要特性。
详细说明:
1. 窗口函数
sql
-- 按部门排名
SELECT name, dept, salary,
RANK() OVER (PARTITION BY dept ORDER BY salary DESC) as rank
FROM employees;
-- 累计求和
SELECT date, amount,
SUM(amount) OVER (ORDER BY date) as running_total
FROM sales;
2. 通用表达式(CTE)
sql
WITH RECURSIVE org_tree AS (
SELECT id, name, parent_id, 1 as level
FROM org WHERE parent_id IS NULL
UNION ALL
SELECT o.id, o.name, o.parent_id, ot.level + 1
FROM org o
JOIN org_tree ot ON o.parent_id = ot.id
)
SELECT * FROM org_tree;
3. 隐藏索引
sql
-- 隐藏索引,不被优化器使用,可用于测试删除索引的影响
ALTER TABLE t ALTER INDEX idx_name INVISIBLE;
4. 降序索引
sql
-- 真正支持降序索引(之前是反向扫描)
CREATE INDEX idx_name_age ON t (name ASC, age DESC);
5. 原子 DDL
- DDL 操作变成原子操作,中断时会回滚,不会留下残留
6. 默认使用 ROW 格式的 binlog
- 提升主从复制安全性
7. 资源组
- 控制线程的 CPU 资源分配
8. 撤销表空间自动截断
- 解决 undo log 膨胀问题
42. 如何监控 MySQL 性能?关键指标有哪些?
核心答案:
MySQL 性能监控需要关注连接数、QPS/TPS、慢查询、锁等待、buffer pool 命中率、磁盘 I/O等核心指标。
详细说明:
关键指标及查看方式:
1. 连接数
sql
SHOW STATUS LIKE 'Threads_connected'; -- 当前连接数
SHOW VARIABLES LIKE 'max_connections'; -- 最大连接数
-- 告警阈值:连接数 > 80% 最大连接数
2. QPS/TPS
sql
-- QPS(每秒查询数)
SHOW STATUS LIKE 'Queries';
-- TPS(每秒事务数,基于 Com_commit 和 Com_rollback 计算)
SHOW STATUS LIKE 'Com_commit';
SHOW STATUS LIKE 'Com_rollback';
3. 慢查询
sql
SHOW STATUS LIKE 'Slow_queries'; -- 慢查询数量
SHOW VARIABLES LIKE 'long_query_time'; -- 慢查询阈值
4. InnoDB 行锁等待
sql
SHOW STATUS LIKE 'Innodb_row_lock_waits'; -- 锁等待次数
SHOW STATUS LIKE 'Innodb_row_lock_time'; -- 锁等待总时间
-- 告警阈值:锁等待次数持续增长
5. Buffer Pool 命中率
sql
SHOW ENGINE INNODU STATUS\G
-- 查看 Buffer pool hit rate
-- 理想值:> 99%,低于 95% 说明内存不足
6. 主从延迟
sql
SHOW SLAVE STATUS\G
-- 关注 Seconds_Behind_Master
-- 告警阈值:> 60 秒
7. 磁盘 I/O
bash
# 系统层面
iostat -x 1
# 关注 await、%util
监控工具:
-
Prometheus + Grafana:开源监控方案
-
Percona Monitoring and Management:MySQL 专业监控
-
MySQL Enterprise Monitor:官方监控工具
-
阿里云 RDS 监控:云产品自带
43. 什么是分区表?与分库分表有什么区别?
核心答案:
分区表 是 MySQL 在单库单表层面的物理拆分,对应用透明,将一张表的数据按规则分散到多个物理文件中。
详细说明:
分区类型:
sql
-- RANGE 分区
CREATE TABLE orders (
id INT,
create_date DATE
) PARTITION BY RANGE (YEAR(create_date)) (
PARTITION p2022 VALUES LESS THAN (2023),
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025)
);
-- LIST 分区
PARTITION BY LIST (region) (
PARTITION p_east VALUES IN ('北京','上海'),
PARTITION p_west VALUES IN ('四川','重庆')
);
-- HASH 分区
PARTITION BY HASH (id) PARTITIONS 8;
-- KEY 分区
PARTITION BY KEY (user_id) PARTITIONS 8;
分区表 vs 分库分表:
| 对比项 | 分区表 | 分库分表 |
|---|---|---|
| 物理存储 | 同一数据库,不同物理文件 | 不同数据库或不同实例 |
| 对应用透明 | ✅ 完全透明 | ❌ 需要应用感知或中间件 |
| 跨分区查询 | ✅ 支持,优化器自动处理 | ❌ 需要中间件聚合 |
| 事务支持 | ✅ 完整支持 | ❌ 分布式事务复杂 |
| 扩展性 | 有限(单机瓶颈) | 好(可以无限扩展) |
| 维护成本 | 低 | 高 |
| 适用场景 | 数据量大但单机能承载 | 数据量超大,需要水平扩展 |
分区表的限制:
-
分区数最大 8192
-
所有分区必须在同一个 MySQL 实例
-
外键约束不支持分区表
-
分区键必须包含在主键或唯一索引中
44. 什么是 JSON 类型?如何使用?
核心答案:
MySQL 5.7 开始支持 JSON 数据类型,可以存储和查询 JSON 格式的数据,支持 JSON 函数和索引(通过生成列)。
详细说明:
创建 JSON 列:
sql
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
profile JSON
);
-- 插入 JSON 数据
INSERT INTO users VALUES (1, '张三', '{"age": 25, "city": "北京", "tags": ["程序猿", "运动"]}');
JSON 函数:
sql
-- 提取 JSON 字段
SELECT JSON_EXTRACT(profile, '$.age') FROM users;
SELECT profile->'$.age' FROM users; -- 简化写法
SELECT profile->>'$.age' FROM users; -- 返回无引号字符串
-- 判断是否包含
SELECT * FROM users WHERE JSON_CONTAINS(profile->'$.tags', '"程序猿"');
-- 更新 JSON 字段
UPDATE users SET profile = JSON_SET(profile, '$.age', 26) WHERE id = 1;
-- 添加字段
UPDATE users SET profile = JSON_INSERT(profile, '$.email', 'zhangsan@example.com');
-- 删除字段
UPDATE users SET profile = JSON_REMOVE(profile, '$.age');
创建 JSON 索引(通过生成列):
sql
-- 创建生成列
ALTER TABLE users ADD COLUMN age INT GENERATED ALWAYS AS (profile->>'$.age') STORED;
-- 在生成列上建索引
CREATE INDEX idx_age ON users(age);
适用场景:
-
存储非结构化数据
-
字段经常变化,不适合固定表结构
-
配置信息、用户扩展属性等
45. 什么是全文索引?如何使用?
核心答案:
全文索引(Full-Text Index) 用于对文本内容进行关键词搜索,支持自然语言搜索和布尔搜索,比 LIKE 性能更好。
详细说明:
创建全文索引:
sql
自然语言搜索:
-- 建表时创建
CREATE TABLE articles (
id INT PRIMARY KEY,
title VARCHAR(200),
content TEXT,
FULLTEXT(title, content)
) ENGINE=InnoDB;
-- 或单独添加
ALTER TABLE articles ADD FULLTEXT(title, content);
sql
-- 按相关性排序
SELECT *, MATCH(title, content) AGAINST('MySQL 索引') AS score
FROM articles
WHERE MATCH(title, content) AGAINST('MySQL 索引')
ORDER BY score DESC;
布尔搜索:
sql
-- + 必须包含,- 必须不包含,* 通配符
SELECT * FROM articles
WHERE MATCH(content) AGAINST('+MySQL -Oracle 索引*' IN BOOLEAN MODE);
查询扩展:
sql
-- 自动扩展搜索词(基于相关文档)
SELECT * FROM articles
WHERE MATCH(content) AGAINST('MySQL' WITH QUERY EXPANSION);
注意事项:
-
最小搜索长度:InnoDB 默认 3 个字符(
innodb_ft_min_token_size) -
停用词:默认有一些常见词(the、and 等)被忽略
-
中文需要设置 ngram 分词器:
sql
SET GLOBAL innodb_ft_enable_stopword = OFF;
SET GLOBAL ngram_token_size = 2;
46. 什么是数据库中间件?常用的有哪些?
核心答案:
数据库中间件 是位于应用程序和数据库之间的软件层,用于解决分库分表、读写分离、故障切换、数据聚合等问题。
详细说明:
中间件分类:
1. Proxy 模式(代理层)
-
独立的代理服务,对应用透明
-
优点:应用无侵入
-
缺点:增加网络跳转,有一定性能损耗
| 中间件 | 特点 |
|---|---|
| MyCat | 国产,功能完善,社区活跃 |
| ProxySQL | 高性能,支持读写分离、查询缓存 |
| MySQL Router | 官方轻量级路由 |
| Vitess | YouTube 开源,支持大规模集群 |
2. JDBC 模式(应用层)
-
集成在应用中,作为数据源代理
-
优点:性能好,无需额外部署
-
缺点:语言绑定,只支持 Java
| 中间件 | 特点 |
|---|---|
| ShardingSphere-JDBC | Apache 顶级项目,功能强大 |
| TDDL | 阿里开源(淘宝分布式数据层) |
| Zebra | 美团开源 |
ShardingSphere 核心功能:
-
分库分表:支持取模、范围、哈希等多种分片策略
-
读写分离:支持主从自动路由
-
分布式事务:支持 XA 和 BASE
-
数据加密:敏感数据自动加解密
-
影子库:压测数据隔离
47. 如何保证数据库的高可用?
核心答案:
MySQL 高可用方案主要有主从切换、MHA、MGR、InnoDB Cluster、云原生 RDS等。
详细说明:
方案对比:
| 方案 | 故障切换 | 数据一致性 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 主从 + 手动切换 | 手动,分钟级 | 可能丢数据 | 低 | 非核心业务 |
| MHA | 自动,10-30 秒 | 可能丢数据 | 中 | 传统主从架构 |
| MGR | 自动,秒级 | 强一致(组复制) | 高 | MySQL 5.7+ |
| InnoDB Cluster | 自动,秒级 | 强一致 | 中 | MySQL 官方方案 |
| PXC / Galera | 自动,秒级 | 强一致(同步复制) | 高 | 对一致性要求极高 |
| 云 RDS | 自动,秒级 | 高可用(云厂商保证) | 低 | 上云场景 |
MHA(Master High Availability):
-
成熟的主从切换方案
-
监控主库,故障时选举新主库,自动补偿 binlog 差异
-
需要配置 VIP 或 DNS 切换
MGR(MySQL Group Replication):
-
MySQL 5.7 引入,基于 Paxos 协议
-
支持单主和多主模式
-
自动故障切换,数据强一致
InnoDB Cluster:
-
MySQL 官方高可用方案
-
包含 MySQL Shell、MGR、MySQL Router
-
配置简单,官方支持
云 RDS:
-
阿里云 RDS、腾讯云 MySQL、AWS RDS 等
-
自动高可用(主备切换,数据备份)
-
对应用无感知,降低运维成本
48. 如何备份和恢复 MySQL 数据?
核心答案:
MySQL 备份分为逻辑备份 和物理备份 ,结合全量备份 和增量备份实现数据保护。
详细说明:
备份类型:
| 备份方式 | 工具 | 优点 | 缺点 |
|---|---|---|---|
| 逻辑备份 | mysqldump, mydumper | 跨平台,可编辑,备份 SQL | 恢复慢,影响业务 |
| 物理备份 | XtraBackup, MySQL Enterprise Backup | 热备份,恢复快 | 依赖操作系统 |
常用备份工具:
1. mysqldump(逻辑备份)
bash
# 全量备份
mysqldump -u root -p --all-databases > backup.sql
# 备份单库
mysqldump -u root -p --single-transaction --master-data=2 mydb > mydb.sql
# 恢复
mysql -u root -p mydb < mydb.sql
2. Percona XtraBackup(物理热备份)
bash
# 全量备份
xtrabackup --backup --target-dir=/backup/full
# 增量备份
xtrabackup --backup --target-dir=/backup/inc1 --incremental-basedir=/backup/full
# 准备恢复
xtrabackup --prepare --target-dir=/backup/full
xtrabackup --prepare --target-dir=/backup/full --incremental-dir=/backup/inc1
# 恢复
xtrabackup --copy-back --target-dir=/backup/full
备份策略:
-
全量备份:每周一次(业务低峰期)
-
增量备份:每天一次
-
binlog 备份:实时备份,用于 PITR(Point-in-Time Recovery)
恢复验证:
-
定期在测试环境演练恢复流程
-
验证备份文件的完整性
-
记录恢复时间 RTO(恢复时间目标)
49. 如何选择合适的数据类型?
核心答案:
数据类型选择影响存储空间、查询性能、索引效率,需要根据业务场景合理选择。
详细说明:
整型选择:
| 类型 | 字节数 | 范围 | 适用场景 |
|---|---|---|---|
| TINYINT | 1 | -128~127 | 状态码、枚举 |
| SMALLINT | 2 | -32768~32767 | 小范围数字 |
| INT | 4 | -21亿~21亿 | 一般 ID、计数 |
| BIGINT | 8 | 9e18 | 大 ID、时间戳 |
原则:能用小整型不用大整型,节省空间。
字符串选择:
| 类型 | 场景 |
|---|---|
| CHAR | 长度固定:手机号、身份证、MD5 |
| VARCHAR | 长度变化:用户名、地址 |
| TEXT | 长文本:文章内容、备注 |
| BLOB | 二进制:图片、文件(不建议存数据库) |
原则:VARCHAR(255) 是常用值,255 以内长度前缀 1 字节。
时间类型选择:
| 类型 | 字节数 | 范围 | 精度 |
|---|---|---|---|
| DATE | 3 | 1000-01-01 ~ 9999-12-31 | 天 |
| DATETIME | 8 | 1000-01-01 00:00:00 ~ 9999-12-31 23:59:59 | 秒 |
| TIMESTAMP | 4 | 1970-01-01 00:00:01 ~ 2038-01-19 03:14:07 | 秒 |
| TIME | 3 | -838:59:59 ~ 838:59:59 | 秒 |
原则:
-
TIMESTAMP 占 4 字节,但范围有限,2038 年问题
-
DATETIME 占 8 字节,范围更大
-
存储毫秒级时间戳可用 BIGINT
DECIMAL vs FLOAT:
-
DECIMAL:精确计算,适合金额、价格
-
FLOAT/DOUBLE:近似计算,适合科学计算
50. 什么是数据库连接数过高?如何排查?
核心答案:
连接数过高指 MySQL 的并发连接数接近或超过 max_connections,可能导致连接失败、响应变慢、甚至服务不可用。
详细说明:
排查步骤:
1. 查看当前连接数
sql
SHOW STATUS LIKE 'Threads_connected'; -- 当前连接数
SHOW VARIABLES LIKE 'max_connections'; -- 最大连接数
2. 查看连接来源
sql
-- 查看每个客户端 IP 的连接数
SELECT host, COUNT(*) FROM information_schema.processlist GROUP BY host;
-- 查看每个用户的状态
SELECT user, state, COUNT(*) FROM information_schema.processlist GROUP BY user, state;
3. 查看空闲连接
sql
-- 查看长时间空闲的连接
SELECT id, user, host, command, time, state, info
FROM information_schema.processlist
WHERE command = 'Sleep' AND time > 60;
4. 查看慢查询或锁等待
sql
-- 查看正在执行的慢查询
SELECT * FROM information_schema.processlist WHERE time > 10 AND command != 'Sleep';
常见原因及解决方案:
| 原因 | 解决方案 |
|---|---|
| 连接池配置过大 | 调整 maximumPoolSize,不超过数据库最大连接数 |
| 连接泄漏(未归还) | 检查代码,确保 finally 中关闭连接 |
| 慢查询堆积 | 优化 SQL,增加索引 |
| 长事务未提交 | 检查事务,确保及时提交 |
| 突发流量 | 扩容、限流、增加连接数上限 |
紧急处理:
sql
-- 杀掉空闲超时的连接
SELECT CONCAT('KILL ', id, ';') FROM information_schema.processlist
WHERE command = 'Sleep' AND time > 300;
-- 临时增大连接数
SET GLOBAL max_connections = 500;
以上是 50 道 MySQL 高频面试题的详细解答,覆盖了从基础到进阶的各个知识点。建议结合自己的项目经验,理解每个知识点背后的原理,而不是死记硬背。面试时如果能结合实际场景举例,会更加分。祝你面试顺利!