问题

Redis热点key问题如何解决?

答案

1. 核心概念

热点key指在短时间内被高频访问的Redis键,可能导致:

  • 单点压力:大量请求集中到存储该key的Redis节点,造成CPU、网络带宽瓶颈
  • 性能下降:单个key的并发读写超过Redis单实例处理能力(~10万QPS)
  • 集群倾斜:在Redis Cluster中,热点key所在slot的节点负载远高于其他节点

典型场景

  • 电商秒杀活动的商品详情
  • 微博明星热门微博
  • 突发热点新闻

2. 热点key的识别与监控

2.1 识别方法

方法一:Redis自带工具
# 使用redis-cli的hotkeys参数(需开启maxmemory-policy)
redis-cli --hotkeys

# 使用monitor命令实时监控(性能影响大,生产慎用)
redis-cli monitor | head -n 10000
方法二:客户端统计
/**
 * 基于客户端埋点统计热点key
 */
public class RedisHotkeyCollector {
    private final ConcurrentHashMap<String, AtomicLong> keyAccessCount = new ConcurrentHashMap<>();
    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    
    public RedisHotkeyCollector() {
        // 每分钟上报访问量Top10的key
        scheduler.scheduleAtFixedRate(() -> {
            List<Map.Entry<String, AtomicLong>> hotkeys = keyAccessCount.entrySet().stream()
                .sorted((a, b) -> Long.compare(b.getValue().get(), a.getValue().get()))
                .limit(10)
                .collect(Collectors.toList());
            
            // 上报到监控系统
            reportToMonitoring(hotkeys);
            keyAccessCount.clear();
        }, 1, 1, TimeUnit.MINUTES);
    }
    
    public void recordAccess(String key) {
        keyAccessCount.computeIfAbsent(key, k -> new AtomicLong()).incrementAndGet();
    }
}
方法三:代理层统计

使用Redis代理(如Twemproxy、Codis)的统计功能,或自研代理收集访问热点。


3. 解决方案与关键点

3.1 本地缓存(一级缓存)

原理:在应用服务器内存中缓存热点数据,减少对Redis的访问。

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

/**
 * 使用Guava Cache实现本地缓存
 */
public class LocalCacheService {
    // 本地缓存,容量1000,TTL 5秒
    private final LoadingCache<String, String> localCache = CacheBuilder.newBuilder()
        .maximumSize(1000)
        .expireAfterWrite(5, TimeUnit.SECONDS)
        .recordStats()  // 开启统计
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                return redisTemplate.opsForValue().get(key);
            }
        });
    
    public String get(String key) {
        try {
            return localCache.get(key);
        } catch (ExecutionException e) {
            // 降级逻辑
            return null;
        }
    }
}

适用场景

  • 读多写少的场景
  • 允许短暂(秒级)数据不一致

优化要点

  • TTL不宜过长,避免数据陈旧
  • 监控命中率,评估效果
  • 使用Caffeine等高性能缓存库

3.2 热点key分片(多副本)

原理:将热点key复制多份(加随机后缀),读请求随机路由到不同副本。

/**
 * 热点key分片策略
 */
public class HotkeyShardingService {
    private static final int SHARD_COUNT = 10;  // 分片数量
    private final Random random = new Random();
    
    /**
     * 写入时复制多份
     */
    public void set(String key, String value, long ttl) {
        for (int i = 0; i < SHARD_COUNT; i++) {
            String shardKey = key + "#" + i;
            redisTemplate.opsForValue().set(shardKey, value, ttl, TimeUnit.SECONDS);
        }
    }
    
    /**
     * 读取时随机选择一个分片
     */
    public String get(String key) {
        int shardIndex = random.nextInt(SHARD_COUNT);
        String shardKey = key + "#" + shardIndex;
        return redisTemplate.opsForValue().get(shardKey);
    }
}

适用场景

  • 读频繁、写不频繁
  • 能接受多次写入的开销

