ARTICLE

Redis 与 MySQL 数据库面试题整理:缓存、索引与事务

Redis 和 MySQL 经常一起出现在后端面试中:MySQL 负责可靠持久化和复杂查询,Redis 负责缓存、计数、排序、限流等高频访问场景。面试官通常会从单个知识点继续追问到缓存一致性、并发控制和故障恢复。本文将两者分开讲解,再通过综合场景串联起来。

一、Redis

1. Redis 为什么性能高?

Redis 的数据主要保存在内存中,命令执行路径短,数据结构实现紧凑,并通过事件循环处理网络请求。它的性能优势并不意味着所有操作都是常数时间:范围查询、集合运算和大 Key 操作仍可能阻塞事件循环。

因此,使用 Redis 时应关注命令复杂度、单次返回数据量、Key 的大小以及慢命令,而不能只看“Redis 是内存数据库”这一点。

2. Redis 常见数据类型如何选择?

  • String:缓存对象、计数器、分布式锁标记和简单字符串。
  • Hash:保存对象的多个字段,需要按字段读取或更新时使用。
  • List:队列、时间线等具有顺序的数据。
  • Set:去重、共同关注、标签集合和交集并集运算。
  • ZSet:排行榜、带分值的优先级集合和延迟任务索引。
  • Bitmap:签到、在线状态等布尔值集合。
  • HyperLogLog:允许一定误差的基数统计。
  • Stream:带消费组能力的消息流。

数据结构的选择应围绕访问模式,而不是为了使用更多类型。对于复杂对象,也要考虑序列化格式、字段更新频率和缓存失效策略。

3. Redis 的过期删除策略是什么?

Redis 通常采用惰性删除和定期删除相结合的方式:

  • 惰性删除:访问 Key 时检查是否过期,过期后再删除。
  • 定期删除:周期性抽查设置了过期时间的 Key,删除其中已经过期的部分。

这种方式在 CPU 和内存之间进行平衡。过期时间到了并不代表 Key 会在同一时刻被立即删除,因此不能把过期时间当作精确的定时任务机制。

4. Redis 的内存淘汰策略有哪些?

当 Redis 达到 maxmemory 限制时,会根据 maxmemory-policy 处理新写入或淘汰已有 Key。常见策略包括:

  • noeviction:不淘汰,写入可能返回 OOM 错误。
  • allkeys-lru:从所有 Key 中淘汰较少使用的 Key。
  • allkeys-lfu:从所有 Key 中淘汰访问频次较低的 Key。
  • volatile-lru:只在设置了过期时间的 Key 中执行 LRU。
  • volatile-lfu:只在设置了过期时间的 Key 中执行 LFU。
  • volatile-ttl:优先淘汰剩余时间较短的 Key。
  • allkeys-random、volatile-random:随机淘汰。

如果选择 volatile-* 策略,却没有足够多的过期 Key,最终仍可能无法释放出足够内存。因此,策略选择要和 Key 的生命周期设计一起考虑。

5. RDB 和 AOF 有什么区别?

RDB 是在某个时间点生成数据快照,文件紧凑,恢复速度通常较快,但可能丢失上一次快照之后的部分数据。AOF 记录写命令,持久化粒度更细,数据安全性通常更好,但文件可能更大,需要定期重写。

两者可以同时开启。选择持久化策略时,要结合数据可恢复性要求、磁盘性能、备份方案和重启恢复时间,而不是简单认为 AOF 一定优于 RDB。

6. Redis 主从复制和 Sentinel 解决什么问题?

主从复制把主节点的数据同步到从节点,可以用于读扩展和故障切换基础。Sentinel 负责监控节点状态、判断主节点下线并在满足条件时完成故障转移,同时向客户端提供新的主节点信息。

复制通常是异步的,因此主从之间可能存在延迟。对于刚写入后立即读取的业务,需要明确是否允许读到旧数据,必要时采用读主库、等待复制或业务补偿等方案。

7. Redis Cluster 如何实现分片?

Redis Cluster 将 Key 映射到固定数量的哈希槽,再由不同节点负责不同槽位。节点可以扩容、缩容并迁移槽位,从而实现数据分布和一定程度的高可用。

