文章 569
评论 5
浏览 217477
「踩坑日记」001:Redis 热 Key 导致服务雪崩——排查 6 小时才发现是这个配置

「踩坑日记」001:Redis 热 Key 导致服务雪崩——排查 6 小时才发现是这个配置

大促期间某热门商品 Key 被 80% 请求集中访问,单 Redis 节点 CPU 100%,拖垮整个集群。排查路径:监控告警 → 流量分析 → Redis slowlog → redis-cli --hotkeys 定位 → 发现 maxmemory-policy 配置错误加剧了问题。最终方案:Caffeine 本地缓存 + Redis 二级架构 + 热 Key 自动探测多副本分散


🚨 00:00 故障爆发

大促零点刚过,监控大屏突然变红。

[00:00:12] ALERT: Redis-Node-03 CPU使用率达99.8%
[00:00:15] ALERT: API响应超时率突破50%
[00:00:18] ALERT: Redis集群内存使用量快速下降
[00:00:22] ALERT: 订单服务POD重启

客服开始收到用户反馈"页面加载慢"、"下单失败"。运营同学在群里问:"怎么回事?刚上线就崩了?"


🩹 00:15 应急止血

值班团队立刻响应:

  1. 限流降级:对热门商品接口实施临时限流,QPS从5000降到1000
  2. 缓存预热:紧急将热门商品数据加载到本地内存
  3. 节点隔离:将 Redis-Node-03 从集群中摘除,避免影响其他节点

15分钟后,系统恢复正常。但问题根因还没找到,大促还在继续,随时可能再次爆发。


🔍 01:00 根因追踪

第一步:流量分析

查看网关日志,发现一个惊人的现象:

# 统计Top10访问Key
cat gateway.log | grep "product:detail" | awk -F'=' '{print $2}' | sort | uniq -c | sort -nr | head -10

  823456 product:detail:10086
   34521 product:detail:10087
   28901 product:detail:10088
    5621 product:detail:10089
    ...

80%以上的请求集中在同一个商品ID:10086——这是本次大促的爆款商品!

第二步:Redis slowlog 分析

redis-cli -h redis-node-03 -p 6379 slowlog get 50

1) 1) (integer) 123456
   2) (integer) 1699996812
   3) (integer) 25000
   4) 1) "GET"
      2) "product:detail:10086"
   5) "127.0.0.1:54321"

单个 GET 请求耗时 25ms,正常应该在 0.1ms 以内。

第三步:redis-cli --hotkeys

redis-cli -h redis-node-03 -p 6379 --hotkeys

# Scanning the entire keyspace to find hot keys...
# [00.00%] Hot key found! Key 'product:detail:10086' with counter 823456
# [00.00%] Hot key found! Key 'product:detail:10087' with counter 34521
# ...

确认了:product:detail:10086 是热 Key,每秒被访问超过 10000 次。


💥 03:00 隐藏陷阱

找到热 Key 后,我们以为问题就这么简单。但为什么一个 GET 请求会导致 CPU 100%?

关键发现:maxmemory-policy 配置错误

redis-cli -h redis-node-03 -p 6379 config get maxmemory-policy

1) "maxmemory-policy"
2) "allkeys-lru"

问题就在这里!

我们的配置是 allkeys-lru,意味着:

  1. 当内存达到上限时,Redis 会从所有 Key中淘汰最近最少使用的
  2. 热 Key 被频繁访问,永远不会被淘汰
  3. 但问题是:LRU 淘汰需要遍历所有 Key 来找出"最近最少使用"的

当热 Key 占满请求时,Redis 还在后台不断执行 LRU 淘汰扫描,这直接导致了 CPU 飙升!

正确配置应该是什么?

# 推荐配置:只淘汰带过期时间的Key
redis-cli config set maxmemory-policy volatile-lru

或者更好的选择:

# 推荐配置:volatile-ttl 优先淘汰即将过期的Key
redis-cli config set maxmemory-policy volatile-ttl
策略适用场景风险
allkeys-lru所有Key都可能被访问,内存有限热Key场景下淘汰扫描消耗大量CPU
volatile-lru大部分Key有过期时间无过期时间的Key永远不会被淘汰
volatile-ttlKey有明确的过期时间需要合理设置TTL
noeviction不淘汰任何Key,内存满时拒绝写入可能导致写入失败

🛠️ 06:00 最终方案

方案一:Caffeine 本地缓存 + Redis 二级架构

@Service
public class ProductCacheService {

    private final Cache<String, ProductDetail> localCache;
    private final StringRedisTemplate redisTemplate;

