Article
数据库与存储引擎 —— 后端核心竞争力
2026-10-18
10 分钟阅读
Java 后端学习路线 · 17 篇
原则:不只会写 SQL,还要会优化。 MySQL 索引原理 + Redis 缓存策略 + 消息队列异步解耦 = 高并发系统的三条腿。
一、MySQL 深度优化(★★★★★)
1.1 B+ 树索引原理
复制 为什么 MySQL 用 B+ 树而不用 B 树?
B+ 树优势:
1. 数据全部存在叶子节点,非叶子节点只存 key → 非叶子节点可以存更多索引,树更矮
2. 叶子节点之间通过双向链表连接 → 范围查询只需遍历链表,不需要回溯
3. 查询稳定性:所有查询都走到叶子节点,IO 次数固定
B 树缺点:
1. 数据分布在所有节点 → 非叶子节点能存的索引少,树更高
2. 范围查询需要中序遍历 → 效率低
3. 查询不稳定:可能在根节点就命中,也可能到叶子节点
InnoDB 页大小:默认 16KB
假设一行数据 1KB,key 占 8 字节,指针占 6 字节:
- 非叶子节点:16KB / (8+6)B ≈ 1170 个索引项
- 叶子节点:16KB / 1KB ≈ 16 行数据
- 3 层 B+ 树 ≈ 1170² × 16 ≈ 2000 万行
- 所以 2000 万数据,主键查询只需 3 次 IO
1.2 聚簇索引 vs 非聚簇索引
对比维度 聚簇索引(主键索引) 非聚簇索引(二级索引) 叶子节点存储 完整行数据 主键 ID(需要回表) 数量限制 1 个(只有一个物理排序) 多个 查找流程 走 B+ 树 → 叶子节点直接拿到数据 走 B+ 树 → 叶子节点拿到主键 → 回表查询聚簇索引 插入性能 可能页分裂(主键不是自增时) 较快
覆盖索引 :查询列都在索引中,不需要回表,Extra 列显示 Using index。
1.3 联合索引与最左匹配原则
复制 索引:INDEX(a, b, c)
能使用索引的查询:
WHERE a = 1 ✓ 使用 a
WHERE a = 1 AND b = 2 ✓ 使用 a, b
WHERE a = 1 AND b = 2 AND c = 3 ✓ 使用 a, b, c
WHERE a = 1 AND c = 3 ✓ 使用 a(c 不走索引,因为跳过了 b)
WHERE b = 2 ✗ 全表扫描(跳过了 a)
索引失效场景(面试重点):
1. 对索引列使用函数:WHERE LEFT(name, 3) = 'abc' → 失效
2. 隐式类型转换:WHERE phone = 13800138000 → phone 是 varchar,失效
3. LIKE 前缀模糊:WHERE name LIKE '%abc' → 失效('abc%' 有效)
4. OR 条件包含非索引列:WHERE a=1 OR b=2 → 如果 b 无索引则全表扫描
5. 联合索引不满足最左匹配
6. 范围查询右边的列失效:WHERE a=1 AND b>10 AND c=3 → c 不走索引
7. != / <> / NOT IN:可能导致索引失效(优化器可能选择全表扫描)
8. IS NULL / IS NOT NULL:取决于数据分布
1.4 MVCC 多版本并发控制
复制 核心组成:
- 隐藏列:DB_TRX_ID(事务 ID)、DB_ROLL_PTR(回滚指针)、DB_ROW_ID
- Undo Log:记录数据的历史版本,形成版本链
- Read View:快照读时判断哪些版本可见
Read View 可见性判断:
1. 版本的事务 ID = 当前事务 ID → 可见(自己的修改)
2. 版本的事务 ID < min_trx_id → 可见(已提交的老事务)
3. 版本的事务 ID > max_trx_id → 不可见(未开始的事务)
4. min_trx_id ≤ 版本的事务 ID ≤ max_trx_id → 看是否在活跃列表中
- 在活跃列表中 → 不可见(未提交)
- 不在活跃列表中 → 可见(已提交)
RC(读已提交):每次 SELECT 生成新的 Read View
RR(可重复读):事务第一次 SELECT 生成 Read View,后续复用
1.5 MySQL 锁机制
锁类型 粒度 说明 全局锁 整个数据库 FLUSH TABLES WITH READ LOCK,全库只读表锁 表级别 LOCK TABLE t1 READ,MyISAM 默认行锁 行级别 InnoDB 通过索引实现,锁的是索引记录 间隙锁 间隙 锁定索引记录之间的间隙(RR 级别下防幻读) 临键锁 行 + 间隙 行锁 + 前间隙的合体(InnoDB RR 级别默认) 意向锁 表级别 IS/IX,表锁和行锁的桥梁
死锁排查 :SHOW ENGINE INNODB STATUS 查看 LATEST DETECTED DEADLOCK 部分。
1.6 EXPLAIN 执行计划解读
复制 EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 'PAID';
字段 含义 关注值 type 访问类型(越靠左越好) system > const > eq_ref > ref > range > index > ALL key 实际使用的索引 看是否命中期盼的索引 rows 扫描行数(估算) 越小越好 Extra 额外信息 Using index(覆盖索引,最好)/ Using filesort(需排序优化)/ Using temporary(需临时表优化)
1.7 大表优化方案
复制 单表 > 1000 万行的发展路径:
1. 索引优化 → 覆盖索引 + 联合索引优化
2. SQL 优化 → 避免 SELECT *,分页优化(延迟关联)
3. 缓存 → Redis 热数据缓存
4. 读写分离 → 主库写 + 从库读
5. 分库分表 → ShardingSphere(水平拆分:按 user_id 取模)
6. 冷热分离 → 历史数据归档到 ES/Hive
二、Redis 缓存(★★★★★)
2.1 5 大数据结构底层实现与场景
结构 底层实现 使用场景 String SDS(Simple Dynamic String)/ int 分布式锁、计数器、Session 共享 Hash ziplist → hashtable 存储对象(用户信息、购物车) List quicklist(ziplist 的双向链表) 消息队列、最新列表(LPUSH + LTRIM) Set intset → hashtable 标签、共同好友(SINTER)、抽奖(SRANDMEMBER) ZSet ziplist → skiplist + dict 排行榜(ZADD + ZRANGE)、延迟任务
2.2 缓存三大问题及解决方案
复制 ┌──────────┬────────────────────┬─────────────────────────────┐
│ 问题 │ 描述 │ 解决方案 │
├──────────┼────────────────────┼─────────────────────────────┤
│ 缓存穿透 │ 查不存在的数据,每次│ 1. 布隆过滤器(前置过滤) │
│ │ 都穿透到数据库 │ 2. 缓存空值(TTL 短,如 5 分钟) │
├──────────┼────────────────────┼─────────────────────────────┤
│ 缓存击穿 │ 热点 key 过期瞬间, │ 1. 互斥锁(SETNX 抢锁后查 DB) │
│ │ 大量请求打到数据库 │ 2. 逻辑过期(永不过期 + 异步更新)│
├──────────┼────────────────────┼─────────────────────────────┤
│ 缓存雪崩 │ 大量 key 同时过期, │ 1. 过期时间 + 随机值(打散) │
│ │ 数据库瞬间压力过大 │ 2. 多级缓存(本地 + Redis) │
│ │ │ 3. 限流熔断兜底 │
└──────────┴────────────────────┴─────────────────────────────┘
2.3 分布式锁实现
复制 方案演进:
1. SETNX + Expire → ✗ 非原子操作,中间崩溃死锁
2. SET key value NX EX seconds → ✓ 原子操作(Redis 2.6.12+)
3. Lua 脚本释放(先 GET 判断再 DEL)→ ✓ 保证释放的是自己的锁
4. Redisson(WatchDog 自动续期)→ ✓ 生产环境首选
Redisson WatchDog 原理:
- 加锁成功 → 启动定时任务,每 10s 续期到 30s
- 业务执行完 → 释放锁,取消定时任务
- 业务崩溃 → 30s 后锁自动过期(不会永久死锁)
Redlock 算法(多节点 Redis 集群):
1. 依次向 N 个 Redis 节点请求加锁
2. 过半节点加锁成功且总耗时 < 锁过期时间 → 加锁成功
3. 否则向所有节点释放锁
2.4 缓存与数据库双写一致性
复制 方案对比:
1. 先更新 DB,再删除缓存 → Cache Aside(最常用)
- 缺点:删除缓存失败导致脏数据
- 补偿:MQ 重试 + 订阅 binlog(Canal)
2. 先删除缓存,再更新 DB → ✗ 有并发问题
- 问题:删除缓存后、更新 DB 前,另一线程读到旧数据回种缓存
3. 先更新 DB,再更新缓存 → ✗ 并发更新导致数据不一致
生产方案:Cache Aside + Canal binlog 异步更新
更新 DB → Canal 监听 binlog → 异步删除/更新缓存 → 失败重试
2.5 Redis 为什么快?
复制 1. 纯内存操作:数据存在内存中,无磁盘 IO
2. 单线程模型:无需上下文切换 + 无需考虑并发锁
3. IO 多路复用:一个线程通过 epoll 处理多个客户端连接
4. 高效数据结构:SDS(O(1)获取长度)、跳表(O(logN)查找)、压缩列表(省内存)
5. RESP 协议简单:解析开销小
注意:Redis 6.0+ 引入多线程处理网络 IO,
但命令执行仍是单线程(保证原子性)
三、消息队列(★★★★☆)
3.1 三大 MQ 对比
对比维度 RabbitMQ Kafka RocketMQ 语言 Erlang Java/Scala Java 吞吐量 万级 百万级 十万级 延迟 微秒级 毫秒级 毫秒级 消息可靠性 高(confirm + ack) 可配置 高(同步刷盘) 事务消息 ✗ ✗ ✓(半消息机制) 适用场景 实时性要求高 大数据流处理 电商/金融 国内大厂 中小企业 大数据团队 阿里/美团/滴滴
3.2 消息不丢失方案
复制 生产端 → MQ Broker → 消费端 三阶段保证:
1. 生产端:confirm 机制
RabbitMQ:publisher confirm(异步确认)
Kafka:acks=all(所有副本写入才算成功)
RocketMQ:同步刷盘 + 同步复制
2. Broker 端:持久化
RabbitMQ:消息持久化 + 队列持久化
Kafka:日志文件 + 多副本
RocketMQ:CommitLog + ConsumeQueue
3. 消费端:手动确认
业务处理成功 → 手动 ack
业务处理失败 → nack + 重试 / 死信队列
3.3 消息幂等性保证
复制 为什么需要幂等:消费者重试可能导致重复消费
方案:
1. 唯一 ID 去重:每条消息生成唯一 ID,Redis SET 记录已消费 ID
2. 数据库唯一约束:INSERT 时依赖唯一索引防重
3. 前置状态判断:UPDATE 时加版本号或状态机条件
UPDATE order SET status='PAID' WHERE id=123 AND status='UNPAID'
4. Token 机制:消费前获取 Token,消费时校验 Token
3.4 Kafka 高性能原理
复制 1. 顺序写磁盘:比随机写快 3 个数量级(接近内存速度)
2. Page Cache:利用操作系统页缓存,读写都在内存
3. 零拷贝:sendfile() 系统调用,数据不经过用户空间
4. 批量发送:Producer 攒一批数据一起发(batch.size + linger.ms)
5. 压缩传输:支持 Gzip/Snappy/LZ4 压缩,减少网络传输
6. 分区并行:一个 Topic 多 Partition,消费者并行消费
面试自查清单
复制 □ B+ 树和 B 树的 3 个关键区别能说清
□ 聚簇索引和非聚簇索引的回表机制理解
□ 联合索引最左匹配 + 8 种索引失效场景能列举
□ MVCC Read View 的可见性判断流程能画出来
□ EXPLAIN 执行计划 type 列从高到低排序
□ 缓存穿透/击穿/雪崩各能说出 2 种方案
□ 分布式锁从 SETNX 到 Redisson 的演进过程
□ Cache Aside 模式 + 双写一致性问题
□ 消息不丢失的三阶段保证
□ Kafka 高性能的 5 个核心原因