Redis 商业场景训练营
Redis 不能只学 set/get,也不能只背“缓存穿透、击穿、雪崩”。商业项目里真正要会的是:知道某个业务数据适不适合放 Redis,知道 Key 怎么设计,知道为什么会 timeout,知道大 Key 和热 Key 怎么发现,知道缓存一致性为什么只能做最终一致,知道分布式锁为什么要 token、Lua、续期和业务幂等兜底。
训练目标:每个场景都要能写命令或代码,能解释原理,能说明不这样会怎样,能给出生产排查路径,能转成面试标准回答。
训练总流程
flowchart TD
A["识别业务场景"] --> B["选择数据结构"]
B --> C["设计 Key 和 TTL"]
C --> D["实现读写流程"]
D --> E["模拟高并发问题"]
E --> F["观察慢命令和内存"]
F --> G["治理大 Key / 热 Key"]
G --> H["设计一致性和降级"]
H --> I["整理面试回答"]训练一:资产详情 Cache Aside
场景
医疗资产详情页访问频繁,但资产最终事实数据在 MySQL。Redis 只做缓存,缓存丢了可以从数据库重建。
Key 设计
asset:detail:{assetId}示例命令:
set asset:detail:1001 '{"assetNo":"A001","name":"CT设备","status":"USED"}' ex 1800
get asset:detail:1001
ttl asset:detail:1001Java Demo
public AssetDTO getAsset(Long assetId) {
String key = "asset:detail:" + assetId;
String cached = stringRedisTemplate.opsForValue().get(key);
if (cached != null) {
return JsonUtils.fromJson(cached, AssetDTO.class);
}
Asset asset = assetMapper.selectById(assetId);
if (asset == null) {
stringRedisTemplate.opsForValue().set(key, "{}", Duration.ofMinutes(3));
return null;
}
AssetDTO dto = AssetDTO.from(asset);
stringRedisTemplate.opsForValue().set(key, JsonUtils.toJson(dto), Duration.ofMinutes(30));
return dto;
}原理图
flowchart TD
A["请求资产详情"] --> B["查 Redis"]
B --> C{"命中"}
C -- "是" --> D["返回缓存"]
C -- "否" --> E["查 MySQL"]
E --> F{"数据库有数据"}
F -- "有" --> G["写 Redis 并设置 TTL"]
F -- "无" --> H["缓存短 TTL 空值"]
G --> I["返回结果"]
H --> I为什么这样设计
| 设计 | 原因 | 不这样会怎样 |
|---|---|---|
| 数据库仍是事实源 | Redis 有淘汰、过期、故障切换风险 | 订单、资产状态可能丢失或不一致 |
| TTL | 给旧缓存自动修复机会 | 脏缓存可能长期存在 |
| 空值短 TTL | 防缓存穿透 | 不存在 ID 每次都打数据库 |
| JSON 控制大小 | 只缓存详情页需要字段 | 大 JSON 形成大 Key |
训练二:缓存穿透、击穿、雪崩
穿透:查不存在的数据
错误表现:大量随机 assetId 请求,Redis 没有,MySQL 也没有,每次都打数据库。
治理组合:
flowchart TD
A["请求 assetId"] --> B{"参数是否合法"}
B -- "否" --> C["直接拒绝"]
B -- "是" --> D{"布隆过滤器可能存在吗"}
D -- "否" --> E["直接返回空"]
D -- "是" --> F["查 Redis / MySQL"]
F --> G["不存在则缓存短 TTL 空值"]击穿:热点 Key 过期
热点资产详情刚好过期,大量请求同时回源数据库。
互斥锁 Demo:
public AssetDTO getHotAsset(Long assetId) {
String dataKey = "asset:detail:" + assetId;
String lockKey = "lock:asset:detail:" + assetId;
String cached = redis.get(dataKey);
if (cached != null) {
return JsonUtils.fromJson(cached, AssetDTO.class);
}
Boolean locked = redis.opsForValue()
.setIfAbsent(lockKey, "1", Duration.ofSeconds(10));
if (Boolean.TRUE.equals(locked)) {
try {
AssetDTO dto = loadFromDb(assetId);
redis.opsForValue().set(dataKey, JsonUtils.toJson(dto), Duration.ofMinutes(30));
return dto;
} finally {
redis.delete(lockKey);
}
}
sleep(50);
return getHotAsset(assetId);
}这里的锁只是为了保护回源,不是严格分布式业务锁。生产中还要控制递归重试、超时和降级。
雪崩:大量 Key 同时失效
解决:
- TTL 加随机抖动,例如 30 分钟加 0 到 5 分钟随机值。
- 热点数据提前预热。
- Redis 高可用。
- 数据库限流和降级。
- 多级缓存,例如本地缓存 + Redis。
训练三:大 Key 发现和治理
构造大 Key
hset hospital:assets:1 asset:1 '{"name":"A1"}'
hset hospital:assets:1 asset:2 '{"name":"A2"}'如果一个医院几十万资产都塞进一个 Hash,就会形成大 Key。
发现方式
redis-cli --bigkeys
memory usage hospital:assets:1
hlen hospital:assets:1
slowlog get 10为什么危险
flowchart TD
A["一个大 Key"] --> B["读取网络包大"]
A --> C["序列化耗时长"]
A --> D["删除释放内存慢"]
A --> E["AOF / 主从复制压力大"]
A --> F["Cluster 分片内存倾斜"]治理方式
| 问题 | 治理 |
|---|---|
| 一个 Hash field 太多 | 按页、科室、状态拆 Key |
| 需要遍历 | 用 HSCAN,禁止 HGETALL 大集合 |
| 删除阻塞 | 用 UNLINK 替代 DEL |
| 单 value 太大 | 只缓存必要字段,拆详情和扩展信息 |
示例拆分:
hospital:assets:{hospitalId}:dept:{deptId}:page:{pageNo}
asset:detail:{assetId}训练四:热 Key 发现和治理
场景
首页热门设备、全站配置、秒杀库存、热门排行榜都可能成为热 Key。
热 Key 为什么 Cluster 也扛不住
Redis Cluster 按 slot 分片,但一个 Key 只能落到一个 slot,也就是一个主节点处理。热 Key 的问题是流量集中,不是总容量不足。
flowchart TD
A["大量请求同一个 Key"] --> B["同一个 slot"]
B --> C["同一个 Master"]
C --> D["CPU / 网络打满"]
D --> E["其他节点仍然空闲"]治理方式
| 方法 | 适合场景 | 代价 |
|---|---|---|
| 本地缓存 | 极热点读多写少 | 有短暂不一致 |
| 多级缓存 | 配置、字典、详情 | 复杂度增加 |
| 拆 Key | 可分片计数、库存读 | 聚合复杂 |
| 请求合并 | 击穿回源 | 实现复杂 |
| 限流降级 | 秒杀和突发流量 | 牺牲部分请求 |
本地缓存示例:
LoadingCache<String, String> localCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofSeconds(5))
.build(key -> stringRedisTemplate.opsForValue().get(key));本地缓存 TTL 要短,避免配置或状态长期不一致。
训练五:Redis 与数据库一致性
更新流程
常用 Cache Aside 更新策略:先更新数据库,再删除缓存。
flowchart TD
A["更新资产状态"] --> B["写 MySQL"]
B --> C{"提交成功"}
C -- "否" --> D["回滚并返回失败"]
C -- "是" --> E["删除 Redis 缓存"]
E --> F["下次查询回源重建"]为什么不是先删缓存再写数据库
先删缓存后写数据库,中间如果有读请求,会读旧数据库值并回填缓存,导致旧缓存持续存在。
flowchart TD
A["删除缓存"] --> B["读请求进来"]
B --> C["查数据库旧值"]
C --> D["旧值写回缓存"]
D --> E["写数据库新值"]
E --> F["缓存仍是旧值"]删除缓存失败怎么办
删除失败不能静默吞掉。常用补偿:
- 重试删除。
- 写本地消息表或补偿表。
- 通过 MQ 异步删除。
- 通过 binlog CDC 监听数据库变更删除缓存。
- TTL 兜底最终过期。
标准回答
Redis 和数据库很难做到强一致,常用 Cache Aside 做最终一致。读流程先查缓存,未命中查数据库再回填;写流程通常先更新数据库,再删除缓存。删除缓存失败要有重试、MQ、补偿表或 binlog CDC 兜底,TTL 作为最终恢复手段。不能把 Redis 当事实源,订单、支付、资产状态仍以数据库为准。训练六:分布式锁和误删问题
基础加锁
set lock:collect:task:1001 8f4c9b ex 30 nx这个命令同时满足:
nx:不存在才设置。ex 30:自动过期,避免死锁。- value 是随机 token:用于释放时校验身份。
错误释放
不能直接:
del lock:collect:task:1001因为锁可能已经过期并被别人拿到,直接删除会删掉别人的锁。
Lua 释放
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end原理图
flowchart TD
A["客户端 A 加锁"] --> B["业务执行太久"]
B --> C["锁过期"]
C --> D["客户端 B 加锁成功"]
D --> E["A 直接 DEL"]
E --> F["误删 B 的锁"]所以锁必须有 token,释放必须比较 token。更严格场景还要 Fencing Token 或数据库状态机兜底。
训练七:哨兵和 Cluster 选型
| 模式 | 解决什么 | 不解决什么 |
|---|---|---|
| 主从 | 读扩展和备份副本 | 自动故障转移 |
| 哨兵 | 主从自动故障转移 | 横向分片扩容 |
| Cluster | 分片扩容和自动故障转移 | 单 Key 热点、跨 slot 多 key 操作 |
哨兵故障转移
flowchart TD
A["Sentinel 监控 Master"] --> B{"Master 是否主观下线"}
B -- "是" --> C["多个 Sentinel 投票"]
C --> D{"客观下线"}
D -- "是" --> E["选举新 Master"]
E --> F["其他 Slave 改复制源"]
F --> G["客户端更新地址"]Cluster 访问重定向
flowchart TD
A["客户端访问 Key"] --> B["计算 slot"]
B --> C{"当前节点负责吗"}
C -- "是" --> D["执行命令"]
C -- "否" --> E["返回 MOVED / ASK"]
E --> F["客户端访问正确节点"]最终验收清单
做完这页后,你应该能回答:
- Redis 适合放什么数据,不适合放什么数据?
- Cache Aside 读写流程是什么?
- 缓存穿透、击穿、雪崩分别是什么?
- 布隆过滤器为什么能拦截不存在 ID?
- 大 Key 为什么危险,怎么发现?
- 热 Key 为什么 Cluster 也可能扛不住?
- Redis 和数据库一致性为什么只能最终一致?
- 删除缓存失败怎么补偿?
- 分布式锁为什么要 token 和 Lua?
- Redlock 为什么仍要业务幂等或 Fencing Token 兜底?
- 哨兵和 Cluster 的区别是什么?
- Redis timeout 应该从哪些方向排查?
关联知识点
| 知识点 | 入口 |
|---|---|
| Redis 主线 | 从零到生产级掌握 |
| 命令与数据结构 | 命令执行与数据结构原理 |
| 过期与淘汰 | 过期删除与内存淘汰原理 |
| 缓存问题 | 缓存问题 |
| 布隆过滤器 | 布隆过滤器原理 |
| 大 Key / 热 Key | 大 Key 与热 Key 治理 |
| 高并发治理 | 高并发缓存治理 |
| 缓存一致性 | 缓存一致性 |
| 分布式锁 | 分布式锁 |
| 面试 | Redis 面试知识点 |
