一、读路径:Cache Aside(旁路缓存)

先查 Redis,命中就返回;未命中查 DB,回写 Redis 后返回。

Redis 三件套

问题 场景 解法
穿透 查一个不存在的 key,每次都打到 DB 缓存空值(短 TTL) + 布隆过滤器 + 参数校验
击穿 某个热点 key 过期,瞬间大量请求打到 DB 分布式锁只放一个线程回源 + 逻辑过期(异步重建) + 热点 key 不过期
雪崩 大批 key 同时过期或 Redis 宕机 TTL 加随机抖动 + 多级缓存(Caffeine + Redis) + 熔断降级(Sentinel) + 集群/哨兵高可用

二、写路径:先写 DB,再删缓存

不用"更新缓存"而用"删除缓存":避免并发写导致的脏数据,也避免为不会被读的数据做无用计算(lazy loading)。

为什么是"先 DB 后删"而不是"先删后写 DB"? 先删缓存,在写 DB 的间隙可能有读请求把旧值重新加载进缓存,且这个旧值会一直存在直到过期。

但先写后删仍有极小概率不一致(读线程读到旧 DB 值 → 写线程写 DB + 删缓存 → 读线程回写旧值)。兜底方案:

  • 缓存本身设 TTL(最终一致)
  • 延迟双删:删缓存 → 写 DB → sleep 一小段 → 再删一次
  • Canal 订阅 binlog 异步删缓存(与业务解耦,最可靠)

三、写 DB + 发消息:本地消息表(Outbox)

不能"写完 DB 再发 MQ"——两者不在同一事务里,发消息失败或应用宕机就丢消息;也不能先发 MQ,DB 可能回滚。

Outbox 做法:把消息当成一条记录,和业务数据在同一个本地事务里写进同一个库。事务提交 = 消息一定不丢。再由独立的投递方(定时轮询 outbox 表 / Canal 订阅 binlog)读出来发 MQ,发成功后标记已发送。

代价:至少投递一次(at-least-once),消费端必须幂等(唯一约束 / 去重表 / 状态机判断)。

横向扩容注意: 分布式锁。同一时刻只能有一个实例进行轮询

graph TD
    A[业务请求] --> B[开启本地事务]
    B --> C[写业务表]
    C --> D[写 outbox 表]
    D --> E[提交事务]
    E --> F[投递方轮询/binlog]
    F --> G[发送到 Kafka]
    G --> H[标记已发送]
    H --> I[消费端幂等处理]

四、分布式锁

用途:跨 JVM 的互斥。缓存击穿回源、防重复下单、定时任务单实例执行。

Redis 实现要点

  • SET key value NX EX <ttl> 一条命令完成加锁+设过期,防死锁
  • value 存唯一标识(UUID),解锁必须用 Lua 脚本校验 value 再删,防止删掉别人的锁
  • 锁过期但业务没执行完 → 看门狗自动续期(Redisson tryLock 默认 30s,每 10s 续期)
  • 生产直接用 Redisson,别手写

选型对比

方案 一致性 性能 说明
Redis (Redisson) 弱(主从切换可能丢锁) 绝大多数场景够用
RedLock 较强 争议大,Redisson 有实现
ZooKeeper / etcd 强(CP) 中低 临时顺序节点 + Watch,无羊群效应
DB 唯一索引/悲观锁 简单场景可用

注意:分布式锁只是性能优化和降低冲突概率,不能替代数据库层面的最终一致性保证(唯一约束、乐观锁版本号)。

五、消息队列相关

如何保证消息不丢?三段式

阶段 措施
生产端 Kafka acks=all + retries + 回调确认;RocketMQ 事务消息 / Outbox
Broker 端 多副本(replication.factor>=3)、min.insync.replicas=2、关闭 unclean 选举、刷盘策略
消费端 关闭自动提交,业务处理成功后手动 commit offset

