跳到主要内容

热点 Key 处理

问题

什么是热点 Key?热点 Key 有什么问题?如何发现和处理?

答案

一、什么是热点 Key

热点 Key 是指在极短时间内被大量并发访问的缓存 Key。比如:

  • 秒杀商品的库存 Key
  • 热门微博/话题的详情 Key
  • 明星出轨相关的搜索 Key
  • 首页推荐列表 Key

二、热点 Key 的危害

在 Redis Cluster 中,一个 Key 只属于一个 Slot(一个节点),所以热点 Key 会导致单节点过载,即使集群有很多节点也无济于事。

三、热点 Key 发现

方法一:Redis 命令

# redis-cli 的 hotkeys 功能(需要开启 maxmemory-policy = allkeys-lfu)
redis-cli --hotkeys

# MONITOR 命令(生产环境慎用,性能开销大)
redis-cli MONITOR | head -1000

方法二:业务侧统计

/**
* 基于滑动窗口的热点检测
*/
@Component
public class HotKeyDetector {

// Key -> 滑动窗口计数器
private final ConcurrentHashMap<String, AtomicLong> counterMap =
new ConcurrentHashMap<>();

// 10秒内访问超过 1000 次即为热点 Key
private static final long THRESHOLD = 1000;
private static final long WINDOW_MS = 10_000;

/**
* 记录访问并判断是否为热点
*/
public boolean isHotKey(String key) {
AtomicLong counter = counterMap.computeIfAbsent(key, k -> new AtomicLong(0));
long count = counter.incrementAndGet();
return count > THRESHOLD;
}

// 定时重置计数器
@Scheduled(fixedRate = 10_000)
public void resetCounters() {
counterMap.clear();
}
}

方法三:开源工具

  • JD HotKey(京东开源):客户端 SDK + Worker + Etcd 探测热点 Key
  • Redis 内置 LFUOBJECT FREQ <key> 查看访问频率

四、热点 Key 解决方案

方案一:本地缓存(最常用)

将热点数据缓存到应用的 JVM 内存中,不经过 Redis。

@Service
public class HotKeyCacheService {

// L1 本地缓存:容量小,TTL 短
private final Cache<String, Object> localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(3, TimeUnit.SECONDS) // 3秒过期
.build();

@Autowired
private RedisTemplate<String, Object> redis;

@Autowired
private HotKeyDetector hotKeyDetector;

public Object get(String key) {
// 1. 热点 Key 走本地缓存
if (hotKeyDetector.isHotKey(key)) {
Object value = localCache.getIfPresent(key);
if (value != null) {
return value;
}
}

// 2. 查 Redis
Object value = redis.opsForValue().get(key);
if (value != null) {
// 热点 Key 回填本地缓存
if (hotKeyDetector.isHotKey(key)) {
localCache.put(key, value);
}
return value;
}

// 3. 查数据库...
return loadFromDB(key);
}
}
JD HotKey 方案

京东的 HotKey 框架通过 Worker 集群实时探测热点 Key,一旦识别,自动将热点数据推送到各应用节点的本地缓存。应用代码几乎无需修改:

// 使用 JD HotKey,自动识别热点并本地缓存
Object value = JdHotKeyStore.getValue(key);

方案二:Key 分片(分散热点)

将一个热点 Key 拆分成多个子 Key,分散到不同 Redis 节点。

/**
* 热点 Key 分片:将一个 Key 拆分成 N 个
*/
public class KeySharding {

private static final int SHARD_COUNT = 10; // 分片数

/**
* 写入时:随机写入一个分片
*/
public void set(String key, Object value) {
for (int i = 0; i < SHARD_COUNT; i++) {
String shardKey = key + ":shard:" + i;
redis.opsForValue().set(shardKey, value, 30, TimeUnit.MINUTES);
}
}

/**
* 读取时:随机读取一个分片
*/
public Object get(String key) {
int shard = ThreadLocalRandom.current().nextInt(SHARD_COUNT);
String shardKey = key + ":shard:" + shard;
return redis.opsForValue().get(shardKey);
}
}

原理:原来所有请求访问同一个 Key(同一个 Redis 节点),现在分成 10 个 Key,分布到 10 个不同的 Slot → 不同的节点,压力分散。

注意事项
  • Key 分片只适合读多写少的热点数据
  • 写入时需要同步更新所有分片
  • 更新分片之间存在短暂不一致窗口

方案三:读写分离 + 从节点读

利用 Redis 主从复制,让读请求分散到多个从节点:

// Lettuce 配置从节点读取
@Bean
public LettuceClientConfiguration lettuceConfig() {
return LettuceClientConfiguration.builder()
.readFrom(ReadFrom.REPLICA_PREFERRED) // 优先从从节点读取
.build();
}

