1. 项目背景

1.1 业务场景

商品详情页是电商系统流量最大的接口。每个商品详情需要聚合商品基础信息、SKU 价格、库存快照、店铺信息、营销标签和最近 N 条精选评论。在没有缓存的情况下,每一次请求都要走 5-6 张数据库表的查询,P95 延迟在 80ms 左右。日常运营期间 QPS 只有 2000,MySQL 还能游刃有余。

大促当天,流量翻了 8 倍。首页推荐流引导更多用户点击商品详情,单接口 QPS 飙到 16000。数据库连接池很快被打满(max_connections=500),新请求在连接获取阶段就超时。更要命的是——某些热门商品(Top 10 爆款)被反复查询同一份数据,数据库为完全相同的查询反复执行 JOIN、排序、聚合。P95 延迟从 80ms 直线上涨到 900ms,超时率从 0.1% 涨到 8%。

DBA 紧急加了只读副本、扩大了连接池,但治标不治本。根本问题是:高频重复的读请求不应该每次都打到数据库。Redis 缓存就是来解决这个问题的。

1.2 缓存接入的典型痛点

问题 表现 为什么重要
怎么知道缓存何时失效? 用户看到的商品价格和下单时的不一样 缓存不一致会导致用户体验差,严重时造成资损
缓存没命中时怎么办? 缓存过期瞬间,大量请求打到数据库 这就是"缓存击穿"——热点 key 过期时最危险
不存在的商品 ID 被刷 恶意请求大量不存在的商品 ID,绕过缓存直接打数据库 这就是"缓存穿透"——攻击成本极低
大量缓存同时过期 缓存预热后 10 分钟,所有缓存同时失效 这就是"缓存雪崩"——数据库瞬间承压

1.3 本章目标

掌握 Redis 缓存的核心模式和实践:

  • Cache Aside(旁路缓存)模式:读(先查缓存→未命中查数据库→写缓存)和写(先更新数据库→删除缓存)的标准流程。
  • 缓存穿透的初级解法:空值缓存 + 参数校验 + 布隆过滤器思路。
  • 缓存击穿的初级解法:互斥锁重建(SET NX EX)。
  • 缓存雪崩的初级解法:TTL 随机抖动。
  • 压测验证:对比缓存接入前后的 QPS、P95 延迟和数据库查询次数。

2. 项目设计

2.1 第一幕:缓存不是让数据库下班

小胖:"数据库慢?那把热门商品全塞 Redis 里!SET product:1 json,以后只查 Redis,数据库直接下班,完美!"

小白:"那商品价格变了怎么办?库存扣光了怎么办?你 Redis 里的数据还是昨天的——用户看到 199 元,点进去下单时数据库是 299 元。你去解释?"

大师:"缓存不是'替代数据库',而是'帮数据库挡刀'。数据库永远是数据的事实来源(source of truth),Redis 只是它的投影。我画一下最经典的 Cache Aside 模式:"

Cache Aside(旁路缓存)读流程:

  请求到达
    │
    ▼
  ┌─────────────┐    命中    ┌──────────┐
  │ 查 Redis     │──────────▶│ 返回数据   │
  └─────────────┘            └──────────┘
    │ 未命中
    ▼
  ┌─────────────┐    存在    ┌──────────┐
  │ 查数据库     │──────────▶│ 写 Redis  │──▶ 返回数据
  └─────────────┘            └──────────┘
    │ 不存在
    ▼
  ┌─────────────┐
  │ 写空值缓存   │──▶ 返回 404/空
  └─────────────┘

Cache Aside 写流程:

  ┌─────────────┐    ┌──────────┐
  │ 更新数据库   │───▶│ 删除缓存  │
  └─────────────┘    └──────────┘

技术映射:应用层负责缓存和数据库之间的协调——先查缓存、未命中回源数据库、写入缓存。写操作是先更新数据库、再删除缓存(不是更新缓存)。

