大促避坑指南:Redis CPU 100% 热Key/大Key问题定位与实战解决
前言
每年的618、双11大促,对技术人来说都是一场"大考"。去年618当晚,突然收到告警:核心Redis集群CPU使用率100%! 紧接着,业务侧反馈"购物车加载缓慢"、“下单超时”。
事后复盘,罪魁祸首就是热Key和大Key。本文将结合这次实战经验,深入剖析:
- 如何快速定位热Key和大Key?
- 有哪些行之有效的处理方案?
- 如何设计架构防患于未然?
一、场景复现:大促下的Redis之痛
1.1 现象描述
大促期间,流量峰值达到平时的50倍。Redis集群出现以下症状:
- CPU使用率:从30%飙升至100%
- 慢查询日志:出现大量超过1秒的命令
- 客户端异常:连接超时、Read Timeout
- 业务影响:涉及该Redis的业务全部响应缓慢
1.2 根本原因分析
热Key(Hot Key):某一时刻,某个key被海量并发访问。
- 例如:一个爆款商品的详情页key,瞬间被千万用户请求。
- 后果:单节点CPU被打满,该节点上的所有key都受影响。
大Key(Big Key):key对应的value过大。
- 例如:一个Hash结构存储了某个用户的全部好友关系,包含10万条数据。
- 后果:操作耗时增加,阻塞后续命令;网络传输压力大。
二、快速定位:如何揪出元凶?
大促期间分秒必争,定位问题必须快、准、狠。以下是我总结的"三阶定位法"。
2.1 第一阶:应急排查命令
方法一:redis-cli --hotkeys(推荐,但需开启淘汰策略)
Redis 4.0+提供了–hotkeys参数,可以分析热key。
$ redis-cli -h r-123.redis.rds.aliyuncs.com --hotkeys
# 输出示例
-------- summary -------
hot keys found (with 'object idletime' field代表key的空闲时间,越小越热)
1) "product:123456" - 热度: 1000次/秒
2) "activity:seckill" - 热度: 800次/秒
注意: 该命令依赖maxmemory-policy配置(如allkeys-lru),因为Redis需要维护LFU/LRU信息。如果没开启,可能无法使用。
方法二:redis-cli monitor(谨慎使用)
monitor命令可以实时打印Redis执行的所有命令。
$ redis-cli monitor
1698372617.382138 [0 10.0.0.1:54321] "GET" "product:123456"
1698372617.382238 [0 10.0.0.2:54322] "GET" "product:123456"
1698372617.382318 [0 10.0.0.3:54323] "GET" "product:123456"
⚠️ 警告: monitor在高并发下会成倍消耗CPU,生产环境慎用!通常只能在短时间、低峰期使用。
方法三:redis-cli --bigkeys(定位大Key)
Redis自带的大Key扫描工具。
$ redis-cli --bigkeys
# 输出示例
-------- summary -------
Sampled 1000000 keys in the keyspace!
Total key length in bytes is 25000000 (avg len 25.00)
Biggest string found "product:999" has 10485760 bytes
Biggest hash found "user:123:friends" has 100000 fields
Biggest list found "news:list" has 50000 items
2.2 第二阶:客户端侧统计
生产环境不建议直接在Redis上跑命令,更好的方式是在客户端(如Jedis、Lettuce)侧统计。
Lettuce热Key统计示例:
@Configuration
public class RedisConfig {
@Bean
public RedisClient redisClient() {
RedisClient redisClient = RedisClient.create("redis://localhost");
// 注册命令延迟事件监听
redisClient.addListener(new RedisCommandListener() {
@Override
public void onCommand(RedisCommand command, CommandTimedOutException e) {
String key = command.getArg(0); // 获取key
countHotKey(key); // 自定义统计逻辑
}
});
return redisClient;
}
private void countHotKey(String key) {
// 使用滑动窗口或时间轮统计key的访问频率
// 超过阈值(如1000次/秒)则记录告警
}
}
2.3 第三阶:监控平台告警
完善的监控体系应该主动告警,而不是等我们去查。
关键监控指标:
- CPU使用率:>80% 告警
- QPS峰值:同比突增
- 慢查询数:>10个/秒
- 网络IO:>100MB/s
三、热Key处理方案流程图
定位到热Key后,如何应对?下面是三种主流方案的决策流程:
四、深度实战:热Key解决方案
4.1 方案一:本地缓存(多级缓存)
这是应对热Key最有效的方案。在应用服务器JVM内缓存热点数据,彻底杜绝Redis压力。
架构图:
代码实现(Caffeine + Redis):
@Service
public class ProductService {
// 本地缓存:最大1万条,过期10秒
private final Cache<String, Product> localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.SECONDS)
.recordStats()
.build();
@Autowired
private StringRedisTemplate redisTemplate;
public Product getProduct(String productId) {
String key = "product:" + productId;
// 1. 查本地缓存
Product product = localCache.getIfPresent(key);
if (product != null) {
return product;
}
// 2. 查Redis(可以用分布式锁防止缓存击穿)
String json = redisTemplate.opsForValue().get(key);
if (StringUtils.hasText(json)) {
product = JSON.parseObject(json, Product.class);
localCache.put(key, product); // 回填本地缓存
return product;
}
// 3. 查数据库
product = queryFromDB(productId);
// 4. 回填Redis和本地缓存
redisTemplate.opsForValue().set(key, JSON.toJSONString(product), 1, TimeUnit.HOURS);
localCache.put(key, product);
return product;
}
}
优点: 抗热点效果极佳,QPS可达百万级。
缺点: 存在数据一致性问题(缓存延迟),本地缓存占用JVM内存。
4.2 方案二:热Key拆分
将单个热Key拆分为多个子Key,分散到不同Redis节点。
拆分逻辑:
public class HotKeySharding {
// 原Key:product:123456
// 拆分为:product:123456:0, product:123456:1, ... product:123456:9
private static final int SHARD_COUNT = 10;
public Product getProductSharded(String productId) {
// 随机选择一个分片
int shard = ThreadLocalRandom.current().nextInt(SHARD_COUNT);
String key = "product:" + productId + ":" + shard;
// 从Redis读取
String json = redisTemplate.opsForValue().get(key);
if (json == null) {
// 缓存失效时,需要重建所有分片或使用分布式锁
rebuildShards(productId);
}
return JSON.parseObject(json, Product.class);
}
private void rebuildShards(String productId) {
// 注意:需要加锁防止并发重建
Product product = queryFromDB(productId);
String json = JSON.toJSONString(product);
// 重建所有分片
for (int i = 0; i < SHARD_COUNT; i++) {
String key = "product:" + productId + ":" + i;
redisTemplate.opsForValue().set(key, json, 1, TimeUnit.HOURS);
}
}
}
优点: 将热点打散到多个节点,充分利用集群资源。
缺点: 需要修改客户端代码,缓存重建逻辑复杂。
4.3 方案三:读写分离
对于读多写少的场景,可以通过增加从节点来分摊读流量。
@Configuration
public class RedisReadWriteConfig {
@Bean
public LettuceClientConfiguration clientConfiguration() {
return LettuceClientConfiguration.builder()
.readFrom(ReadFrom.REPLICA_PREFERRED) // 优先读从节点
.build();
}
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStaticMasterReplicaConfiguration config =
new RedisStaticMasterReplicaConfiguration("master-host", 6379);
config.addNode("replica1-host", 6379);
config.addNode("replica2-host", 6379);
return new LettuceConnectionFactory(config, clientConfiguration());
}
}
五、大Key处理方案
5.1 大Key的危害
- 阻塞后续命令:Redis单线程模型,操作大Key耗时较长。
- 网络拥塞:传输大数据包占用带宽。
- 内存不均:集群模式下数据倾斜。
5.2 拆分数据结构
案例: 一个Hash存储了用户的所有粉丝(10万条)。
优化前:
key: user:123:fans (Hash)
field: fan_1, value: {…}
field: fan_2, value: {…}
…
field: fan_100000, value: {…}
优化后: 按粉丝ID哈希取模拆分
key: user:123:fans:0 (Hash,存储粉丝ID尾号为0的)
key: user:123:fans:1 (Hash,存储粉丝ID尾号为1的)
…
key: user:123:fans:9 (Hash)
5.3 异步删除(unlink)
删除大Key时,使用unlink代替del。
// 错误示范:阻塞删除
redisTemplate.delete("user:123:fans");
// 正确示范:异步删除
redisTemplate.unlink("user:123:fans");
unlink 命令会在后台回收内存,主线程可以继续处理其他命令。
5.4 渐进式扫描删除
如果必须删除一个大集合,可以分批删除。
-- Lua脚本:分批删除Hash中的字段
local cursor = 0
local count = 0
repeat
-- 每次扫描100个field
local result = redis.call('HSCAN', KEYS[1], cursor, 'COUNT', 100)
cursor = tonumber(result[1])
local fields = result[2]
-- 删除这些field
for i=1, #fields, 2 do
redis.call('HDEL', KEYS[1], fields[i])
count = count + 1
end
until cursor == 0
return count
六、预防体系建设
与其等故障发生再处理,不如建立完善的预防机制。
6.1 代码审查规范
- 禁止:直接存储大对象(如用户的所有粉丝列表)。
- 禁止:使用keys * 命令。
- 提醒:合理设置过期时间。
- 提醒:使用压缩算法存储大文本。
6.2 压测常态化
大促前必须进行全链路压测,模拟真实流量:
- 单Key QPS压测
- 大Key性能压测
- 故障演练(如节点宕机)
6.3 自动化治理平台
七、总结
| 问题类型 | 定位手段 | 处理方案 | 预防措施 |
|---|---|---|---|
| 热Key | --hotkeys、客户端统计 |
本地缓存、Key拆分、读写分离 | 压测、监控告警 |
| 大Key | --bigkeys |
拆分结构、unlink删除 |
代码审查、阈值限制 |
核心思想:
- 热Key的本质是读压力集中,解决方案是分散压力(本地缓存)或扩容读取能力(读写分离)。
- 大Key的本质是数据结构不合理,解决方案是拆分和异步化。
更多推荐




所有评论(0)