【Java面试】——MySQL
2026/7/26 6:36:17 网站建设 项目流程

好的,我们继续按照原大纲的结构,对数据库(★★★★★)部分进行详细补充。这部分以 MySQL 为核心,涵盖存储引擎、索引、事务隔离、日志体系、锁机制,以及 SQL 优化的完整方法论。


三、数据库(核心能力 20%)

数据库是绝大多数业务系统的最终一致性保障。高级工程师不仅要能写出复杂的 SQL,更要理解数据库内核的工作机制,能够在高并发场景下设计出高性能、高可用的数据存储方案。

3.1 MySQL 体系架构概览

MySQL 的整体架构分为三层:

层级组件职责
客户端层Connectors(JDBC、ODBC、Python 等)建立连接、认证、权限校验
Server 层连接池、SQL 接口、解析器、优化器、缓存(8.0 已移除)SQL 解析、优化、执行计划生成
存储引擎层InnoDB、MyISAM、Memory 等数据的实际存储和读取、事务支持

InnoDB vs MyISAM 核心对比(★★★★★ 必问)

特性InnoDBMyISAM
事务支持✅ ACID
行级锁❌(表级锁)
外键约束
MVCC
聚簇索引
全文索引✅(5.6+)
崩溃恢复✅(Redo Log)
适用场景OLTP 高并发读写读多写少、数据仓库

3.2 索引核心原理(★★★★★)

3.2.1 聚簇索引(Clustered Index)与二级索引(Secondary Index)

聚簇索引(又称主键索引):

  • InnoDB 中,表数据本身就是索引,数据行存储在 B+Tree 的叶子节点中。
  • 如果定义了主键,则主键作为聚簇索引;如果没有主键,则使用第一个唯一非空索引;如果还没有,则 InnoDB 隐式生成 6 字节的ROW_ID作为聚簇索引。
  • 特性:叶子节点存储整行数据(完整记录)。

二级索引(非聚簇索引、辅助索引):

  • 叶子节点存储的是索引列值 + 主键值
  • 查询过程:先扫描二级索引获得主键值,再回表到聚簇索引查询完整行数据。
  • 如果查询列全部在二级索引中,则无需回表,称为覆盖索引(Covering Index)

3.2.2 B+Tree 索引结构深度剖析

MySQL 使用 B+Tree 而非 B-Tree 的原因(★★★★★):

  1. 查询稳定性:所有数据都在叶子节点,每次查询的 IO 次数固定(树高度)。
  2. 范围查询高效:叶子节点通过链表连接,范围查询只需遍历链表。
  3. 更高的扇出(Fan-out):非叶子节点只存索引值,不存数据,页大小固定,可存储更多索引键 → 树更矮 → IO 更少。

B+Tree 层数与数据量估算

  • 假设 B+Tree 页大小 16KB,主键为 bigint(8字节)+ 指针(6字节)= 14字节,一个页可存约 1170 个索引条目。
  • 高度为 3 时:根节点 1 个页,第二层 1170 个页,叶子层 1170 × 1170 = 约 136 万个页。
  • 每个叶子页可存约 16 行数据(按行大小 1KB 估算)→ 总数据量 ≈ 136 万 × 16 ≈2176 万行
  • 结论:3 层 B+Tree 足以支撑千万级数据,索引查询仅需 2~3 次磁盘 IO。

3.2.3 索引失效场景(★★★★★ 高频 SQL 优化考点)

场景原因示例
对索引列使用函数计算破坏索引有序性WHERE DATE(create_time) = '2024-01-01'→ 应改为create_time BETWEEN ... AND ...
隐式类型转换字符串列与数字比较WHERE phone = 13800001111(phone 为 varchar)→ 需要加引号
使用!=<>不等值无法利用索引有序性可能导致全表扫描
使用LIKE '%xxx'通配符在开头无法匹配前缀LIKE 'xxx%'可以走索引
OR 条件中左右列索引不一致MySQL 难以选择最优执行计划可将 OR 拆分为 UNION
联合索引违反最左前缀原则索引多列有序性依赖左列(a, b, c)索引,查询条件只有bc不走索引
范围查询后的列范围查询列后的索引列失效WHERE a > 1 AND b = 2,只能用到 a 的索引

3.2.4 联合索引(Composite Index)最左前缀原则

核心规则:联合索引(a, b, c)实际上等价于三个索引(a)(a, b)(a, b, c)。查询条件必须包含最左列a,索引才能生效。