小胖追问:"为什么是'删除缓存'而不是'更新缓存'?我直接 SET 新值不是更快吗?"

大师:"因为'更新缓存'有四个坑。第一,商品详情可能涉及多张表的聚合——你更新了价格表,但缓存的 JSON 还包含库存、标签、评论数,这些字段怎么更新?第二,并发顺序问题——A 更新价格、B 更新库存,同时写 Redis,最终缓存里可能是 A 的价格 + B 的库存(不是某个完整时刻的数据)。第三,写缓存可能覆盖刚刚写完的新数据。第四,如果缓存更新成功但后续业务逻辑失败——这个缓存就没法回滚。删除缓存则简单得多——让下一次读自然回源重建。"

2.2 第二幕:穿透、击穿、雪崩——缓存的三座大山

小胖:"缓存看上去很简单啊,为什么网上有那么多'缓存穿透'、'缓存击穿'、'缓存雪崩'的文章?"

大师:"因为这三个问题是缓存架构的'经典三连'。它们名字相似,但成因和应对完全不同:"

问题 1: 缓存穿透
  成因: 请求的数据既不在缓存中,也不在数据库中(如恶意请求不存在的 ID)
  攻击方式: 脚本持续请求 product/-1、product/-2、product/-3...
  后果: 每次请求都穿透缓存直接打数据库,数据库做无用查询
  解法:
    ① 参数校验——ID 格式、范围不合法的直接拒绝
    ② 空值缓存——数据库返回 null 时,缓存一个短 TTL 的空标记
    ③ 布隆过滤器——用极小内存判断"key 一定不存在"(进阶方案)

问题 2: 缓存击穿
  成因: 某个热点 key 过期的一瞬间,大量并发请求同时回源数据库
  场景: 爆款商品缓存刚好在流量高峰期过期
  后果: 数据库瞬间承受成千上万个相同查询
  解法:
    ① 互斥锁重建——只有一个请求去查数据库,其他请求等待
    ② 逻辑过期——缓存 value 里带过期时间,发现过期后用异步线程重建
    ③ 永不过期 + 后台刷新——热点 key 不设 TTL,由后台任务定期更新

问题 3: 缓存雪崩
  成因: 大量 key 在同一时间段集中过期
  场景: 批量预热了 10 万条商品缓存,全设 600 秒 TTL
  后果: 600 秒后 10 万个 key 同时过期,数据库被回源请求淹没
  解法:
    ① TTL 加随机抖动——基础 600s + random(0, 120)
    ② 多级缓存——本地缓存 + Redis + 数据库,逐级兜底
    ③ 限流降级——缓存不可用时触发熔断,保护数据库

小白追问:"互斥锁重建具体怎么做?"

大师:"思路很简单——当缓存未命中时,不是所有请求都去查数据库。只有拿到'重建锁'的那个请求去查数据库并更新缓存,其他请求等待一小段时间后重试读缓存。"

互斥锁重建流程:

  缓存未命中
    │
    ▼
  尝试获取锁: SET lock:rebuild:product:{id} requestId NX EX 5
    │
    ├── 获取成功 → 查数据库 → 写缓存 → 释放锁 → 返回
    │
    └── 获取失败 → sleep(50ms) → 重新查缓存
                   ├── 有数据了 → 返回
                   └── 仍为空 → 再尝试获取锁(最多重试 3 次)

技术映射:穿透是"数据永远不存在",击穿是"数据突然不存在",雪崩是"数据集体不存在"。三个问题的应对策略不同,但可以组合使用。

2.3 第三幕:命中率——缓存有没有用的核心指标

小胖:"缓存有数据就是命中,没数据就是没命中吧?"

小白:"精确一点——keyspace_hits / (keyspace_hits + keyspace_misses) 就是命中率。但光看全局命中率不够。验证码的命中率几乎是 0%(因为都是一次性读取就删除),购物车命中率可能是 80%,它们和商品缓存的命中率混在一起看就没有意义。"