涉及多个 Key 的操作只有在这些 Key 位于同一个槽位时才容易执行。可以使用 Hash Tag,让相关 Key 的一部分使用相同的标签,例如:

1
2
user:{1001}:profile
user:{1001}:orders

但 Hash Tag 过度集中会造成数据倾斜,使用前需要评估访问热点和容量分布。

二、Redis 缓存问题

1. 什么是缓存穿透、击穿和雪崩?

  • 缓存穿透:查询一个缓存和数据库都不存在的数据,请求持续打到数据库。
  • 缓存击穿:某个热点 Key 在失效瞬间,大量请求同时访问数据库。
  • 缓存雪崩:大量 Key 同时失效,或 Redis 整体不可用,导致请求集中访问数据库。

对应的治理方式包括:

  • 穿透:缓存空值、参数校验、布隆过滤器。
  • 击穿:互斥锁、逻辑过期、热点数据不过期并异步刷新。
  • 雪崩:过期时间增加随机抖动、多级缓存、限流降级和高可用部署。

2. 如何保证缓存和数据库的一致性?

常见的更新流程是先更新数据库,再删除缓存。删除缓存失败时,可以通过重试队列、消息补偿或定时对账处理。对于并发更新,还需要考虑旧数据回写缓存的问题,必要时引入版本号或串行化更新。

延迟双删可以降低并发读写造成旧数据回填的概率,但它不是严格一致性方案,且会增加请求延迟。对一致性要求极高的场景,应通过消息、事务日志、订阅变更事件或直接绕过缓存来设计。

3. 什么是大 Key 和热 Key?

大 Key 是单个 Key 对应的数据量过大,可能导致网络传输慢、删除阻塞和内存分配压力。热 Key 是访问集中在少数 Key 上,可能造成单节点 CPU、带宽或网络连接成为瓶颈。

治理方式包括拆分大集合、分批读取和删除、限制单次返回数量、热点 Key 多副本或本地缓存、请求合并以及限流。处理前应通过监控或扫描工具确认问题 Key,避免盲目操作生产数据。

三、Redis 高级应用

1. Redis 分布式锁有哪些注意事项?

基本实现通常使用带唯一值的 SET key value NX PX milliseconds,释放锁时必须确认 Value 仍属于当前持有者,再删除 Key,不能直接执行 DEL。

锁的过期时间需要覆盖业务执行时间,但不能无限延长。复杂场景还要考虑客户端暂停、网络分区、锁续期、时钟和业务幂等。分布式锁只能降低并发冲突,不能替代数据库约束和业务状态校验。

2. Redis 如何实现排行榜?

可以使用 ZSet 保存成员和分值,通过 ZADD 更新分数,通过范围查询获取排名。需要注意分数相同时的排序规则,并对分页读取、过期榜单和大量成员更新进行限制。

3. Redis 如何实现限流?

可以使用固定窗口计数、滑动窗口、令牌桶或漏桶等算法。简单场景可以通过 INCR 配合过期时间实现固定窗口;需要严格控制速率时,可以使用 Lua 脚本将多条命令组合为原子操作,或使用专门的限流组件。

四、MySQL 基础与索引

1. InnoDB 和 MyISAM 有什么区别?

InnoDB 支持事务、行级锁、崩溃恢复和外键等能力,是现代业务系统更常用的存储引擎。MyISAM 结构简单、读场景曾经具有一定优势,但不支持事务和行级锁,写并发场景下限制较多。

选择存储引擎时要结合事务、并发、恢复和运维要求,而不是只比较某个简单查询的速度。

2. 为什么 MySQL 常使用 B+ 树索引?

B+ 树的非叶子节点主要保存索引键,数据记录集中在叶子节点,树的层级相对较低。叶子节点通常按顺序连接,适合范围查询和顺序扫描,也可以减少磁盘随机 I/O。

索引不是越多越好:索引会占用空间,并增加插入、更新和删除时的维护成本。索引设计应基于真实的查询条件、排序方式、数据分布和执行计划。

3. 什么是聚簇索引、回表和覆盖索引?

在 InnoDB 中,聚簇索引的叶子节点保存完整行数据,通常主键索引就是聚簇索引。普通二级索引的叶子节点保存索引列和主键值,通过二级索引找到主键后再回到聚簇索引查找完整记录,这个过程叫回表。

