BigKey、内存与多级缓存:容量和复制成本怎样失控
一个 5 MB value 不只占 5 MB:它要被分配、复制、持久化、通过网络、解码,再进入客户端堆;多副本和多级缓存会继续相乘。BigKey 的危险是让一次普通操作变成长任务,并在故障恢复时批量重现。
Redis 官方把 key expiration 与 maxmemory eviction 分开定义;过期时间是 key 生命周期,驱逐是内存超限后的容量策略。本地 L1 与远端 L2 又有独立生命周期,必须通过版本和失效协议组合。
TTL:过期时间是业务设计,不是随手写个 30 分钟
TTL 决定缓存的生命周期,也决定数据库回源压力和脏数据窗口。不要所有 key 都写同一个过期时间。
基础 TTL
SET shop:prod:config:pay-channel:v1 '{"enabled":true}' EX 300
TTL shop:prod:config:pay-channel:v1如果 TTL 返回 -1,说明 key 没有过期时间。缓存数据没有 TTL,通常是生产风险,除非你有明确的主动失效和巡检机制。
随机 TTL 防雪崩
大量 key 同时过期,会造成瞬时回源。应用侧可以给 TTL 加随机扰动:
long baseSeconds = 1800;
long jitterSeconds = ThreadLocalRandom.current().nextLong(0, 300);
Duration ttl = Duration.ofSeconds(baseSeconds + jitterSeconds);常见坑是只给热点 key 加随机 TTL,普通 key 全部固定 30 分钟。实际雪崩往往来自批量预热、批量导入或发布后统一回填,所有 key 的过期时间非常接近。
空值 TTL 要短
缓存不存在数据可以防穿透,但空值 TTL 不能太长。
真实对象 TTL:30 分钟到 2 小时,按业务更新频率调整
空值 TTL:30 秒到 5 分钟,按误伤容忍度调整
热点保护 TTL:逻辑过期 + 异步刷新生产建议:TTL 要按缓存类型分层管理。配置、字典、详情、列表、排行榜、空值和热点数据不应该使用同一套过期策略。
过期字典:TTL 到点不等于立刻消失
给 key 设置过期时间以后,Redis 会把过期时间维护在过期索引里。开发侧最容易误解的一点是:TTL 到点不代表 key 在那个毫秒立刻从内存消失。Redis 会结合两条路径清理:
惰性删除:客户端访问 key 时发现已经过期,顺手删除并返回不存在。主动过期:后台周期性抽样检查带过期时间的 key,发现过期就删除。
最小实验:
SET shop:prod:tmp:expire-demo "1" EX 2
TTL shop:prod:tmp:expire-demo
GET shop:prod:tmp:expire-demo
INFO stats
CONFIG GET active-expire-effort验证结果:
TTL 倒计时到 -2 说明 key 不存在。INFO stats 里的 expired_keys 增长,说明过期删除发生过。如果 key 很少被访问,删除可能由主动过期周期完成;如果刚好被访问,就走惰性删除。
active-expire-effort 调高后要观察 CPU、延迟和 expired_keys,不能只看内存下降。
常见坑:
生产建议:TTL 是缓存生命周期,不是业务定时任务。需要精确触发的场景,用任务调度、延迟队列或可靠 MQ,不要指望 Redis 过期事件承担强语义。
淘汰策略:内存满了以后谁先走
TTL 解决“业务上什么时候可以失效”,淘汰策略解决“内存超过上限时 Redis 怎么自救”。这两个概念不能混在一起。
淘汰也不是全局精确排序。LRU、LFU、TTL 类策略都会在候选 key 中采样,maxmemory-samples 越大,近似结果越接近理想淘汰,但 CPU 成本也越高。开发侧要记住:淘汰策略是实例自救,不是业务容量规划。
先看当前配置:
CONFIG GET maxmemory
CONFIG GET maxmemory-policy
CONFIG GET maxmemory-samples
INFO memory
INFO stats常见策略怎么选:
| 策略 | 行为 | 开发侧适用判断 |
|---|---|---|
noeviction | 内存超过上限后,可能写入失败 | 不能接受缓存悄悄丢 key,愿意让写入显式失败 |
allkeys-lru | 从所有 key 里近似淘汰最近最少使用 | 纯缓存实例常见默认选择,要求数据都可回源 |
allkeys-lfu | 从所有 key 里近似淘汰低频访问 | 访问频率差异明显的缓存 |
volatile-lru | 只淘汰设置了 TTL 的 key | 同实例里混有不该淘汰的 key,但更推荐拆实例 |
volatile-ttl | 优先淘汰 TTL 更短的 key | 业务能用 TTL 表达优先级 |
allkeys-random | 随机淘汰 | key 访问分布接近均匀,或临时压测验证 |
验证是否正在淘汰:
INFO stats
INFO memory重点看:
evicted_keys 是否持续增加。keyspace_hits / (keyspace_hits + keyspace_misses) 是否低于预期。used_memory_dataset 是否长期贴近或超过 maxmemory。
写命令是否因为 noeviction 被拒绝。maxmemory-samples 是否被改过,改动前后 P99 和 CPU 是否有对照。
生产建议:不要把永久数据和缓存数据塞在同一个 Redis 实例里再靠 volatile-* 策略“精细治理”。能拆实例就拆实例,不能拆时至少保证所有缓存 key 都有 TTL,并且明确哪些 key 被淘汰后能回源、哪些不能。
热点 key 和 BigKey:性能问题常常不是 Redis 不够快
热点 key 是访问过于集中,BigKey 是单个 key 的 value 或集合成员过大。两者都会让 Redis、网络和客户端抖动。
怎么发现
先看慢命令:
SLOWLOG GET 10看 key 的内存:
MEMORY USAGE shop:prod:order:detail:100001:v1扫描疑似大 key:
redis-cli --bigkeys按模式抽样:
SCAN 0 MATCH shop:prod:order:* COUNT 100SCAN 不是一次性全量阻塞命令,但也不是完全没有成本。生产高峰不要用大 COUNT 粗暴扫描。
怎么治理
热点 key:
本地短缓存,降低 Redis 往返。key 拆分或副本 key,分摊读压力。逻辑过期 + 后台刷新。
对热点接口限流和降级。
BigKey:
大对象拆成多个小 key。大集合分页读取,不用 HGETALL、SMEMBERS、大范围 ZRANGE。删除大 key 用 UNLINK 或渐进式删除,避免阻塞。
建立 value 大小和集合元素数量上限。
示例:
UNLINK shop:prod:large:temp:<RUN_ID>生产建议:BigKey 阈值要按业务和机器规格定。不要迷信一个固定数字。你要看的是单次命令耗时、网络流量、序列化耗时和对其他 key 的影响。
fork、AOF/RDB 和 P99:持久化不是运维才关心
Redis 安装和备份恢复属于运维链路,但开发必须知道持久化会影响在线请求。BGSAVE、AOF rewrite 等操作通常会触发 fork,父子进程通过写时复制共享内存页;如果 fork 期间实例内存很大、页面很活跃、宿主机内存紧张,在线请求 P99 可能抖动。AOF fsync 策略、磁盘抖动和 rewrite 期间的增量缓冲,也会影响写入尾延迟。
开发排障时可以按这条链路把 P99 抖动和持久化现场串起来:
这部分虽然不写成备份恢复教程,但开发必须会把 Redis 慢请求、应用 P99、latest_fork_usec 和批量任务时间线放在一起看。
先看现场:
INFO persistence
INFO memory
LATENCY LATEST
LATENCY DOCTOR重点看:
latest_fork_usec 是否在抖动时突然升高。rdb_bgsave_in_progress、aof_rewrite_in_progress 是否和接口 P99 同时出现。aof_pending_bio_fsync、aof_delayed_fsync 是否持续增长。
used_memory_rss 和 mem_fragmentation_ratio 是否让 fork 写时复制更容易放大。
生产建议:业务批量预热、大 key 删除、批量刷新和数据修复不要和备份、AOF rewrite、实例迁移挤在同一窗口。即使持久化参数由运维负责,开发也要把 Redis P99 抖动和 INFO persistence 放进事故排查路径。
用状态模型验证缓存窗口
两个 Java 17 模型不连接真实 Redis,也不复刻 Caffeine 内部结构。它们把本篇必须守住的状态、版本和容量不变量变成确定输出;真实客户端与 Redis 集成测试继续验证协议、超时、拓扑和服务器行为。
javac --release 17 -Xlint:all -Werror examples/backend-development/cache/bigkey-memory-multilevel/BigKeyBudgetDemo.java examples/backend-development/cache/bigkey-memory-multilevel/MultilevelVersionDemo.java
java -cp examples/backend-development/cache/bigkey-memory-multilevel BigKeyBudgetDemo
java -cp examples/backend-development/cache/bigkey-memory-multilevel MultilevelVersionDemoitems=50000 valueBytes=4800000 replicatedBytes=14400000 rejected=true
l1Version=6 l2Version=7 selectedVersion=7 l1Refreshed=true缓存正确性不是“命中返回值”,而是命中值仍在允许的陈旧窗口内、加载不会放大到权威存储、旧版本不能覆盖新版本、失败能够在预算内退化。模型输出任一项改变,都应触发对应集成用例,而不是只调整命中率告警。