可走索引的查询条件

  • WHERE a = 1
  • WHERE a = 1 AND b = 2
  • WHERE a = 1 AND b = 2 AND c = 3
  • WHERE a = 1 AND c = 3✅(仅 a 列走索引,c 列无法利用)
  • WHERE a > 1 AND b = 2✅(a 走范围,b 失效)

不走索引的条件

  • WHERE b = 2
  • WHERE c = 3
  • WHERE b = 2 AND c = 3

索引下推(ICP,Index Condition Pushdown)

  • MySQL 5.6+ 引入。在没有 ICP 之前,即使索引部分生效,也要回表取完整行再判断其他条件。
  • 启用 ICP 后,可以在索引层面直接过滤掉不满足条件的记录,减少回表次数。
  • 例如WHERE a > 1 AND b = 2,使用 ICP 时,在遍历索引(a)时直接判断 b=2,不满足则跳过,无需回表。

3.2.5 MRR(Multi-Range Read)优化

  • 针对范围查询 + 回表的场景。传统做法:按索引顺序扫描,每次回表随机读取磁盘(随机 IO)。
  • MRR 优化:将查到的行主键先排序(放入 read_rnd_buffer),再按主键顺序回表读取,将随机 IO 转为顺序 IO。
  • 生效条件:mrr=onmrr_cost_based=off强制开启。

3.3 事务隔离级别与 MVCC(★★★★★)

3.3.1 SQL 标准定义的四种隔离级别

隔离级别脏读(Dirty Read)不可重复读(Non-Repeatable Read)幻读(Phantom Read)
READ UNCOMMITTED✅ 可能✅ 可能✅ 可能
READ COMMITTED✅ 可能✅ 可能
REPEATABLE READ(MySQL 默认)❌(MVCC 保证)❌(MVCC + 间隙锁保证)
SERIALIZABLE❌(读加锁)

3.3.2 MVCC(Multi-Version Concurrency Control)—— 多版本并发控制

MVCC 是 InnoDB 实现 RC 和 RR 隔离级别的核心技术,读写不互斥,极大提升了并发性能。

三个核心组件

  1. 隐藏列(Hidden Columns)

    • DB_TRX_ID:创建或最后一次修改该行的事务 ID
    • DB_ROLL_PTR:回滚指针,指向 Undo Log 中该行的旧版本记录
    • DB_ROW_ID:当表没有主键时,InnoDB 用该列作为聚簇索引。
  2. Undo Log(回滚日志)

    • 记录数据行的历史版本(类似于 Git 的提交历史)。
    • 通过DB_ROLL_PTR串联成版本链。
  3. Read View(读视图)

    • 是什么:当前事务可见的数据快照。包含四个重要属性:
      • creator_trx_id:当前事务 ID。
      • up_limit_id:当前活跃的最小事务 ID(即低水位)。
      • low_limit_id:当前未分配的最小事务 ID(即高水位,大于等于它的事务都不可见)。
      • trx_ids:当前活跃事务 ID 列表。
    • 可见性规则(RC vs RR 的核心差异):
      • RC 级别:每次执行 SELECT 语句时重新生成Read View。
      • RR 级别:事务内第一次执行 SELECT 时生成Read View,整个事务期间复用,不会更新。

3.3.3 RR 级别下如何避免幻读?

  • 快照读(Snapshot Read)SELECT ...查询走 MVCC 版本链,无论其他事务插入了多少新行,当前事务看到的仍然是创建 Read View 时的数据快照,因此幻读被 MVCC 天然屏蔽。
  • 当前读(Current Read)SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODEINSERTUPDATEDELETE等操作读取的是最新版本。针对当前读的幻读问题,InnoDB 引入Next-Key Lock(行锁 + 间隙锁的组合)来锁定范围。

3.3.4 为什么 RR 可以避免幻读?(总结)

MVCC(快照读)解决了快照读的幻读,Next-Key Lock(当前读)解决了当前读的幻读。两者结合使得 MySQL InnoDB 在 RR 级别下完美避免了幻读。

3.4 InnoDB 事务日志体系(★★★★★)

3.4.1 Redo Log(重做日志)—— 保证持久性(Durability)

  • 作用:数据库崩溃恢复时,重放 Redo Log,确保已提交事务的修改不丢失(WAL 技术——Write-Ahead Logging,先写日志,后写磁盘)。
  • 存储:物理日志,循环写入固定大小的ib_logfile0ib_logfile1文件(两个文件轮换使用,大小由innodb_log_file_size控制)。
  • 刷盘策略innodb_flush_log_at_trx_commit参数控制:
    • 0:每秒刷盘(最高性能,可能丢失 1 秒数据)
    • 1(默认):每次事务提交刷盘(最安全,性能最低)
    • 2:每次提交写入 OS Cache,每秒刷盘(性能和安全的折中)