如果一个查询所需的列都能从索引中得到,就可以直接返回而不回表,这种索引称为覆盖索引。覆盖索引可以减少随机 I/O,但索引列过多也会增加存储和写入成本。

4. 什么是最左匹配原则?

对于联合索引 (a, b, c),查询通常需要从最左列开始匹配,才能充分利用索引。例如 a = ?、a = ? and b = ? 可以使用较完整的索引范围;如果跳过 a 直接查询 b,通常无法按照预期使用该联合索引。

范围条件会影响后续列的使用方式,具体情况还要结合优化器和执行计划判断。不能只根据 SQL 文本机械地判断索引是否生效。

5. 哪些情况可能导致索引失效?

常见情况包括:

  • 对索引列进行函数、表达式或隐式类型转换。
  • 使用前导模糊查询,例如 LIKE '%abc'。
  • 联合索引跳过最左列。
  • 使用 OR 时部分条件没有索引。
  • 过滤条件选择性很低,优化器认为全表扫描成本更低。
  • 对大范围数据排序、分组,索引无法覆盖全部操作。

最终是否使用索引,应通过 EXPLAIN 和实际执行情况确认。

6. 如何看 EXPLAIN?

常关注以下字段:

  • type:访问类型,通常希望避免 ALL 全表扫描。
  • possible_keys:可能使用的索引。
  • key:实际使用的索引。
  • key_len:使用的索引长度。
  • rows:优化器估算需要扫描的行数。
  • Extra:是否出现临时表、文件排序、索引条件下推等信息。

EXPLAIN 是成本估算,不等于实际执行统计。优化 SQL 后还需要通过慢查询日志、实际耗时和数据量验证效果。

五、MySQL 事务与并发控制

1. 事务的 ACID 是什么?

  • 原子性:事务中的操作要么全部成功,要么全部回滚。
  • 一致性:事务前后数据满足约束和业务规则。
  • 隔离性:并发事务之间按照隔离级别互相隔离。
  • 持久性:提交后的数据在故障恢复后仍然存在。

ACID 不是单一模块完成的,而是由存储引擎、日志、锁、事务管理和恢复机制共同提供。

2. MySQL 的隔离级别解决什么问题?

常见隔离级别从低到高包括读未提交、读已提交、可重复读和串行化。隔离级别越高,并发冲突和锁等待的可能性通常越大,但读到不一致数据的概率更低。

在 InnoDB 的默认隔离级别下,普通查询通常使用一致性读,依靠 MVCC 读取符合条件的历史版本;加锁查询则属于当前读,会读取最新版本并申请锁。

3. MVCC 是如何工作的?

MVCC 通过隐藏字段、Undo Log 和 Read View 等信息,为事务提供符合可见性规则的数据版本。普通快照读不一定需要阻塞其他事务,适合并发查询;更新、删除和带锁查询通常需要进行当前读并参与锁竞争。

MVCC 不能解决所有并发问题。涉及库存扣减、余额变更等业务时,仍需要使用条件更新、行锁、唯一约束或其他并发控制手段。

4. 行锁、间隙锁和临键锁是什么?

  • 行锁:锁定索引记录。
  • 间隙锁:锁定索引记录之间的范围,阻止其他事务在范围内插入。
  • 临键锁:行锁和间隙锁的组合。

是否加间隙锁与隔离级别、查询条件、索引结构和执行计划有关。没有合适索引时,锁的范围可能扩大,导致更严重的并发等待。

5. 如何排查死锁?

死锁表示多个事务互相等待对方释放资源。排查时可以:

  1. 查看数据库提供的死锁信息和锁等待信息。
  2. 对比事务的加锁顺序、访问范围和执行时间。
  3. 保证多个业务流程尽量按照相同顺序访问资源。
  4. 缩短事务范围,避免在事务中执行远程调用或长时间计算。
  5. 为事务失败设计重试和幂等机制。

死锁并不一定说明数据库有 Bug,业务必须能够正确处理死锁回滚后的重试。

六、MySQL 日志与性能优化

