Skip to content

生产运维:部署规格、监控与 bigkey/hotkey 治理

本文是 Redis 系统学习系列的 L3 实战篇。前置:30. 内存模型与编码切换。 学完可以配合面试题食用:10-big-key-problem-detection-mitigation11-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_memoryINFO memory1.0-1.5> 1.5 或 < 1.0
连接数connected_clients< 5000> 8000
阻塞客户端blocked_clients0> 0
latest_fork_usecINFO 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:0user: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 脚本渐进删除

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1