欢迎来到窥视未来的博客

https://github.com/lwx57280 https://gitee.com/li_VillageHead

深夜被叫醒两次:一次Redis热点Key打满带宽,一次大Value拖垮主从,我把这两个问题彻底讲清楚

兄弟们,你们有没有试过一个晚上被叫醒两次的经历?第一次是凌晨1点,Redis CPU突然飙到95%,所有接口超时。第二次是凌晨3点,主从同步延迟从几毫秒飙升到几十秒,从库一直追不上。

第一次查出来是热点Key,第二次查出来是大Value。这两个“杀手”虽然都叫“Redis性能问题”,但症状完全不同,排查思路也天差地别。今天就把这两次翻车经历完整复盘出来。

一、第一次事故:热点Key打爆CPU

1.1 现场还原

凌晨1点,运维群炸了:“Redis CPU 95%!服务全部超时!”

登录服务器后,redis-cli --stat 的输出触目惊心:

------- data ------ --------------------- load -------------------- - child -
keys       mem      clients blocked requests            connections
100234     8.2G     1500    0       4823941 (+0)       1501
100235     8.2G     1500    0       5234982 (+411041)  1501
100235     8.2G     1500    0       5782391 (+547409)  1501

QPS从平时的1万瞬间飙到5万多,单次命令的响应时间从平均0.2ms飙升到50ms。

1.2 根因分析

用 redis-cli --bigkeys 分析发现,所有流量都打在了同一个Key上:

> redis-cli --bigkeys
...
Biggest string found 'hot:product:10086' has 256 bytes

这个Key存储的是某个爆款商品的信息,由于营销活动,所有用户都在疯狂访问它。QPS集中在一个Key上,导致负责该Key的Redis节点CPU被瞬间打满。

mermaid-1785768603683

 

1.3 热点Key的“四宗罪”

 
故障表现原因影响程度
CPU瞬时飙高 单Key QPS过高,Redis主线程被占满 ⚠️⚠️⚠️
网络带宽打满 热点Value频繁传输,占用带宽 ⚠️⚠️
缓存穿透/雪崩 热点Key过期瞬间,大量请求直击DB ⚠️⚠️⚠️
集群热点倾斜 Cluster模式下,单一节点负载远高于其他节点 ⚠️⚠️⚠️

1.4 排查命令

# 查看当前哪些Key被频繁访问(需要开启monitor,生产慎用)
redis-cli monitor | head -100

# 统计各Key的访问频率(使用redis-cli --hotkeys)
redis-cli --hotkeys

# 查看当前QPS
redis-cli INFO stats | grep instantaneous_ops_per_sec

# 查看CPU使用率
redis-cli INFO cpu

1.5 热点Key解决方案

mermaid-1785768708169

 方案一:本地缓存(Caffeine)+ 一致性Hash

@Component
public class HotKeyCache {
    
    private final Cache<String, Object> localCache = Caffeine.newBuilder()
        .maximumSize(10000)           // 最多缓存1万条
        .expireAfterWrite(1, TimeUnit.MINUTES)  // 1分钟过期
        .build();
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    public Object getData(String key) {
        // L1: 本地缓存
        Object data = localCache.getIfPresent(key);
        if (data != null) {
            return data;
        }
        
        // L2: Redis
        data = redisTemplate.opsForValue().get(key);
        if (data != null) {
            localCache.put(key, data);
            return data;
        }
        
        // L3: DB
        data = queryFromDB(key);
        if (data != null) {
            redisTemplate.opsForValue().set(key, data, 1, TimeUnit.HOURS);
            localCache.put(key, data);
        }
        return data;
    }
}

方案二:Key拆分 + 随机后缀

热点Key可以通过增加随机后缀分散流量:

public String getHotProduct(Long productId) {
    // 将热点Key拆分成N个副本,随机访问
    int N = 10;
    int slot = ThreadLocalRandom.current().nextInt(N);
    String key = "hot:product:" + productId + ":" + slot;
    return redisTemplate.opsForValue().get(key);
}

每次写入时,同时更新所有副本:

public void setHotProduct(Long productId, Object data) {
    for (int i = 0; i < 10; i++) {
        String key = "hot:product:" + productId + ":" + i;
        redisTemplate.opsForValue().set(key, data, 1, TimeUnit.HOURS);
    }
}

二、第二次事故:大Value拖垮主从

2.1 现场还原

凌晨3点,报警再次响起:“主从延迟超过60秒,从库数据严重落后!”

> redis-cli INFO replication
# Replication
role:master
connected_slaves:1
slave0:ip=192.168.1.2,port=6379,state=online,offset=4823941,lag=60

lag已经60秒了。主库写入正常,但从库迟迟追不上。

2.2 根因分析

检查后发现某个Key的Value达到了8MB——这是一个用户画像的JSON,包含了该用户的所有行为轨迹。

# 查看大Key
redis-cli --bigkeys

# 最大Key:user:profile:12345,Value大小:8.2MB