大师:"所以要做分业务的命中率监控。商品详情接口可以自己打点——每次请求记录'命中缓存'还是'回源数据库'。这样才能看出缓存策略的效果:"

业务维度命中率监控:

  商品详情接口: 命中率 92%、P95 15ms、数据库查询次数下降 85%
  店铺信息接口: 命中率 98%、P95 3ms(几乎全命中)
  营销标签接口: 命中率 78%、P95 25ms(标签变化频繁,可以考虑区分)

全局限(INFO stats): 命中率 65%(被验证码/一次性 token 拉低了)

3. 项目实战

3.1 环境准备

docker exec -it redis-lab redis-cli -a redis123

3.2 实战一:Cache Aside 基础实现

步骤 1:商品详情写入缓存

# 基础缓存写入(带随机 TTL)
127.0.0.1:6379> SET mall:cache:product:1001 '{"id":1001,"name":"机械键盘","price":299,"stock":88}' EX 637
OK
127.0.0.1:6379> GET mall:cache:product:1001
"{\"id\":1001,\"name\":\"机械键盘\",\"price\":299,\"stock\":88}"

步骤 2:Java 旁路缓存实现

public class ProductCacheService {

    private static final int BASE_TTL = 600;
    private static final int JITTER_MAX = 120;
    private static final int NULL_TTL = 60;  // 空值缓存 60 秒

    private final ThreadLocalRandom random = ThreadLocalRandom.current();

    // 读流程——旁路缓存
    public Product getProduct(Long productId) {
        // 1. 参数基础校验(防穿透第一关)
        if (productId == null || productId <= 0) {
            return null;
        }

        String cacheKey = "mall:cache:product:" + productId;
        String nullKey = cacheKey + ":null";

        // 2. 先查缓存
        String cached = redis.get(cacheKey);
        if (cached != null) {
            // 命中——检查是否是空值标记
            if ("__NULL__".equals(cached)) {
                return null;
            }
            return objectMapper.readValue(cached, Product.class);
        }

        // 3. 检查空值缓存(防止不存在 ID 反复穿透)
        if (Boolean.TRUE.equals(redis.hasKey(nullKey))) {
            return null;
        }

        // 4. 缓存未命中 → 查数据库
        Product product = database.findProduct(productId);

        if (product != null) {
            // 5. 写入缓存(带随机抖动)
            int ttl = BASE_TTL + random.nextInt(JITTER_MAX);
            redis.set(cacheKey, objectMapper.writeValueAsString(product), Duration.ofSeconds(ttl));
        } else {
            // 6. 写入空值缓存(防穿透)
            redis.set(nullKey, "__NULL__", Duration.ofSeconds(NULL_TTL));
        }

        return product;
    }

    // 写流程——先更新数据库,再删除缓存
    @Transactional
    public void updateProduct(Long productId, ProductUpdateCommand cmd) {
        // 1. 更新数据库(事实来源)
        database.updateProduct(productId, cmd);

        // 2. 删除缓存(让下一次读自然重建)
        redis.del("mall:cache:product:" + productId);

        // 注意: 不在这里 SET 新值!原因见对话中的分析
    }

    // 构建 key(统一入口,防止命名混乱)
    private String buildCacheKey(Long productId) {
        return "mall:cache:product:" + productId;
    }
}

3.3 实战二:互斥锁防止缓存击穿

步骤 1:手动模拟互斥锁

# 假设商品缓存已过期

# 请求 A 尝试获取重建锁
127.0.0.1:6379> SET mall:lock:rebuild:product:1001 req-A-001 NX EX 5
OK                           # 获取锁成功

# 请求 B 同时尝试获取
127.0.0.1:6379> SET mall:lock:rebuild:product:1001 req-B-001 NX EX 5
(nil)                        # 获取失败——请求 A 持有锁