3.4.2 Undo Log(回滚日志)—— 保证原子性(Atomicity)

  • 作用:事务回滚时将数据恢复到修改前的状态。
  • 存储:逻辑日志,存储在 Undo Tablespace(undo_001undo_002),记录的是如何逆操作(INSERT → DELETE,DELETE → INSERT,UPDATE → 旧值)。
  • 清理:Purge 线程异步清理不再需要的 Undo Log(当没有事务需要访问旧版本数据时)。

3.4.3 Binlog(归档日志)—— 保证主从复制与数据恢复

  • 作用:记录所有逻辑修改操作(DDL + DML),用于主从复制、数据备份恢复、数据审计。
  • 格式STATEMENT(记录 SQL 原文)、ROW(记录行级别变化,推荐)、MIXED(混合模式)。
  • 与 Redo Log 的区别
特性Redo LogBinlog
存储引擎InnoDB 特有MySQL Server 层通用
日志内容物理日志(页级别修改)逻辑日志(SQL 或行变化)
写入时机事务执行过程中不断写入事务提交时写入
空间管理循环覆盖,固定大小追加写入,可设置过期时间
主要用途崩溃恢复(Crash-safe)主从复制、数据恢复

3.4.4 两阶段提交(2PC)—— 保证 Redo Log 和 Binlog 一致性

问题:MySQL 中 Redo Log 和 Binlog 是两套独立的日志系统,若无协调机制,在数据库崩溃时可能出现“Redo Log 有数据但 Binlog 没记录”或相反的情况,导致主从数据不一致。

解决方案两阶段提交(2PC),由XA RECOVER协调:

  1. Prepare 阶段:写 Redo Log,状态为PREPARE,刷盘。
  2. Commit 阶段:写 Binlog,刷盘。Binlog 写入成功后,将 Redo Log 状态改为COMMIT,事务正式完成。

崩溃恢复规则

  • 如果 Redo Log 处于PREPARE状态且 Binlog 完整(已写入),则提交该事务。
  • 如果 Redo Log 处于PREPARE状态但 Binlog 未写入,则回滚该事务。
  • 如果 Redo Log 已COMMIT,则直接提交。

3.5 InnoDB Buffer Pool —— 缓冲池

  • 作用:缓存磁盘数据页到内存,减少磁盘 IO,是 MySQL 性能优化的核心之一。
  • 结构:LRU 链表 + Flush 链表 + Free 链表。
  • LRU 优化
    • 采用冷热分离的 LRU(old区占 3/8,young区占 5/8)。
    • 新数据页先放入old区头部。如果数据在old区停留超过innodb_old_blocks_time(默认 1000ms),再次访问时才提升到young区。
    • 目的:防止全表扫描将热点数据挤出 LRU(预读 + 全表扫描污染问题)。
  • 预读机制(Read-Ahead)
    • 线性预读:顺序读取一个区的多个页后,异步预读后续页。
    • 随机预读:不连续页访问时预读(5.5 后默认关闭)。

3.6 Change Buffer(写缓冲区,原 Insert Buffer)

  • 作用:缓存二级索引的修改操作(INSERT、UPDATE、DELETE),减少随机磁盘 IO。MySQL 5.5 中仅支持 INSERT,5.6+ 扩展为支持 UPDATE/DELETE,因此改名为 Change Buffer。
  • 工作原理:当修改二级索引页时,如果该页不在 Buffer Pool 中,不立即读取磁盘页,而是将修改记录在 Change Buffer 中。后续该页被读入 Buffer Pool 时,再合并(Merge)这些修改。
  • 适用条件非唯一二级索引(唯一索引需要立即检查唯一性约束,无法延迟)。

3.7 Double Write(双写)—— 解决页断裂(Partial Write)

  • 问题:InnoDB 页大小 16KB,操作系统每次写 4KB,若写入 4KB 时系统崩溃,会导致数据页损坏(Partial Write)。
  • 解决方案
    • 先将脏页复制到内存中的Double Write Buffer(2MB)
    • 将 Double Write Buffer 顺序写入磁盘的共享表空间(ibdata1)中(双写区域)。
    • 然后再将脏页写入实际数据文件。
  • 恢复:如果数据页损坏,从双写区域拷贝备份进行修复。
  • 注意:Double Write 对性能有约 5%-10% 的影响,但为了保证数据完整性,建议保持开启(5.6+ 默认开启,不能关闭)。

