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 适用与不适用场景
适用场景:
- 读多写少——商品详情、店铺信息、配置项、静态页面片段。
- 允许短暂不一致——用户看到的价格延迟 10 分钟完全可以接受。
- 查询成本高——一次 SQL 涉及多表 JOIN+聚合,缓存后收益大。
- 热点数据集中——Top 100 商品占了 80% 的流量,优先缓存。
不适用场景:
- 写入频繁且需要强一致的——支付状态、余额、库存扣减的核心判断。
- 数据实时性要求极高——如实时竞价、股票行情。
- 查询条件千变万化——每次都是不同的 SQL WHERE 条件,缓存命中率极低。
- 数据量巨大但热点不集中——全量缓存成本太高,收益太低。
4.4 常见踩坑经验
| 案例 | 表象 | 根因 | 解法 |
|---|---|---|---|
| 先删缓存再更新数据库 | 商品更新后页面仍显示旧价格 | 删除缓存后被并发读重新写入旧数据 | 改为先更新数据库再删除缓存 |
| 缓存 value 过大 | 商品详情接口 P95 延迟 50ms(缓存命中时也慢) | value 包含完整评论列表(500KB),每次网络传输成本高 | 评论列表拆成独立 key 或分页加载 |
| 互斥锁未设 TTL | 重建锁的请求崩溃后,所有请求都在等待锁释放 | 锁 key 未设 EX,持有锁的线程挂了,锁永不释放 | 锁必须带 EX,且释放用 Lua 比较 requestId |
| 空值缓存 TTL 太长 | 新建商品后 5 分钟内页面显示"商品不存在" | 空值标记 TTL=600s,商品创建后缓存仍有效 | 商品创建时主动删除对应的空值缓存 |
4.5 思考题
-
设计题:一个商品详情页需要展示"商品基本信息(变化频率低)"和"实时库存(变化频率高)"。两者的数据放在同一个缓存 key 里好还是两个 key 好?如果拆成两个 key,各自 TTL 应该怎么设计?
-
故障题:线上 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的网络实战圣经

微信公众号: 架构师日常笔记 欢迎关注!
浙公网安备 33010602011771号