注意事项

  • 分片数量根据QPS评估(建议2-10个)
  • 更新时需同时更新所有分片
  • 可结合MQ异步更新降低写延迟

3.3 多级缓存架构

架构设计

客户端请求 
  ↓
Nginx本地缓存(OpenResty + Lua)
  ↓(miss)
应用本地缓存(Caffeine/Guava)
  ↓(miss)
Redis缓存
  ↓(miss)
数据库

Nginx层示例

-- OpenResty Lua脚本
local cache = ngx.shared.hot_cache
local key = ngx.var.arg_key

-- 尝试从Nginx共享内存获取
local value = cache:get(key)
if value then
    ngx.say(value)
    return
end

-- 回源到后端服务
local res = ngx.location.capture("/backend?key=" .. key)
if res.status == 200 then
    cache:set(key, res.body, 5)  -- 缓存5秒
    ngx.say(res.body)
end

优势

  • 多层拦截,极大降低Redis压力
  • Nginx层可承载百万级QPS

3.4 限流与降级

场景:极端热点(如突发事件),使用限流保护Redis。

import com.google.common.util.concurrent.RateLimiter;

/**
 * 热点key限流
 */
public class HotkeyRateLimiter {
    // 针对特定key限流,QPS=1000
    private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
    
    public String getWithRateLimit(String key) {
        RateLimiter limiter = limiters.computeIfAbsent(key, 
            k -> RateLimiter.create(1000.0));
        
        if (!limiter.tryAcquire(100, TimeUnit.MILLISECONDS)) {
            // 限流降级:返回默认值或提示
            return "系统繁忙,请稍后重试";
        }
        
        return redisTemplate.opsForValue().get(key);
    }
}

4. 分布式场景的考量

4.1 缓存一致性问题

本地缓存的一致性

  • 方案1:设置短TTL(3-5秒),允许短暂不一致
  • 方案2:使用消息队列(Kafka/Redis Pub/Sub)主动推送更新
/**
 * 基于Redis Pub/Sub的缓存失效通知
 */
@Component
public class CacheInvalidationListener {
    @Autowired
    private LoadingCache<String, String> localCache;
    
    @RedisListener(topics = "cache:invalidate")
    public void onMessage(String key) {
        localCache.invalidate(key);
        log.info("本地缓存失效: {}", key);
    }
}

4.2 集群环境注意事项

  • Redis Cluster:热点key会导致slot所在节点负载不均,考虑将热点数据独立部署到单独的Redis实例
  • 主从架构:读写分离,将热点读请求分散到多个从节点
  • 监控告警:设置QPS阈值告警,及时发现热点

4.3 性能优化

  • Pipeline批量操作:减少网络往返
  • 连接池调优:增加连接数上限(如Lettuce的maxTotal)
  • 序列化优化:使用高效序列化(Protobuf/Kryo替代Java原生)

5. 面试答题总结

标准回答模板

Redis热点key会导致单节点压力过大,解决方案包括:

  1. 本地缓存:使用Caffeine/Guava在应用层缓存热点数据,设置短TTL(3-5秒)
  2. key分片:将热点key复制多份(加随机后缀),读请求随机路由,分散压力
  3. 多级缓存:Nginx + 应用本地缓存 + Redis三层架构,逐层拦截
  4. 限流降级:对热点key设置QPS上限,超限返回默认值
  5. 监控识别:通过客户端埋点或代理层统计,实时发现热点key

实战选择

  • 临时热点(如突发新闻):本地缓存 + 限流
  • 可预期热点(如秒杀):提前预热 + 多级缓存 + key分片
  • 长期热点:独立部署Redis实例 + 读写分离

常见追问

  • 如何识别热点key? → 客户端统计、Redis hotkeys命令、代理层分析
  • 本地缓存的一致性如何保证? → 短TTL + 消息队列主动失效
  • key分片会带来什么问题? → 写放大(需更新所有副本)、复杂度增加
  • Nginx缓存和应用缓存区别? → Nginx在网关层,减少后端调用;应用缓存更灵活,可结合业务逻辑