3.8 InnoDB 锁机制(★★★★★)

3.8.1 锁粒度

锁类型说明
全局锁(Global Lock)FLUSH TABLES WITH READ LOCK,整个库只读,用于备份
表级锁(Table Lock)LOCK TABLES t READ/WRITE,MyISAM 默认,InnoDB 也可用
行级锁(Row Lock)InnoDB 默认,锁定单行记录,并发性能最高
间隙锁(Gap Lock)锁定索引记录之间的间隙,防止幻读(RR 级别默认)
Next-Key Lock行锁 + 间隙锁的组合,锁定记录本身及其前后的间隙
意向锁(Intention Lock)表级锁,表示事务想要在行上加共享锁/排他锁,用于避免 DDL 与行锁冲突

3.8.2 共享锁(S) vs 排他锁(X)

  • 共享锁(S)SELECT ... LOCK IN SHARE MODE,允许其他事务读取,但不能修改。
  • 排他锁(X)SELECT ... FOR UPDATEINSERTUPDATEDELETE自动加 X 锁,其他事务不能读不能写。
  • 兼容矩阵
XS
X❌ 冲突❌ 冲突
S❌ 冲突✅ 兼容

3.8.3 Next-Key Lock 与 Gap Lock 深入分析

  • Gap Lock 出现时机(RR 级别 + 当前读)
    • 查询条件命中范围或未命中记录时,Gap Lock 锁定不存在记录的间隙。
    • 示例:表有 id = 5, 10, 15。
      • SELECT * FROM t WHERE id > 6 FOR UPDATE会在 (5, 10)、(10, 15)、(15, +∞) 三个区间加 Gap Lock,阻止其他事务插入 id = 7、9、12、20 等记录。
  • Next-Key Lock 的锁定区间
    • 对于id = 10查询,Next-Key Lock 锁定区间为 (5, 10] + (10, 15] 即 (5, 15)。
  • RC 级别不使用 Gap Lock,这是 RC 级别无法避免幻读的根本原因。

3.8.4 死锁(Deadlock)

死锁的四个必要条件(与 OS 一致):

  1. 互斥条件(Mutual Exclusion)
  2. 持有并等待(Hold and Wait)
  3. 不可抢占(No Preemption)
  4. 循环等待(Circular Wait)

MySQL 死锁的典型场景

  • 并发加锁顺序不一致:事务 A 先锁 table1 再锁 table2,事务 B 先锁 table2 再锁 table1。
  • 唯一键冲突:两个并发插入相同唯一键,先到的持有行锁,后到的检测到重复键,尝试加 S 锁,但 X 锁已被持有 → 死锁。
  • 批量更新范围重叠:两个事务使用不同的条件更新同一范围的数据。

线上死锁排查流程(★★★★★ 实战)

  1. SHOW ENGINE INNODB STATUS查看最新的死锁信息(LATEST DETECTED DEADLOCK部分)。
  2. 开启死锁日志(innodb_print_all_deadlocks=ON),将死锁记录到 MySQL error log。
  3. 分析死锁图中的事务 SQL 和持有的锁类型(X/S 锁 + 间隙锁)。
  4. 定位代码:找到对应的 SQL 语句,分析加锁顺序。
  5. 解决方案
    • 统一加锁顺序(所有事务按相同顺序访问表/索引)。
    • 将 RR 隔离级别降为 RC(RC 无 Gap Lock,死锁概率大幅降低,但需确认业务可接受)。
    • 使用INSERT ... ON DUPLICATE KEY UPDATE减少唯一键冲突死锁。
    • 缩短事务(减少锁持有时间),拆分大事务。
    • 对热点行加队列(排队机制),避免并发冲突。

3.9 SQL 优化与执行计划分析

3.9.1 Explain 输出解读(★★★★★ 必须熟练)

以 MySQL 8.0 的 Explain 输出为例,重点关注的列:

列名核心含义重点关注值
id执行顺序,越大越先执行子查询 > 外层
select_type查询类型SIMPLE(最好)、PRIMARYSUBQUERYDERIVED(派生表)、UNION
table表名或别名-
type最重要访问类型,性能从优到差system>const>eq_ref>ref>range>index>ALL目标:至少达到 range 级别,最好是 ref 或 eq_refALL(全表扫描)通常需要优化
possible_keys可能用到的索引-
key实际使用的索引如果为 NULL,说明未走索引
key_len使用的索引字节长度可用于判断联合索引使用了多少列
ref索引列与哪个值进行比较常量(const)或关联列
rows预估扫描行数越小越好
filtered存储引擎层过滤后剩余百分比越高越好(100% 最优)
Extra关键信息额外信息重点关注:Using index(覆盖索引,好)、Using index condition(ICP,好)、Using where(Server 层过滤)、Using filesort(文件排序,性能差)、Using temporary(临时表,性能差)、Using join buffer(没走索引,用了 Join Buffer)