# 请求 A 重建完成后释放锁(配合 Lua 保证原子性,第 13 章详解)
127.0.0.1:6379> EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end" 1 mall:lock:rebuild:product:1001 req-A-001
(integer) 1

步骤 2:Java 互斥锁实现

public class ProductCacheWithMutex {

    private static final int LOCK_TTL = 5;  // 锁 5 秒自动释放
    private static final int RETRY_SLEEP_MS = 50;
    private static final int MAX_RETRY = 3;

    public Product getProductWithMutex(Long productId) {
        String cacheKey = "mall:cache:product:" + productId;

        // 1. 先尝试读缓存
        String cached = redis.get(cacheKey);
        if (cached != null) {
            return deserialize(cached);
        }

        // 2. 缓存未命中 → 尝试互斥锁重建
        String lockKey = "mall:lock:rebuild:product:" + productId;
        String requestId = UUID.randomUUID().toString();

        for (int retry = 0; retry < MAX_RETRY; retry++) {
            // 尝试获取锁
            Boolean locked = redis.setIfAbsent(lockKey, requestId, Duration.ofSeconds(LOCK_TTL));
            if (Boolean.TRUE.equals(locked)) {
                // 获取锁成功 → 负责重建
                try {
                    Product product = database.findProduct(productId);
                    if (product != null) {
                        redis.set(cacheKey, serialize(product),
                            Duration.ofSeconds(BASE_TTL + random.nextInt(JITTER_MAX)));
                    }
                    return product;
                } finally {
                    // 释放锁(只释放自己持有的)
                    releaseLock(lockKey, requestId);
                }
            }

            // 锁被别人持有 → 等一会儿重试读缓存
            safeSleep(RETRY_SLEEP_MS);

            cached = redis.get(cacheKey);
            if (cached != null) {
                return deserialize(cached);
            }
        }

        // 3. 重试耗尽 → 降级:直接查数据库(不写缓存,避免重复竞争)
        log.warn("互斥锁重建失败,降级查库: productId={}", productId);
        return database.findProduct(productId);
    }

    private void releaseLock(String lockKey, String requestId) {
        // 用 Lua 保证原子性
        String lua = "if redis.call('GET', KEYS[1]) == ARGV[1] then " +
                     "return redis.call('DEL', KEYS[1]) else return 0 end";
        redis.eval(lua, Collections.singletonList(lockKey),
                   Collections.singletonList(requestId));
    }
}

3.4 实战三:空值缓存防穿透

# 查询一个不存在的商品 ID
127.0.0.1:6379> GET mall:cache:product:99999
(nil)    # 缓存未命中

# 数据库也查不到 → 写入空值标记(短 TTL)
127.0.0.1:6379> SET mall:cache:product:99999:null "__NULL__" EX 60
OK

# 60 秒内再次查询 → 命中空值缓存 → 不再查数据库
127.0.0.1:6379> GET mall:cache:product:99999:null
"__NULL__"

# 查看效果:空值 key 存在期间,数据库查询次数未增加

3.5 实战四:压测对比——有缓存 vs 无缓存

# 无缓存场景:每次请求去掉缓存,直接查数据库
# (实际项目中可用 A/B 开关控制)

# 有缓存场景:典型的 Cache Aside 流程
# 先预热热门商品
127.0.0.1:6379> SET mall:cache:product:1001 '{"id":1001,"name":"机械键盘","price":299}' EX 637
127.0.0.1:6379> SET mall:cache:product:1002 '{"id":1002,"name":"鼠标","price":99}' EX 642
127.0.0.1:6379> SET mall:cache:product:1003 '{"id":1003,"name":"显示器","price":1999}' EX 654

# 使用 redis-benchmark 模拟高并发 GET
docker exec redis-lab redis-benchmark -a redis123 -t get -n 100000 -c 100 -q

# 查看命中率(执行压测前后对比)
127.0.0.1:6379> INFO stats | grep keyspace
# keyspace_hits:xxx
# keyspace_misses:xxx

