主题
生产运维:部署规格、监控与 bigkey/hotkey 治理
本文是 Redis 系统学习系列的 L3 实战篇。前置:30. 内存模型与编码切换。 学完可以配合面试题食用:10-big-key-problem-detection-mitigation、11-hot-key-discovery-solutions
部署规格:单实例能放多大
Redis 单实例的内存上限不是硬件决定的,是三个因素卡的:
- fork 耗时:RDB/AOF 重写都要 fork 子进程。10GB 实例 fork 大约 20-30ms,50GB 可能到 200ms+,期间服务阻塞。
- 主从同步时间:全量同步要传 RDB 文件。50GB 实例在 1Gbps 网络下传输约 400 秒,期间从库不可用。
- 恢复时间:实例重启后加载 RDB/AOF 恢复数据,50GB 实例可能 5-10 分钟才能 ready。
生产建议:单实例控制在 10GB 以内。超过 20GB 必须拆实例或转 Cluster。CPU 不用太高,4-8 核即可(Redis 单线程,计算密集度低)。网络带宽是瓶颈,千兆网单实例大约能扛 8-10 万 QPS 的简单 SET/GET。
监控大盘必采指标
不是所有 info 字段都重要,以下 8 个是底线:
| 指标 | 来源 | 正常范围 | 报警阈值 |
|---|---|---|---|
| 命中率 | keyspace_hits / (keyspace_hits + keyspace_misses) | > 90% | < 80% |
| used_memory_rss / used_memory | INFO memory | 1.0-1.5 | > 1.5 或 < 1.0 |
| 连接数 | connected_clients | < 5000 | > 8000 |
| 阻塞客户端 | blocked_clients | 0 | > 0 |
| latest_fork_usec | INFO stats | < 50ms | > 200ms |
| slowlog 计数 | 按分钟聚合 | 0 | > 1/min |
| 复制延迟 | master_repl_offset - slave_repl_offset | < 10 | > 100 |
| 过期 key 占比 | expired_keys / (expired_keys + total_keys) | < 5% | > 10% |
碎片率 > 1.5 说明内存碎片严重,检查 activedefrag 是否开启;< 1.0 说明实例在 swap(被迫用虚拟内存)或内存不足。
bigkey 治理流程
发现
三个方法,按覆盖度递增:
bash
# 方法一:redis-cli --bigkeys(顺序扫描,逐类型统计)
redis-cli --bigkeys
# 输出示例:
# -------- biggest key in type string --------
# key: "session:user:320918" size: 2.3 MB
# -------- biggest key in type hash --------
# key: "product:tags:9872" size: 45 MB (362000 fields)
# -------- biggest key in type set --------
# key: "news:all_readers" size: 1.8 MB (12300 members)
# 方法二:RDB 离线分析(rdb-tools),全量精确
# rdb -c memory dump.rdb --bytes 10 | head -20
# 方法三:SCAN 脚本逐 key 取 MEMORY USAGE
# redis-cli --scan --pattern "*" | while read k; do
# size=$(redis-cli memory usage "$k")
# [ "$size" -gt 10485760 ] && echo "$k = $size bytes"
# done--bigkeys 只取每个类型最大的,适合快速扫描。生产上建议定时跑 RDB 离线分析,能拿到全量分布。
改造
bigkey 的改造方向取决于业务:
- Hash 大 key:按 field 做 hash 分桶。例如
user:12345:friends拆成user:12345:friends:0到user:12345:friends:15,查询时读 16 个桶再合并。拆分后每个桶 < 1 万 field。 - List 大 key:改成 LTRIM 限制长度,或拆成多段 List。
- Set 大 key:改成分片 Set + 业务层聚合,或用 HyperLogLog 替代(如果只需要计数)。
- String 大 value:压缩(gzip 后 store),或拆分到多个 key。
重建
删除 bigkey 不能直接 DEL(阻塞事件循环)。用 SCAN 渐进删除:
java
// 渐进删除大 Hash
String key = "product:tags:9872";
int cursor = 0;
do {
// SCAN 每次取 100 个 field
String resp = redisTemplate.execute(
(RedisCallback<String>) conn -> {
Object[] args = new Object[]{key.getBytes(),
String.valueOf(cursor).getBytes(), "COUNT".getBytes(), "100".getBytes()};
return conn.execute("HSCAN", args).toString();
}
);
// 解析 cursor 并 HDEL 这批 field
// ... 实际用 Lua 脚本原子化完成 SCAN + HDEL
cursor = parseCursor(resp);
} while (cursor != 0);Redis 4.0+ 提供 UNLINK 命令(后台线程回收内存),替代 DEL 做 bigkey 删除。生产上删除 bigkey 优先用 UNLINK。
hotkey 治理
发现
hotkey 的发现没有 bigkey 那么方便,原因在于 Redis 本身不记录每个 key 的访问频率(极简设计,不做统计)。常用方法:
- redis-cli --hotkeys(Redis 6.0+):以约 10% QPS 性能损耗为代价,通过
object freq采样。适合"先查一下有没有 hotkey"的场景。 - 客户端采样:在 Jedis/Lettuce 的拦截器里计数每个 key 的调用次数,周期性上报到监控系统。精度最高,但需要改业务代码。
- 代理层统计:如果前端有 Codis/Twemproxy 或自研代理,在代理层统计算是最优解——不改业务代码,全局可见。
解决方案
hotkey 治理有三层递进策略:
第一层:本地缓存兜底
java
// 热点数据先查本地缓存,挡住 90% 的请求
public class HotkeyAwareService {
private final Cache<String, Object> localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(1, TimeUnit.SECONDS) // 短过期,一致性窗口
.build();
public Object get(String key) {
Object val = localCache.getIfPresent(key);
if (val != null) return val;
val = redisTemplate.opsForValue().get(key);
if (val != null) {
localCache.put(key, val);
}
return val;
}
}第二层:key 打散(随机后缀)
单 key 成为热点时,单个 Redis 实例扛不住。把请求分散到 N 个副本 key:
java
public class KeyScatterService {
private static final int SCATTER_SIZE = 10;
// 读:随机选一个副本
public String getScattered(String baseKey) {
int idx = ThreadLocalRandom.current().nextInt(SCATTER_SIZE);
String scatterKey = baseKey + ":" + idx;
return redisTemplate.opsForValue().get(scatterKey);
}
// 写:更新所有副本(用 pipeline 或 Lua 批量写)
public void setScattered(String baseKey, String value) {
List<String> keys = new ArrayList<>();
for (int i = 0; i < SCATTER_SIZE; i++) {
keys.add(baseKey + ":" + i);
}
redisTemplate.executePipelined((RedisCallback<Object>) conn -> {
for (String k : keys) {
conn.set(k.getBytes(), value.getBytes());
}
return null;
});
}
}第三层:写频率降级。如果 hotkey 同时是写热点(如秒杀扣库存),本地缓存不适用,要用 Redis 自身的 Lua 脚本做原子计数 + 异步写 DB。
危险命令管控
default 配置下 Redis 是"裸奔"的,这些命令建议全部 rename 或禁用:
bash
# redis.conf 中重命名/禁用危险命令
rename-command KEYS ""
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command CONFIG "ADMIN_CONFIG"
rename-command SHUTDOWN "ADMIN_SHUTDOWN"
rename-command DEBUG ""
rename-command SCRIPT ""
# 如果使用 Cluster,还有
rename-command MIGRATE ""
rename-command RESTORE ""KEYS 线上一律禁用,用 SCAN 替代。FLUSHALL/FLUSHDB 误操作就是删库,日常开发用 FLUSHDB 的都要 rename 加限权。CONFIG 的 rename 要注意,修改运行时配置的功能(如 CONFIG SET maxmemory)在生产上应该走配置管理而不是命令行。
常见误区与小结
- 误区:单实例开到 50GB 以上没问题。fork 超时 + 同步慢 + 恢复慢,三个代价叠加,10GB 以内是保险线。
- 误区:碎片率 > 1.5 直接重启。先开 activedefrag,等碎片率降下来再看。重启只是临时清空,数据重新写入后碎片还会回来。
- 误区:--bigkeys 跑一遍就够。它只取每个类型最大的,小 bigkey(5MB 的 Hash,30 万 field)可能漏掉。定期 RDB 离线分析才是全量审计。
- 误区:hotkey 打散后写一致性没问题。读随机副本降低了单实例压力,但写端要更新所有副本,存在短暂的不一致窗口。业务要能容忍秒级不一致。
- 误区:危险命令 rename 后万事大吉。rename 只是改了名字,如果有人知道新名字照样能执行。还要配合 ACL(Redis 6.0+)做用户级权限控制。
小结:bigkey 和 hotkey 是生产运维中最常见的两类问题——前者靠离线分析预防,后者靠本地缓存和打散逐层缓解。监控指标不在多,8 个核心指标 + 十万分段阈值就够了。下一篇会是 redis 系统学习系列的收尾:延迟排查案例集,从 P99 抖动到缓存雪崩复盘。
参考
参考:Redis 官方文档 SECURITY / redis-cli --bigkeys / evalsha 脚本渐进删除