适合 Redis 大版本中支持的读写分离场景。但要注意主从延迟可能导致从节点读到旧数据。

方案四:限流 + 降级

对热点 Key 的访问进行限流,超出限流阈值的请求直接返回降级数据。

// Guava RateLimiter 限流
private final RateLimiter rateLimiter = RateLimiter.create(10000); // 每秒 10000 次

public Object getWithRateLimit(String key) {
if (!rateLimiter.tryAcquire()) {
return getDefaultValue(key); // 降级:返回兜底数据
}
return cacheService.get(key);
}

五、方案对比

方案效果复杂度一致性适用场景
本地缓存⭐⭐⭐⭐⭐秒级延迟通用方案,首选
Key 分片⭐⭐⭐⭐写入延迟读多写少
从节点读⭐⭐⭐主从延迟Redis 集群
限流降级⭐⭐⭐无影响兜底方案
最佳实践

组合使用:本地缓存 + 限流降级。本地缓存消化大部分请求,限流保护 Redis 和数据库。

六、热点 Key 在秒杀场景中的应用

秒杀中库存 Key 是典型的热点 Key,但它是写热点(频繁扣减),不能简单用本地缓存。

/**
* 秒杀库存扣减方案:Redis + Lua 原子操作
*/
public boolean deductStock(String itemId) {
String key = "stock:" + itemId;
String luaScript =
"local stock = tonumber(redis.call('get', KEYS[1]))\n" +
"if stock and stock > 0 then\n" +
" redis.call('decr', KEYS[1])\n" +
" return 1\n" +
"end\n" +
"return 0";

Long result = redis.execute(
new DefaultRedisScript<>(luaScript, Long.class),
List.of(key));
return result != null && result == 1;
}

秒杀场景的热点 Key 方案:

  1. 预热:提前将库存加载到 Redis
  2. Lua 脚本原子扣减:避免超卖
  3. 本地缓存售罄标记:库存为 0 后在本地标记,后续请求直接返回
  4. 消息队列异步:将扣减请求投递到 MQ,削峰填谷

常见面试问题

Q1: 如何发现线上的热点 Key?

答案

  1. Redis 监控redis-cli --hotkeys(需要 LFU 淘汰策略)
  2. 代理层统计:在 Redis 代理(如 Twemproxy、Codis)中统计 Key 访问频率
  3. 客户端 SDK:在 Redis 客户端封装统计逻辑
  4. JD HotKey:开源的热点探测框架,实时探测 + 自动缓存
  5. 业务预判:秒杀、热搜等场景可以提前预判热点

Q2: 热点 Key 和大 Key 有什么区别?

答案

对比热点 Key大 Key
问题访问频率过高单个 Value 体积过大
危害单节点 CPU 打满网络带宽打满,阻塞其他操作
示例秒杀商品库存10MB 的用户列表 JSON
解决本地缓存、Key 分片拆分 Value、压缩

两个问题可能同时出现(又热又大),需要同时处理。

Q3: 本地缓存和 Redis 数据不一致怎么办?

答案

  1. 设置短 TTL:本地缓存 3~10 秒过期,定期从 Redis 刷新
  2. 广播失效:数据更新时通过 Redis Pub/Sub 或 MQ 通知所有节点清除本地缓存
  3. 业务容忍:大多数热点场景可以容忍秒级不一致(如商品详情)

详见多级缓存架构

Q4: Redis Cluster 中热点 Key 为什么不能自动负载均衡?

答案

Redis Cluster 按 Key 的 CRC16 哈希值分配到 16384 个 Slot 中,每个 Key 固定属于一个 Slot,一个 Slot 固定在一个节点上。所以:

  • 热点 Key 的所有请求都打到同一个节点
  • Redis 不会自动迁移热点 Key 到其他节点
  • 增加 Slot 数量无法解决问题(Key 还是只在一个 Slot)

解决思路:要么在客户端分散请求(本地缓存、Key 分片),要么增加从节点走读写分离。

Q5: JD HotKey 的原理是什么?

答案

JD HotKey 架构:

  1. 客户端 SDK:嵌入应用,统计每个 Key 的访问频率
  2. Worker 集群:接收各节点上报的访问统计,聚合判断热点
  3. Etcd:存储热点 Key 规则和配置
  4. 探测到热点后:Worker 通过推送通知所有客户端 SDK,SDK 自动将热点 Key 缓存到本地 JVM

整个过程对业务代码透明,只需调用 JdHotKeyStore.getValue(key) 即可。

相关链接