压测结论对比:

指标 无缓存 有缓存 改善
QPS ~2000 ~25000 12.5x
P50 延迟 80ms 2ms 40x
P95 延迟 150ms 5ms 30x
P99 延迟 500ms 15ms 33x
数据库 QPS 2000 ~160(8% miss) -92%
命中率 0% 92% -

3.6 实战五:TTL 随机抖动验证

# 批量写入 100 条缓存,观察 TTL 分布

# 脚本模拟(Java):
for (int i = 0; i < 100; i++) {
    String key = "mall:cache:product:" + (2000 + i);
    int ttl = 600 + ThreadLocalRandom.current().nextInt(120);
    redis.set(key, "data-" + i, Duration.ofSeconds(ttl));
}

# 查看 TTL 分布(10 分钟后)
# 某些 key 已过期(剩余 0-20s),某些尚未过期(剩余 60-120s)
# → 过期时间分散在 600~720s,防止集中过期

3.7 实战六:观察 INFO stats 指标

# 执行多次缓存读操作后查看统计
127.0.0.1:6379> INFO stats
# 重点关注:
# keyspace_hits:152340        总命中次数
# keyspace_misses:12890       总未命中次数
# instantaneous_ops_per_sec:8240  每秒命令数
# expired_keys:156           累计过期 key 数
# evicted_keys:0             累计淘汰 key 数

# 命中率 = 152340 / (152340 + 12890) ≈ 92.2%

# 按业务维度手动验证命中率
127.0.0.1:6379> GET mall:cache:product:1001   # 命中
127.0.0.1:6379> GET mall:cache:product:99999  # 未命中(不存在)

3.8 实战七:本地缓存 + Redis 二级缓存架构

当 Redis 本身也成为瓶颈时(比如 Redis 带宽打满),可以在应用层加本地缓存(Caffeine/Guava)作为 L1,Redis 作为 L2:

public class TwoLevelCacheService {

    // L1: 本地缓存(极快,但每个实例独立)
    private final Cache<String, Product> localCache = Caffeine.newBuilder()
        .maximumSize(1000)           // 只缓存最热的 1000 个商品
        .expireAfterWrite(30, TimeUnit.SECONDS)  // 30 秒过期
        .recordStats()               // 记录命中率
        .build();

    // L2: Redis(跨实例共享)
    public Product getProduct(Long productId) {
        String key = "mall:cache:product:" + productId;

        // 1. 查 L1 本地缓存
        Product cached = localCache.getIfPresent(key);
        if (cached != null) {
            metricsCollector.record("cache.l1.hit");
            return cached;
        }

        // 2. 查 L2 Redis
        String redisJson = redis.get(key);
        if (redisJson != null) {
            Product product = deserialize(redisJson);
            localCache.put(key, product);  // 回填 L1
            metricsCollector.record("cache.l2.hit");
            return product;
        }

        // 3. 回源数据库
        metricsCollector.record("cache.miss");
        Product product = database.findProduct(productId);
        if (product != null) {
            redis.set(key, serialize(product), Duration.ofSeconds(BASE_TTL + random.nextInt(JITTER_MAX)));
            localCache.put(key, product);
        }
        return product;
    }

    // 缓存更新时——先删 Redis(L2),本地缓存靠 TTL 自然过期
    // 如果需要即时生效,可以通过 Redis Pub/Sub 广播失效消息
    public void invalidateCache(Long productId) {
        String key = "mall:cache:product:" + productId;
        redis.del(key);
        localCache.invalidate(key);
    }
}

二级缓存的关键权衡:

维度 单级 Redis Redis + 本地缓存
P50 延迟 2ms(网络) 0.01ms(内存)
一致性 读 Redis 总是最新的 L1 可能滞后 30 秒(本地缓存过期)
内存 集中在 Redis 分散在各应用实例
失效通知 删 Redis 即可 需要 Pub/Sub 或消息通知全网刷新
适用场景 一致性要求高、数据变化频繁 热点极高、可容忍 30s 延迟