3.9.2 索引优化策略(实战)

假设有一张 5000 万行的订单表order,查询 SQL:

SELECTorder_id,user_id,amount,status,create_timeFROMorderWHEREuser_id=12345ANDstatus=1ANDcreate_timeBETWEEN'2024-01-01'AND'2024-12-31'ORDERBYcreate_timeDESCLIMIT20;

执行 30 秒,需要优化。

优化思路

  1. 分析执行计划(Explain):

    • 如果type=ALL,说明需要加索引。
    • 如果Extra=Using filesort,说明排序未走索引。
  2. 创建联合索引(遵循最左前缀原则):

    • 条件中user_id是等值查询,status是等值查询,create_time是范围查询 + 排序字段。
    • 推荐索引(user_id, status, create_time)
    • 索引排序原因:创建时间同时满足范围过滤和 ORDER BY 排序,可以避免filesort
  3. 覆盖索引(如果可能,进一步优化):

    • 当前查询 SELECT 包含了order_id(主键)、user_idamountstatuscreate_time
    • 如果amount不在索引中,则回表取amount。如果业务允许,可以把amount也加入索引(但索引列太多会导致写入性能下降)。
    • 使用覆盖索引 + 延迟关联技巧:
      SELECTo.order_id,o.user_id,o.amount,o.status,o.create_timeFROM(SELECTorder_idFROMorderWHEREuser_id=12345ANDstatus=1ANDcreate_timeBETWEEN'2024-01-01'AND'2024-12-31'ORDERBYcreate_timeDESCLIMIT20)tmpJOINorderoONtmp.order_id=o.order_id;
      • 子查询中只查主键,可完全用覆盖索引完成(二级索引包含主键)。
      • 外层用主键回表取全部列,但只取 20 行,回表成本可控。
  4. 索引下推(ICP)确保开启:

    • 确保optimizer_switch="index_condition_pushdown=on"

3.9.3 Join 优化 —— Join Buffer 与 Hash Join

  • Nested Loop Join(嵌套循环连接):驱动表每条记录遍历被驱动表。如果被驱动表没有索引,复杂度 O(N × M),性能极差。
  • Join Buffer(连接缓冲区):当被驱动表无法走索引时,MySQL 将驱动表的 join 列放入内存(Join Buffer),减少被驱动表的扫描次数。大小由join_buffer_size控制。
  • Block Nested Loop(BNL):使用 Join Buffer 分批加载驱动表,批量匹配被驱动表。
  • Hash Join(MySQL 8.0.18+ 引入)
    • 将驱动表构建为哈希表(在内存中),然后遍历被驱动表,用哈希探测匹配。
    • 适合等值连接大表无索引的场景。
    • 比 BNL 更高效(时间复杂度 O(N+M))。

优化原则:Join 查询中,始终让小表作为驱动表(被 EXPLAIN 中的第一张表),且确保被驱动表的 join 列上有索引。如果无法加索引,尝试升级到 MySQL 8.0 使用 Hash Join。

3.9.4 分页查询优化(★★★★★ 百万级分页)

问题 SQL

SELECT*FROMordersORDERBYidLIMIT1000000,20;

MySQL 需要扫描 1000020 行,丢弃前 1000000 行,性能极差。

三种优化方案

  1. 延迟关联(覆盖索引 + 回表)

    SELECTo.*FROMorders oJOIN(SELECTidFROMordersORDERBYidLIMIT1000000,20)tmpONo.id=tmp.id;
    • 子查询走覆盖索引(二级索引),避免回表扫描 100 万行。
  2. 游标分页(记住最后一条 ID)

    SELECT*FROMordersWHEREid>{last_id}ORDERBYidLIMIT20;
    • 适用于顺序翻页场景(如 App 无限滚动),不走大 Offset,性能恒定。
  3. 业务场景妥协:如果不可避免需要精确跳转,可考虑将数据导入 Elasticsearch 或使用搜索引擎解决。


本篇内容覆盖了原大纲中数据库(MySQL)的全量核心知识点,包括存储引擎、索引结构、事务隔离与 MVCC、日志体系、Buffer Pool、锁机制以及 SQL 优化方法论。下一篇我们将继续补充 Redis 和 MQ(消息队列)部分。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询