一、读路径: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 就是这套)