消息重复 / 幂等:网络重传 + at-least-once 决定了必然重复。解法:唯一业务 ID + 去重表 / Redis SETNX / 数据库唯一索引 / 状态机流转。

消息顺序:Kafka 只保证单 Partition 有序。同一业务 key 路由到同一分区(如订单 ID 做 key),消费端单线程或按 key 哈希到线程处理。

消息积压:先看是消费慢还是生产暴增。临时扩分区+扩消费者、消费端改批量/异步、必要时先转存再补偿。

死信队列:重试 N 次仍失败进 DLQ,人工/定时补偿,避免阻塞整个分区。

六、分布式事务全景

graph TD
    A[分布式事务] --> B[强一致]
    A --> C[最终一致]
    B --> B1[2PC/XA
同步阻塞,性能差] B --> B2[Seata AT
自动生成反向SQL] C --> C1[TCC
Try-Confirm-Cancel
侵入强,性能好] C --> C2[Saga
长事务,补偿操作] C --> C3[本地消息表/Outbox
最常用] C --> C4[事务消息
RocketMQ半消息]

面试话术:能不用分布式事务就不用(合并服务边界)。必须用时,金融强一致选 TCC,长流程选 Saga,跨系统通知选 Outbox。

七、缓存进阶

  • 多级缓存:Caffeine(本地) → Redis → DB。本地缓存需处理集群间一致性(MQ 广播失效)
  • 热点 Key 探测:JD hotkey / 自研统计,发现后打散到本地缓存
  • 大 Key / 热 Key 问题:大 key 阻塞主线程(hash 分片存储),热 key 打垮单节点(加随机后缀分散)
  • 缓存预热:上线前/定时任务把热数据灌进 Redis
  • Redis 持久化:RDB(快照,快但丢数据) + AOF(日志,everysec 折中),生产混合模式

八、数据库

  • 索引:最左前缀、覆盖索引、索引下推、为什么用 B+ 树不用 B 树/哈希
  • MVCC:undo log 版本链 + ReadView,RC 每次读生成、RR 事务首次读生成
  • :记录锁/间隙锁/临键锁,RR 下如何避免幻读
  • 三大日志:redo log(崩溃恢复,WAL)、undo log(回滚+MVCC)、binlog(主从+归档),两阶段提交保证一致
  • 主从延迟:写后读走主库 / 半同步复制 / 并行复制
  • 分库分表:ShardingSphere,分片键选择,跨库 join 和分页问题,扩容(一致性哈希/双写迁移)

九、高可用与限流

手段 说明
限流 固定窗口 / 滑动窗口 / 漏桶 / 令牌桶(Sentinel、Guava RateLimiter)
熔断 错误率或 RT 超阈值直接快速失败,半开状态探活(Sentinel/Resilience4j)
降级 非核心链路返回兜底数据
隔离 线程池隔离 / 信号量隔离,防止雪崩传导
超时重试 必须设超时;重试要配合幂等 + 退避,否则放大故障

十、幂等设计(单独考的高频点)

场景 方案
前端重复提交 Token 机制(下单前领 token,提交时 Redis 删除)
接口重复调用 唯一请求 ID + 去重表(唯一索引)
状态流转 UPDATE ... WHERE status = '待支付' 乐观锁
MQ 重复消费 消费记录表 / Redis SETNX

十一、ID 生成

雪花算法(Snowflake) 时钟回拨问题、美团 Leaf(号段模式 + Snowflake)、UUID 无序导致 B+ 树页分裂。

十二、经典业务场景题

  • 秒杀:前端限流 → 网关限流 → Redis 预扣库存(Lua 保证原子) → MQ 削峰异步下单 → 库存最终对账
  • 延迟任务:Redis ZSet / RocketMQ 延迟消息 / 时间轮 / DB 轮询
  • 排行榜:Redis ZSet
  • 分布式 Session:Redis 集中存储 / JWT 无状态(你的 Sweet Home 就是这套)