别再手撸热点缓存了:一个注解搞定Redis热点问题(已开源)

别再手撸热点缓存了:一个注解搞定Redis热点问题

双十一Redis连接池被打爆后,我们做了这套自动化热点防护框架

一、问题是怎么出现的

去年双十一前一周,我们线上出了个事故。某个爆款商品的详情页突然访问量暴增,QPS 从平时的几百飙到 3 万+。然后就是经典的连锁反应:

  1. Redis 连接池被打满(我们用的是 Jedis,连接池配置 200)
  2. 大量请求等待超时,响应时间从 50ms 飙到 5s+
  3. 服务 CPU 打满,开始拒绝请求
  4. 监控报警一堆,老板在群里@所有人

当时的紧急方案是手动把这个商品 ID 加到配置中心,单独走本地缓存。问题是解了,但心里知道这不是长久之计。

核心矛盾在于:少数热点 Key(可能只占 1% 的数据)会消耗掉 80% 的 Redis 资源。你不可能提前知道哪个商品会爆,只能被动应对。

二、我们试过的几种方案

2.1 方案一:纯本地缓存(Caffeine)

最简单的想法------所有商品详情都放本地缓存,不走 Redis。

java 复制代码
Cache<Long, Product> cache = Caffeine.newBuilder()
    .maximumSize(10000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build();

问题

  • 容量有限,10 万个商品只能缓存 1 万个,命中率低
  • 更新商品时没法通知其他节点,数据不一致
  • 冷启动时缓存都是空的,全打到 DB

结论:只适合单机应用或者数据量小的场景,不适合我们。

2.2 方案二:Redis + 手动管理本地缓存

这是我们最开始的方案。正常流程走 Redis,出现热点后,通过配置中心手动添加到本地缓存白名单。

java 复制代码
// 伪代码
if (localCacheWhitelist.contains(productId)) {
    return localCache.get(productId);
}
return redisTemplate.opsForValue().get("product:" + productId);

问题

  • 完全是人肉运维,发现热点 → 加配置 → 重启,至少 5 分钟
  • 配置一旦加上去就不会删,内存占用越来越高
  • 更新商品时还是要手动通知各个节点清缓存

这个方案用了半年,每次大促前都要提前梳理可能的热点商品,很痛苦。

2.3 方案三:看了看业界方案

调研了一圈,发现大厂都有自己的解决方案:

京东的 HotKey

开源了,基于客户端 SDK + 单独的热点探测服务。

优点

  • 探测准确,有专门的服务收集统计
  • 支持热点推送到客户端

问题(对我们来说):

  • 需要单独部署热点探测集群,运维成本高
  • 客户端 SDK 侵入性强,要改很多代码
  • 和我们现有的 Redis/Sentinel 技术栈不太匹配
阿里云的 Tair

云服务,自带热点缓存功能。

优点

  • 完全托管,不用操心
  • 性能优化做得好

问题

  • 要钱,而且不便宜
  • 绑定阿里云,迁移成本高
  • 不开源,出问题只能提工单
美团的 Squirrel

内部方案,没开源,只能看看文章学习思路。核心是多级缓存 + 智能预热。

三、HotArmor 的设计思路

在吸取了上面这些方案的经验后,我们自己做了一套。核心想法是:

自动化 + 渐进式

不要人工介入,让系统自己识别热点并处理。同时不能一刀切,要分层过滤。

3.1 四级漏斗架构

这是核心设计。请求不是直接打到数据源,而是经过四层过滤:

vbnet 复制代码
用户请求
    ↓
L1: 本地缓存(Caffeine)------ 命中率 60%+,响应 1μs
    ↓ Miss
L2: 噪音过滤 ------ 过滤低频访问,减少 Sentinel 压力
    ↓ Pass
L3: 热点探测(Sentinel)------ 识别 QPS 超阈值的 Key
    ↓ 热点!
L4: 安全回源(Redisson 分布式锁)------ 防止缓存击穿

为什么要 L2?

最开始我们没有 L2,直接 L1 → L3。结果发现 Sentinel 的 ParamFlowRule 在高并发下会有性能损耗(每次调用都要统计)。

加了 L2 之后,只有访问频率达到阈值(比如 10 秒内访问 5 次)的 Key 才会进入 Sentinel 检测。这样把 Sentinel 的压力降低了 80%。

3.2 热点自动晋升 + 集群同步

这是和其他方案最大的不同。

传统方案的问题:

  • 热点数据只在检测到的那个节点上缓存
  • 其他节点还是要访问 Redis

HotArmor 的方案

  1. 节点 A 检测到热点,晋升到本地缓存
  2. 通过 Redis Pub/Sub 广播给所有节点
  3. 其他节点收到广播后,也把这个数据晋升到本地缓存

这样整个集群都能享受到本地缓存的性能提升。

java 复制代码
// 伪代码
if (isHotspot) {
    // 1. 本节点晋升
    l1Cache.put(key, value);

    // 2. 广播给其他节点
    broadcastNotifier.publishPromotion(context, value);
}

代价是什么?

会增加一点网络开销(Redis Pub/Sub 的消息)。但相比热点带来的收益,完全值得。

我们实测过,一个热点商品被 10 个节点同时缓存,总的内存增加不到 10KB(商品对象本身很小)。但省下的 Redis 请求是每秒几千次。

3.3 缓存一致性的取舍

这是个经典问题,没有完美方案,只能权衡。

HotArmor 提供了两种机制:

立即失效广播(必选)

更新商品时,通过 Redis Pub/Sub 通知所有节点删除本地缓存。

java 复制代码
@HotArmorEvict(resource = "product:detail", key = "#product.id")
public void updateProduct(Product product) {
    productMapper.updateById(product);
    // 框架自动发送广播
}

这个机制延迟很低(< 10ms),能满足大部分场景。

延迟双删(可选)

如果你的场景对一致性要求极高,可以开启延迟双删。

yaml 复制代码
consistencyConfig:
  enableDelayedDoubleDelete: true
  delayTimeMs: 5000

为什么默认关闭?

因为大部分场景不需要。我们的电商系统,商品详情可以容忍几秒的延迟(用户刷新一下就看到新数据了)。强一致性是有成本的:

  • 需要 RocketMQ(增加依赖)
  • 会有 5 秒的二次删除延迟
  • 极端情况下还是可能不一致(消息丢失)

真正需要延迟双删的场景

  • 用户余额/积分(显示值要准确)
  • 文章阅读数/点赞数(允许短暂延迟,但不能回退到旧值)
  • 商品评价数量(用户刚发了评价,刷新页面不能看不到)

我们的建议是:能用最终一致性就用最终一致性,别追求强一致。像库存扣减、订单支付这种场景,一定要走数据库事务,不适合用缓存方案解决一致性问题。

四、使用体验

我们在商品详情、用户信息、首页胶囊位三个服务上接入了 HotArmor。

接入成本

  • 引入 Maven 依赖
  • 添加 YAML 配置
  • 在需要缓存的方法上加注解

整个过程半天搞定,比预想的简单。

线上效果

  • 热点商品的详情页响应时间明显下降
  • Redis 的 QPS 降低了 60% 左右(热点数据走本地缓存了)
  • 不再需要人工配置热点商品白名单

踩坑记录:后面会详细说。

五、一些踩过的坑

坑1:Sentinel 规则加载时机

最开始我们在 Bean 初始化时加载 Sentinel 规则,结果启动时报 NPE。

原因 :Sentinel 的 ParamFlowRuleManager 需要在第一次调用 SphU.entry() 时才会初始化完成。

解决:改成懒加载,第一次使用时才加载规则。

坑2:本地缓存内存占用

开启热点晋升广播后,每个节点都会缓存热点数据。如果热点太多,内存会爆。

解决:给 L1 设置容量上限(maximumSize),采用 LRU 淘汰策略。我们的配置是单节点最多缓存 2 万个对象。

六、适用场景和局限性

适合的场景

  1. 读多写少:商品详情、配置信息、用户信息
  2. 热点明显:20% 的数据贡献 80% 的访问量
  3. 能容忍短暂不一致:不是强一致性场景

不适合的场景

  1. 写多读少:每次写都要广播,开销大
  2. 数据量巨大且无热点:所有数据访问频率差不多,本地缓存没意义
  3. 必须强一致:金融交易、库存扣减等(虽然可以开延迟双删,但还是建议用其他方案)

和京东 HotKey 的对比

维度 HotArmor 京东 HotKey
部署复杂度 简单,只是个 Spring Boot Starter 复杂,需要单独部署探测集群
侵入性 低,只需要加注解 中等,需要集成 SDK
探测准确性 高(基于 Sentinel) 非常高(专业探测服务)
适用规模 中小规模(< 100 节点) 大规模(支持上千节点)
学习成本 中等

我的建议

  • 如果你的服务节点 < 50 台,用 HotArmor 足够了,简单实用
  • 如果你是超大规模集群,或者对热点探测有极高要求,考虑 HotKey

七、一些思考

为什么不做成独立服务?

最开始我们也考虑过做成独立的热点探测服务(类似 HotKey)。但后来放弃了,主要原因:

  1. 运维成本:多了一个服务要维护,挂了还要处理降级
  2. 网络开销:每次请求都要调用探测服务,增加 RT
  3. 技术栈绑定:独立服务要考虑多语言客户端

最终选择了 库的形式(Spring Boot Starter),牺牲了一点扩展性,换来了简单和高性能。

关于"过度设计"

写这个框架的时候,团队里有人质疑:"我们就一个商品服务有热点问题,搞这么复杂干啥?"

确实,如果只解决一个场景,手动管理本地缓存就够了。但我们发现:

  • 用户服务也有类似问题(网红用户访问量大)
  • 配置中心也有(某些配置被高频访问)
  • 活动服务也有(秒杀商品)

与其每个服务都写一套类似逻辑,不如抽象成框架,一次解决。

当然,这是我们的选择。如果你的系统只有一两个地方有热点,手撸就行,不需要引入框架。

八、总结

热点数据治理没有银弹,关键是找到适合自己场景的方案。

HotArmor 的设计哲学是:在保证简单的前提下,尽可能自动化。我们不追求大而全,只解决最常见的 80% 场景。

如果你也遇到了类似的问题,可以试试 HotArmor。有问题欢迎提 Issue,一起改进。


参考资料

相关推荐
程可爱4 小时前
详解Redis的五种基本数据类型(String、List、Hash、Set、ZSet)
redis
济南java开发,求内推4 小时前
redis升级至7.0.15版本
redis
摇滚侠5 小时前
Redis 零基础到进阶,Redis 持久化,RDB,AOF,RDB AOF 混合,笔记 28-46
数据库·redis·笔记
叫我龙翔5 小时前
【Redis】从零开始掌握redis --- 认识redis
数据库·redis·缓存
斯普信专业组5 小时前
Redis 持久化及应用场景详解
redis
源代码•宸5 小时前
goframe框架签到系统项目(安装 redis )
服务器·数据库·经验分享·redis·后端·缓存·golang
忍冬行者15 小时前
清理三主三从redis集群的过期key和键值超过10M的key
数据库·redis·缓存
TimberWill15 小时前
使用Redis队列优化内存队列
数据库·redis·缓存
川石课堂软件测试17 小时前
Mysql中触发器使用详详详详详解~
数据库·redis·功能测试·mysql·oracle·单元测试·自动化