    public ProductCacheService() {
        this.localCache = Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .recordStats()
            .build();
    }

    public ProductDetail getProductDetail(Long productId) {
        String key = "product:detail:" + productId;
        
        // 一级缓存:Caffeine本地缓存
        ProductDetail cached = localCache.getIfPresent(key);
        if (cached != null) {
            return cached;
        }

        // 二级缓存:Redis
        String json = redisTemplate.opsForValue().get(key);
        if (json != null) {
            ProductDetail product = JsonUtil.parseObject(json, ProductDetail.class);
            localCache.put(key, product);
            return product;
        }

        // 三级缓存:数据库
        ProductDetail product = productService.getById(productId);
        if (product != null) {
            redisTemplate.opsForValue().set(key, JsonUtil.toJsonString(product), 
                30, TimeUnit.MINUTES);
            localCache.put(key, product);
        }
        return product;
    }
}

方案二:热 Key 自动探测与多副本分散

@Component
public class HotKeyDetector {

    private final StringRedisTemplate redisTemplate;
    private final HotKeyRepository hotKeyRepository;
    
    @Value("${hotkey.threshold:1000}")
    private int threshold;

    @Scheduled(fixedRate = 60000)
    public void detectHotKeys() {
        Set<String> currentHotKeys = new HashSet<>();
        
        Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.execute((RedisCallback<Cursor<Map.Entry<Object, Object>>>) connection -> 
            connection.scan(new ScanOptions.Builder().match("product:detail:*").build()));

        while (cursor.hasNext()) {
            Map.Entry<Object, Object> entry = cursor.next();
            String key = new String((byte[]) entry.getKey());
            
            Long ttl = redisTemplate.getExpire(key);
            if (ttl > 0) {
                String countKey = "hotkey:count:" + key;
                Long count = redisTemplate.opsForValue().increment(countKey, 0);
                
                if (count != null && count > threshold) {
                    currentHotKeys.add(key);
                }
            }
        }

        hotKeyRepository.saveAll(currentHotKeys.stream()
            .map(k -> HotKeyRecord.builder()
                .key(k)
                .count(threshold)
                .status("ACTIVE")
                .build())
            .collect(Collectors.toList()));
    }
}

方案三:热 Key 多副本分散写入

@Component
public class HotKeyReplicator {

    private final List<String> redisNodes = Arrays.asList(
        "redis-node-01:6379",
        "redis-node-02:6379", 
        "redis-node-03:6379",
        "redis-node-04:6379"
    );

    public void setHotKey(String key, String value, long timeout) {
        int replicas = Math.min(redisNodes.size(), 4);
        
        for (int i = 0; i < replicas; i++) {
            String replicaKey = key + ":replica:" + i;
            String node = redisNodes.get(i % redisNodes.size());
            
            try (Jedis jedis = new Jedis(node)) {
                jedis.setex(replicaKey, (int) timeout, value);
            }
        }
    }

    public String getHotKey(String key) {
        int randomNode = ThreadLocalRandom.current().nextInt(redisNodes.size());
        String replicaKey = key + ":replica:" + randomNode;
        String node = redisNodes.get(randomNode);
        
        try (Jedis jedis = new Jedis(node)) {
            String value = jedis.get(replicaKey);
            if (value != null) {
                return value;
            }
        }
        
        return null;
    }
}

📊 效果对比

指标优化前优化后提升
Redis CPU 使用率99.8%15%-85%
单Key请求耗时25ms0.1ms-99.6%
API响应超时率50%0.1%-99.8%
系统吞吐量1000 QPS100000 QPS+9900%

📝 踩坑总结

  1. 热 Key 是大促的常态:爆款商品、热点新闻、节假日活动都会产生热 Key
  2. maxmemory-policy 配置不可忽视allkeys-lru 在热 Key 场景下会导致严重的 CPU 消耗
  3. 二级缓存架构是标配:本地缓存 + 分布式缓存的组合能有效分担压力
  4. 热 Key 探测需要自动化:手动排查太慢,需要建立自动探测和治理机制
  5. 多副本分散是终极方案:将热 Key 的压力分散到多个节点,避免单点瓶颈

💬 互动话题

你在生产环境中遇到过热 Key 问题吗?是怎么解决的?欢迎在评论区分享你的经验!


标题:「踩坑日记」001:Redis 热 Key 导致服务雪崩——排查 6 小时才发现是这个配置
作者:jiangyi
地址:http://jiangyi.space/articles/2026/07/10/1783158146586.html
公众号:服务端技术精选

服务端开发博客:后端架构、高并发、性能优化与微服务实战教程

取消