问题
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会导致单节点压力过大,解决方案包括:
- 本地缓存:使用Caffeine/Guava在应用层缓存热点数据,设置短TTL(3-5秒)
- key分片:将热点key复制多份(加随机后缀),读请求随机路由,分散压力
- 多级缓存:Nginx + 应用本地缓存 + Redis三层架构,逐层拦截
- 限流降级:对热点key设置QPS上限,超限返回默认值
- 监控识别:通过客户端埋点或代理层统计,实时发现热点key
实战选择:
- 临时热点(如突发新闻):本地缓存 + 限流
- 可预期热点(如秒杀):提前预热 + 多级缓存 + key分片
- 长期热点:独立部署Redis实例 + 读写分离
常见追问:
- 如何识别热点key? → 客户端统计、Redis hotkeys命令、代理层分析
- 本地缓存的一致性如何保证? → 短TTL + 消息队列主动失效
- key分片会带来什么问题? → 写放大(需更新所有副本)、复杂度增加
- Nginx缓存和应用缓存区别? → Nginx在网关层,减少后端调用;应用缓存更灵活,可结合业务逻辑