Skip to content

Redis 商业场景训练营

Redis 不能只学 set/get,也不能只背“缓存穿透、击穿、雪崩”。商业项目里真正要会的是:知道某个业务数据适不适合放 Redis,知道 Key 怎么设计,知道为什么会 timeout,知道大 Key 和热 Key 怎么发现,知道缓存一致性为什么只能做最终一致,知道分布式锁为什么要 token、Lua、续期和业务幂等兜底。

训练目标:每个场景都要能写命令或代码,能解释原理,能说明不这样会怎样,能给出生产排查路径,能转成面试标准回答。

训练总流程

mermaid
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 设计

text
asset:detail:{assetId}

示例命令:

bash
set asset:detail:1001 '{"assetNo":"A001","name":"CT设备","status":"USED"}' ex 1800
get asset:detail:1001
ttl asset:detail:1001

Java Demo

java
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;
}

原理图

mermaid
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 也没有,每次都打数据库。

治理组合:

mermaid
flowchart TD
    A["请求 assetId"] --> B{"参数是否合法"}
    B -- "否" --> C["直接拒绝"]
    B -- "是" --> D{"布隆过滤器可能存在吗"}
    D -- "否" --> E["直接返回空"]
    D -- "是" --> F["查 Redis / MySQL"]
    F --> G["不存在则缓存短 TTL 空值"]

击穿:热点 Key 过期

热点资产详情刚好过期,大量请求同时回源数据库。

互斥锁 Demo:

java
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 同时失效

解决:

  1. TTL 加随机抖动,例如 30 分钟加 0 到 5 分钟随机值。
  2. 热点数据提前预热。
  3. Redis 高可用。
  4. 数据库限流和降级。
  5. 多级缓存,例如本地缓存 + Redis。

训练三:大 Key 发现和治理

构造大 Key

bash
hset hospital:assets:1 asset:1 '{"name":"A1"}'
hset hospital:assets:1 asset:2 '{"name":"A2"}'

如果一个医院几十万资产都塞进一个 Hash,就会形成大 Key。

发现方式

bash
redis-cli --bigkeys
memory usage hospital:assets:1
hlen hospital:assets:1
slowlog get 10

为什么危险

mermaid
flowchart TD
    A["一个大 Key"] --> B["读取网络包大"]
    A --> C["序列化耗时长"]
    A --> D["删除释放内存慢"]
    A --> E["AOF / 主从复制压力大"]
    A --> F["Cluster 分片内存倾斜"]

治理方式

问题治理
一个 Hash field 太多按页、科室、状态拆 Key
需要遍历HSCAN,禁止 HGETALL 大集合
删除阻塞UNLINK 替代 DEL
单 value 太大只缓存必要字段,拆详情和扩展信息

示例拆分:

text
hospital:assets:{hospitalId}:dept:{deptId}:page:{pageNo}
asset:detail:{assetId}

训练四:热 Key 发现和治理

场景

首页热门设备、全站配置、秒杀库存、热门排行榜都可能成为热 Key。

热 Key 为什么 Cluster 也扛不住

Redis Cluster 按 slot 分片,但一个 Key 只能落到一个 slot,也就是一个主节点处理。热 Key 的问题是流量集中,不是总容量不足。

mermaid
flowchart TD
    A["大量请求同一个 Key"] --> B["同一个 slot"]
    B --> C["同一个 Master"]
    C --> D["CPU / 网络打满"]
    D --> E["其他节点仍然空闲"]

治理方式

方法适合场景代价
本地缓存极热点读多写少有短暂不一致
多级缓存配置、字典、详情复杂度增加
拆 Key可分片计数、库存读聚合复杂
请求合并击穿回源实现复杂
限流降级秒杀和突发流量牺牲部分请求

本地缓存示例:

java
LoadingCache<String, String> localCache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(Duration.ofSeconds(5))
    .build(key -> stringRedisTemplate.opsForValue().get(key));

本地缓存 TTL 要短,避免配置或状态长期不一致。

训练五:Redis 与数据库一致性