1. Redo Log、Undo Log 和 Binlog 有什么区别?

  • Redo Log:记录数据页的物理变化,用于崩溃恢复,保证已提交事务的持久性。
  • Undo Log:保存修改前的信息,用于事务回滚和 MVCC 版本读取。
  • Binlog:记录逻辑层面的变更事件,主要用于主从复制和数据恢复。

InnoDB 提交事务时需要协调 Redo Log 和 Binlog,避免出现存储引擎已经提交而复制日志没有记录,或相反的状态。

2. SQL 优化的一般流程是什么?

先确认问题是否稳定复现,再查看慢查询日志、执行计划和实际数据量。接着检查索引、过滤条件、排序分页、连接方式以及返回列数,完成修改后通过相同数据规模重新验证。

常见优化方向包括减少不必要的列和行、避免深分页、使用合适的联合索引、拆分复杂查询、批量处理写入,并将统计信息和业务访问模式纳入判断。

3. 什么是深分页问题?

LIMIT offset, size 在 offset 很大时,数据库可能需要扫描并丢弃大量前置记录。可以使用基于有序索引的游标分页,例如记录上一页最后一条数据的主键,再查询更大的主键范围。

1
2
3
4
5
SELECT id, title
FROM article
WHERE id > ?
ORDER BY id
LIMIT 20;

游标分页要求排序字段稳定,并且需要根据业务处理新增、删除和重复数据带来的边界问题。

七、Redis 与 MySQL 综合场景

1. 秒杀系统如何避免库存超卖?

可以先在 Redis 中进行库存预扣减和快速限流,再通过消息队列异步创建订单,最终由 MySQL 进行条件更新兜底:

1
2
3
UPDATE goods
SET stock = stock - 1
WHERE id = ? AND stock > 0;

成功后再创建订单,失败则释放或补偿预扣减库存。整个流程还要处理重复请求、消息重复投递、消费者失败和订单超时关闭等情况。

2. Redis 宕机后应该怎么办?

首先区分 Redis 承担的是缓存、锁、队列还是核心数据。纯缓存可以降级到数据库,但必须通过限流、熔断和热点保护,避免数据库被瞬间打垮;如果 Redis 保存了不可丢失的数据,则必须依赖持久化、主从、哨兵或集群,并制定恢复和数据校验方案。

缓存系统的降级开关、超时策略和数据库连接池上限,都应在正常环境提前演练,而不是故障发生后临时修改。

3. 如何设计缓存更新流程?

一个常见流程如下:

  1. 读取时先查 Redis,命中则返回。
  2. 未命中时通过互斥控制避免大量请求同时查询数据库。
  3. 查询数据库后写入 Redis,并设置合理的过期时间。
  4. 更新时先修改 MySQL,再删除 Redis 缓存。
  5. 删除失败通过重试、消息补偿或对账机制恢复。

如果业务允许短暂不一致,可以采用最终一致性;如果业务不允许读到旧值,则需要缩小缓存使用范围,或让关键读取直接依赖数据库事务结果。

4. 数据库压力突然升高如何排查?

可以沿着“流量 → 缓存 → 应用 → 数据库”的链路排查:

  • 查看接口请求量、缓存命中率和 Redis 延迟。
  • 确认是否发生缓存集中失效、热 Key 或大 Key。
  • 查看连接池、线程池和数据库活跃连接数。
  • 分析慢查询、锁等待、执行计划和临时表。
  • 检查最近发布、配置变更和异常重试。

处理时先止损,例如限流、降级非核心接口和恢复缓存,再定位根因并补充监控、索引或代码修复。

八、总结

Redis 面试重点在于数据结构、持久化、过期淘汰、高可用和缓存问题;MySQL 面试重点在于索引、事务、MVCC、锁、日志和 SQL 优化。综合场景题则要求把两者放进完整业务链路中思考:

  1. Redis 适合承担高频、低延迟和可重建的数据。
  2. MySQL 负责可靠存储、事务约束和最终业务结果。
  3. 缓存一致性需要结合业务容忍度设计,不能依赖单一技巧。
  4. 分布式锁、缓存和消息队列都不能替代数据库约束与幂等设计。
  5. 性能优化必须通过监控、执行计划和真实数据验证。
本文由 Guailper 撰写CC BY-NC-SA 4.0