3.9 实战八:缓存更新策略对比实验

除了 Cache Aside 的"先更新数据库,再删除缓存",还有其他几种策略:

# 策略对比实验:
# 1. 先删缓存 → 再更新数据库 → 再删缓存(延迟双删)
# 2. 先更新数据库 → 再删除缓存(Cache Aside 标准)
# 3. 先更新数据库 → 再更新缓存(不推荐)

# 模拟延迟双删策略:
127.0.0.1:6379> DEL mall:cache:product:1001          # 第1次删除
(integer) 1
# ... 更新数据库 ...
127.0.0.1:6379> SET mall:cache:product:1001 'old' EX 600  # (并发读写入旧值)
OK
# 延迟 500ms 后
127.0.0.1:6379> DEL mall:cache:product:1001          # 第2次删除(清理脏数据)
(integer) 1
// 延迟双删实现
@Transactional
public void updateProductWithDoubleDelete(Long productId, ProductUpdateCommand cmd) {
    String cacheKey = "mall:cache:product:" + productId;

    // 1. 第一次删除缓存
    redis.del(cacheKey);

    // 2. 更新数据库
    database.updateProduct(productId, cmd);

    // 3. 延迟 N 毫秒后第二次删除(用异步任务避免阻塞主线程)
    scheduler.schedule(() -> {
        redis.del(cacheKey);
        log.debug("延迟双删完成: productId={}", productId);
    }, 500, TimeUnit.MILLISECONDS);
}

三种更新策略对比:

策略 不一致窗口 风险 适用场景
先删缓存→更新数据库 中 并发读回写旧值 不推荐单独使用
更新数据库→删缓存 短(微秒级) 缓存删除失败 99% 的读多写少场景
延迟双删 短(等待第二次删除) 复杂度高、仍需异步 一致性要求较高的场景
更新数据库→更新缓存 无 并发写顺序错乱、字段不完整 极简单的 key-value(非聚合对象)

4. 项目总结

4.1 缓存三兄弟速查

问题 成因 核心解法 实现命令
缓存穿透 请求不存在的数据 空值缓存 + 参数校验 SET key:null "__NULL__" EX 60
缓存击穿 热点 key 过期+高并发 互斥锁重建 SET lock:rebuild:... NX EX 5
缓存雪崩 大量 key 同时过期 TTL 随机抖动 EX 600+random(0,120)

4.2 Cache Aside 优点与缺点

维度 详细说明
优点1:简单通用 读和写的逻辑清晰,应用层控制力强
优点2:懒加载 只缓存真正被访问的数据,不浪费内存
优点3:数据库权威 任何时刻数据库都是正确的,缓存只是投影
优点4:故障隔离 缓存挂了只会导致短暂的回源,不会丢失数据
缺点1:不一致窗口 更新数据库→删除缓存之间,读到的是旧缓存
缺点2:首次慢 冷启动或缓存全清后,第一个请求必须回源
缺点3:击穿风险 热点 key 过期瞬间需要额外保护
缺点4:应用负责 所有缓存逻辑都在应用层,多个服务需要统一

4.3 适用与不适用场景

适用场景:

  1. 读多写少——商品详情、店铺信息、配置项、静态页面片段。
  2. 允许短暂不一致——用户看到的价格延迟 10 分钟完全可以接受。
  3. 查询成本高——一次 SQL 涉及多表 JOIN+聚合,缓存后收益大。
  4. 热点数据集中——Top 100 商品占了 80% 的流量,优先缓存。

不适用场景:

  1. 写入频繁且需要强一致的——支付状态、余额、库存扣减的核心判断。
  2. 数据实时性要求极高——如实时竞价、股票行情。
  3. 查询条件千变万化——每次都是不同的 SQL WHERE 条件,缓存命中率极低。
  4. 数据量巨大但热点不集中——全量缓存成本太高,收益太低。