更新流程

常用 Cache Aside 更新策略:先更新数据库,再删除缓存。

mermaid
flowchart TD
    A["更新资产状态"] --> B["写 MySQL"]
    B --> C{"提交成功"}
    C -- "否" --> D["回滚并返回失败"]
    C -- "是" --> E["删除 Redis 缓存"]
    E --> F["下次查询回源重建"]

为什么不是先删缓存再写数据库

先删缓存后写数据库,中间如果有读请求,会读旧数据库值并回填缓存,导致旧缓存持续存在。

mermaid
flowchart TD
    A["删除缓存"] --> B["读请求进来"]
    B --> C["查数据库旧值"]
    C --> D["旧值写回缓存"]
    D --> E["写数据库新值"]
    E --> F["缓存仍是旧值"]

删除缓存失败怎么办

删除失败不能静默吞掉。常用补偿:

  1. 重试删除。
  2. 写本地消息表或补偿表。
  3. 通过 MQ 异步删除。
  4. 通过 binlog CDC 监听数据库变更删除缓存。
  5. TTL 兜底最终过期。

标准回答

text
Redis 和数据库很难做到强一致,常用 Cache Aside 做最终一致。读流程先查缓存,未命中查数据库再回填;写流程通常先更新数据库,再删除缓存。删除缓存失败要有重试、MQ、补偿表或 binlog CDC 兜底,TTL 作为最终恢复手段。不能把 Redis 当事实源,订单、支付、资产状态仍以数据库为准。

训练六:分布式锁和误删问题

基础加锁

bash
set lock:collect:task:1001 8f4c9b ex 30 nx

这个命令同时满足:

  1. nx:不存在才设置。
  2. ex 30:自动过期,避免死锁。
  3. value 是随机 token:用于释放时校验身份。

错误释放

不能直接:

bash
del lock:collect:task:1001

因为锁可能已经过期并被别人拿到,直接删除会删掉别人的锁。

Lua 释放

lua
if redis.call("get", KEYS[1]) == ARGV[1] then
  return redis.call("del", KEYS[1])
else
  return 0
end

原理图

mermaid
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 操作

哨兵故障转移

mermaid
flowchart TD
    A["Sentinel 监控 Master"] --> B{"Master 是否主观下线"}
    B -- "是" --> C["多个 Sentinel 投票"]
    C --> D{"客观下线"}
    D -- "是" --> E["选举新 Master"]
    E --> F["其他 Slave 改复制源"]
    F --> G["客户端更新地址"]

Cluster 访问重定向

mermaid
flowchart TD
    A["客户端访问 Key"] --> B["计算 slot"]
    B --> C{"当前节点负责吗"}
    C -- "是" --> D["执行命令"]
    C -- "否" --> E["返回 MOVED / ASK"]
    E --> F["客户端访问正确节点"]

最终验收清单

做完这页后,你应该能回答:

  1. Redis 适合放什么数据,不适合放什么数据?
  2. Cache Aside 读写流程是什么?
  3. 缓存穿透、击穿、雪崩分别是什么?
  4. 布隆过滤器为什么能拦截不存在 ID?
  5. 大 Key 为什么危险,怎么发现?
  6. 热 Key 为什么 Cluster 也可能扛不住?
  7. Redis 和数据库一致性为什么只能最终一致?
  8. 删除缓存失败怎么补偿?
  9. 分布式锁为什么要 token 和 Lua?
  10. Redlock 为什么仍要业务幂等或 Fencing Token 兜底?
  11. 哨兵和 Cluster 的区别是什么?
  12. Redis timeout 应该从哪些方向排查?

关联知识点

知识点入口
Redis 主线从零到生产级掌握
命令与数据结构命令执行与数据结构原理
过期与淘汰过期删除与内存淘汰原理
缓存问题缓存问题
布隆过滤器布隆过滤器原理
大 Key / 热 Key大 Key 与热 Key 治理
高并发治理高并发缓存治理
缓存一致性缓存一致性
分布式锁分布式锁
面试Redis 面试知识点