每次更新这个Key时,8MB的数据需要:

  1. 主库写入AOF(8MB)

  2. 传输到从库(8MB网络流量)

  3. 从库加载(8MB内存操作)

  4. 写入RDB(8MB)

一次性传输8MB,在网络条件一般的情况下,几十毫秒就过去了。频繁更新时,从库一直被大Value“卡住”,无法追平。

2.3 大Value的“五宗罪”

 
故障表现原因影响程度
内存碎片化严重 大Value频繁分配/释放,jemalloc无法有效回收 ⚠️⚠️
慢查询/阻塞 Redis单线程处理大Value,读写耗时过长 ⚠️⚠️⚠️
AOF/RDB放大 大Value导致AOF重写和RDB生成耗时剧增 ⚠️⚠️
主从复制延迟 大Value传输占用带宽,从库回放慢 ⚠️⚠️⚠️
网络抖动/超时 大Value传输时间长,易超时重传 ⚠️⚠️

mermaid-1785768973518

2.4 大Value排查命令

# 查找大Key
redis-cli --bigkeys

# 查看单个Key的内存占用(Redis 4.0+)
MEMORY USAGE user:profile:12345

# 查看内存碎片率
redis-cli INFO memory | grep mem_fragmentation_ratio

2.5 大Value解决方案

方案一:大Value拆分

// 将一个大JSON拆分成多个小Key
public void saveUserProfile(Long userId, UserProfile profile) {
    // 原方案:一个Key存所有
    // redis.set("user:profile:" + userId, JSON.toJSONString(profile));
    
    // 新方案:按模块拆分
    redis.hset("user:profile:" + userId, "base", JSON.toJSONString(profile.getBase()));
    redis.hset("user:profile:" + userId, "behavior", JSON.toJSONString(profile.getBehavior()));
    redis.hset("user:profile:" + userId, "preference", JSON.toJSONString(profile.getPreference()));
}

方案二:压缩存储

public void saveCompressed(String key, Object data) {
    byte[] json = JSON.toJSONBytes(data);
    // 使用GZIP压缩
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    try (GZIPOutputStream gzip = new GZIPOutputStream(baos)) {
        gzip.write(json);
    }
    redisTemplate.opsForValue().set(key, baos.toByteArray());
}

压缩率通常在70%-90%,8MB的数据压缩后可能只有1-2MB。

方案三:冷热分离

 
数据类型存储方式大小
高频访问字段 Redis Hash(热) < 1KB
中频访问字段 Redis Hash(温) 1-10KB
低频访问字段 MySQL/对象存储(冷) > 10KB

将用户画像按访问频率分层存储,减少Redis中大Value的存储。

三、热点Key vs 大Value:快速诊断对比

 
故障现象热点Key大Value
CPU飙升 ✅ 高QPS导致 ❌ 低频大Value不会,但高频会
带宽打满 ✅ 高频小包 ✅ 低频大包
主从延迟 ❌ 小Value影响小 ✅ 大Value传输慢
慢查询 ❌ 通常不慢 ✅ 读写耗时
内存碎片 ❌ 影响小 ✅ 频繁分配释放
集群倾斜 ✅ 流量倾斜 ✅ 存储倾斜

mermaid-1785769176004

四、生产环境避坑清单

4.1 预防措施

 
措施说明
合理设计Key 避免单个Key过大,建议单个Value < 10KB
设置合理TTL 热点Key设置永不过期+后台刷新,避免缓存击穿
开启大Key监控 使用redis-cli --bigkeys定期巡检
配置告警 CPU > 80%、主从延迟 > 10s 立即告警
客户端配置 设置连接超时、读取超时,避免大Value拖垮连接

 

4.2 监控指标

# 日常巡检命令集
redis-cli INFO stats | grep instantaneous_ops_per_sec   # QPS
redis-cli INFO memory | grep mem_fragmentation_ratio    # 碎片率
redis-cli INFO replication | grep lag                   # 主从延迟
redis-cli --bigkeys -i 0.1                              # 扫描大Key
redis-cli --hotkeys                                     # 扫描热点Key(4.0+)

4.3 应急处理

 
故障应急措施根治方案
热点Key打爆CPU 临时增加从节点分担读 本地缓存 + Key拆分
大Value导致主从延迟 暂停该Key的更新,先让从库追上 Value拆分 + 压缩

五、避坑总结

  1. 热点Key和大Value是两类不同的问题——诊断方向完全不同,看CPU还是看流量是关键区分点

  2. 热点Key靠“散”:本地缓存、Key拆分、读写分离,把流量分散出去

  3. 大Value靠“拆”:拆分存储、压缩存储、冷热分离,把数据打散

  4. 定期巡检是底线:--bigkeys和--hotkeys要纳入日常巡检

  5. 监控告警是生命线:没有告警的故障只会越来越严重

兄弟们,你们在生产环境中遇到过热点Key还是大Value的问题?是怎么发现和解决的?评论区聊聊!

posted on 2026-08-03 23:01  k8s-Mango  阅读(32)  评论(0)    收藏  举报

导航