4.4 常见踩坑经验

案例 表象 根因 解法
先删缓存再更新数据库 商品更新后页面仍显示旧价格 删除缓存后被并发读重新写入旧数据 改为先更新数据库再删除缓存
缓存 value 过大 商品详情接口 P95 延迟 50ms(缓存命中时也慢) value 包含完整评论列表(500KB),每次网络传输成本高 评论列表拆成独立 key 或分页加载
互斥锁未设 TTL 重建锁的请求崩溃后,所有请求都在等待锁释放 锁 key 未设 EX,持有锁的线程挂了,锁永不释放 锁必须带 EX,且释放用 Lua 比较 requestId
空值缓存 TTL 太长 新建商品后 5 分钟内页面显示"商品不存在" 空值标记 TTL=600s,商品创建后缓存仍有效 商品创建时主动删除对应的空值缓存

4.5 思考题

  1. 设计题:一个商品详情页需要展示"商品基本信息(变化频率低)"和"实时库存(变化频率高)"。两者的数据放在同一个缓存 key 里好还是两个 key 好?如果拆成两个 key,各自 TTL 应该怎么设计?

  2. 故障题:线上 Redis 因为网络抖动短暂不可用了 5 秒。恢复后短时间内 keyspace_misses 骤增、数据库 QPS 暴涨。请问这是什么现象?如何在架构层面减少这种故障的影响?

(答案提示将在下一章末尾给出。)

上一章思考题答案提示(第11章):

第1题(多国家站 key 设计):key 模板为 {country}:mall:cache:product:{id},如 cn:mall:cache:product:1001、jp:mall:cache:product:1001。优点:按前缀隔离、统计(SCAN ... MATCH cn:*)、清理。如果法律要求数据不能跨国传输,需要每个国家站部署独立 Redis 实例——key 设计中的国家前缀可作为跨实例迁移的兼容层。

第2题(expired_keys 增长+CPU 升高):可能原因——① 大量短期 key 的定期删除被触发,activeExpireCycle 在大量过期 key 时 CPU 集中消耗;② 过期 key 集中在少数几个大数据库的过期字典中,抽样效率低。健康检查方案:监控 expired_keys 的增长速率(而非绝对数),对比 CPU 使用率趋势;如果过期速率陡增,检查最近是否有大批量短期 key 写入;调整 hz 参数增大定期删除的频率。

延伸阅读与资源

Redis 8 实战精讲:从 CRUD 到源码,构建高可用缓存系统
Java 工程师进阶:从 JVM 生产排障到OpenJDK原理
MySQL Server 9从入门到进阶的实战之旅
Nacos 3.x 注册配置中心实战修炼:从入门到源码扩展
Elasticsearch从入门到进阶的实战之旅
RabbitMQ从入门到进阶的实战之旅:从单机到大促高可用架构
Celery 入门到进阶之路:从异步任务到自研调度平台
LangGraph 生产级实战进阶:从零到生产级Agent工作流开发
Dify 从入门到源码:LLM 应用平台实战修炼
从零到生产级:FastAPI 异步高并发、源码与 SRE 实战
实战SQLAlchemy 2.0: 从 CRUD 到生产级架构
从零打造企业级 AI 助手:LangChain RAG、Agent 与生产实战
后端工程师 AI 转型课:Ollama 私有化大模型从入门到生产
MongoDB 实战进阶与内核修炼
NumPy 从入门到生产落地:全链路实战指南(科学计算/向量化/性能调优)
Milvus向量数据库实战修炼:从 0 到 1 精通向量检索与生产落地
Python 3实战精进:从脚本到高并发订单引擎
python入门:Rquests从菜鸟脚本到企业级SDK的网络实战圣经

posted on 2026-11-06 10:16  一天不进步,就是退步  阅读(7)  评论(0)    收藏  举报