news 2026/9/12 23:16:02

积分系统缓存架构评审:Caffeine+Redis多级缓存完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
积分系统缓存架构评审:Caffeine+Redis多级缓存完整实践

开评审会之前,我其实没想到一个积分系统的缓存改造能吵得这么热闹。争论的焦点不是Redis够不够用,而是—要不要在Redis前面再加一层本地缓存。当时摆在桌上的方案有三套:纯Redis、Caffeine单机缓存、Caffeine+Redis多级缓存。积分系统这个业务,你说它重吧,流量高峰也就集中在积分查询和签到打卡;你说它轻吧,高峰期QPS冲到几千,单库单Redis照样被打得吱哇乱叫。这篇就以我实际参与的这次会员积分系统架构评审为蓝本,把我们在本地缓存和多级缓存之间做的取舍、踩的坑、压测的数据、评审的争议点全部梳理出来,给同样在做缓存选型的朋友一个参考。

1. 评审前夜的痛点:积分系统到底卡在哪里

1.1 积分系统的业务特征与流量画像

先说积分系统的业务特点。这类系统跟订单系统、支付系统有个显著区别——读多写少,而且读的集中度极高。用户打开会员中心首页,第一眼要看的就是积分余额;点进积分商城,要看积分明细;签到、做任务、消费返积分,都会触发一次积分变动。但真正的写操作(加积分、扣积分、过期清零)在总请求量里的占比通常不到10%。

这种特征意味着,缓存系统设计的主战场在“读链路”,而不是“写链路”。如果不做缓存,每次请求都打到数据库,那数据库的查询压力会直线上升。尤其是积分明细分页查询这种重IO操作,表数据量一旦过了千万级,索引再合理也扛不住频繁的随机读。

一个容易被忽略的点是积分数据的热点分布。大部分积分查询都集中在少数高活跃用户身上,比如每天签到打卡、做任务、逛商城的忠实用户。我统计过当时线上一个月的日志,Top 5%的用户贡献了将近60%的积分查询量,Top 20%的用户占了85%以上。这种“二八定律”甚至“一九定律”式的访问模型,恰恰是本地缓存最擅长的场景。

1.2 老架构的缓存链路与问题清单

老架构其实不是没有缓存,只是链路非常简单粗暴——Redis缓存。请求进来先查Redis,命中就直接返回,没命中回源MySQL,然后把结果回填Redis。这套逻辑在业务初期没问题,但随着用户量增长和活动玩法变多,问题开始逐渐浮现。

首先是Redis的CPU和带宽压力。高峰期读QPS上去之后,Redis单实例的CPU开始飙升,网络带宽也成了瓶颈。有一次做完大促活动,Redis的带宽跑到了200Mbps+,吓得运维直接限流。本地缓存最大的优势恰恰在这里——请求根本不出应用服务器,连网络开销都省了,对Redis的压力自然大幅缓解。

其次是热点key问题。某个爆款积分商品的详情页,某个头部KOL的积分排行榜,都可能成为单个key被高频访问的对象。Redis单key承载的QPS是有上限的,一旦热点key的访问量冲到几十万,即使Redis的性能再强,面对单key的序列化、网络传输开销也会显得力不从心。

第三个问题更隐蔽——数据回源抖动。Redis缓存miss之后,大量请求同时穿透到MySQL,很容易把数据库打崩。当时做过一次压测,把Redis里一个热门key提前删掉,结果瞬间有几千个请求同时打到数据库,主库的CPU直接飙到了90%以上。这种缓存击穿导致的连锁反应,单靠Redis本身是防不住的。

正是这些问题,促使我们发起了这次架构评审。评审的核心议题就一个:缓存架构要不要从“Redis单级”升级为“Caffeine+Redis多级”。但方案嘴上说得简单,真到落地环节,各种细节博弈才刚开始。

2. 架构评审的第一轮交锋:本地缓存凭什么被重新提起

2.1 候选方案全景图:纯Redis、纯Caffeine与多级组合

评审会的第一轮是方案陈列。当时候选池里有三套,我先把各自的特点摆出来:

纯Redis方案,就是老架构的增强版,分片集群、读写分离,再把热点key做本地预热。这套思路的好处是架构简单、团队熟悉、不需要引入新的中间件,但解决不了Redis本身的网络和单key瓶颈。集群扩容可以横向分摊压力,但单key热点问题在分片模式下反而更棘手,因为热点key只会落在某一个分片上。

纯Caffeine方案,把所有积分数据都放到应用本地缓存里,查询完全走内存。这个方案单机性能确实炸裂,但有个致命伤——每个应用实例各存一份,数据一致性没法保证。用户在这个节点查到积分是100,在另一个节点查到可能是90,对积分这种对准确性敏感的业务来说,这是不可接受的。

多级缓存方案,Caffeine做一级缓存(L1),Redis做二级缓存(L2),数据库在最底层兜底。查询顺序是Caffeine -> Redis -> MySQL,回填顺序反过来,MySQL加载后回填Redis和Caffeine。这套方案的出发点是结合两者的优点——用本地缓存扛热点和带宽压力,用Redis保证分布式环境下各实例之间的数据最终一致性。

2.2 评审维度与取舍逻辑:性能、一致性、成本、复杂度

三套方案列完,争论就开始升级了。有人指出纯Caffeine的一致性问题是硬伤,有人反驳纯Redis的带宽瓶颈早晚会爆。当时我主导评审,给出了一套四维评估框架:性能、一致性、成本、复杂度,每个维度权重不同,逐一打分。

性能上,多级缓存和纯Caffeine显然占优,Caffeine的本地读取是纯内存操作,耗时在纳秒到微秒级别,而Redis一次get请求至少是0.5到2毫秒的网络往返。一致性上,纯Redis最强,因为全局只有一份数据;多级缓存次之,因为引入了本地副本,存在短暂的不一致窗口;纯Caffeine最差。成本和复杂度上,纯Redis最优,多级缓存最重,因为它需要额外管理两级缓存的同步、失效、监控等一系列问题。

评审最忌讳的是“既要又要”。我们当时明确了两条底线:第一,积分查询的性能必须从P99响应时间上持续优化,目标是从当前的30ms+压进10ms以内;第二,数据一致性不允许出现长期偏差,但允许秒级的短暂延迟。这两条底线,等于直接把纯Caffeine方案和纯Redis方案都否掉了,多级缓存成为唯一同时满足要求的路径。

2.3 单级方案的边缘场景盲区:击穿、穿透与带宽

评审会上有个细节让我印象很深。反对多级缓存的同事抛出一个问题:“我们的QPS真的高到Redis扛不住了吗?”我当时没有正面回答,而是放了一组压测数据。模拟场景是积分首页的集中访问,持续把同一个用户积分key打满30分钟。纯Redis架构下,Redis实例的CPU先到瓶颈,随后响应时间直线上升,P99从2ms恶化到40ms以上;多级缓存架构下,Redis的QPS下降了差不多80%,CPU占用平稳,P99稳定在3ms以内。

这个对比说明了单级缓存方案在边缘场景下的盲区。你以为系统QPS不高,但热点key会人为制造“局部高并发”,这个局部压力对Redis是真实存在的。另一个盲区是缓存穿透,如果恶意用户频繁查询不存在的积分流水号,每次请求都会穿透缓存打到数据库。纯Redis方案对这种问题没有很好的缓解手段,而Caffeine本地缓存可以缓存空值,把穿透拦截在应用层。

评审结论在这一轮其实已经清晰了,多级缓存不是锦上添花,而是针对积分系统流量特征的必然选择。接下来的问题是:落地时怎么做得优雅、可控,而不是搞出一套复杂的中间件。

3. 多级缓存架构的落地细节:Spring Cache + Caffeine + Redis

3.1 整体链路设计与数据流说明

架构选定后,我们开始细化落地。技术选型上用Spring Cache作为缓存抽象层,底层CacheManager自定义实现,一级缓存用Caffeine,二级缓存用Redis。为什么选Spring Cache?因为它提供了统一的缓存注解和API,可以把缓存逻辑跟业务代码解耦,团队成员不需要关心底层到底走了哪级缓存。

链路设计是这样的。查询积分余额时,先查Caffeine,本地内存里有直接返回,没有则查Redis,Redis命中就回填Caffeine并返回,还没有就查数据库,然后按层级回填。这套链路的关键在于:本地缓存是“加速层”,Redis是“一致层”,数据库是“兜底层”,每一层承担的角色不同,不能混为一谈。

Spring Cache默认只支持单级缓存,要实现多级,必须自定义CacheManager和Cache。核心思路是写一个CompositeCache类,实现Spring的Cache接口,内部持有Caffeine和Redis两个Cache实例,get方法先走本地再走远端,put方法同时写入本地和远端。这里有一个细节需要注意:put时必须先写Redis再写Caffeine,因为如果先写了本地而Redis写入失败,本地缓存就成了脏数据源头。

3.2 CacheManager定制实现:两级缓存怎么串联

有朋友可能觉得自定义CacheManager很难,其实核心代码量不多,但要对Spring Cache的扩展点有清晰理解。我们定义了一个MultiLevelCacheManager,继承AbstractCacheManager,在loadCaches()方法里逐个创建缓存实例。每个缓存实例由名称、Caffeine配置、RedisTemplate组成。

真正的工作量在MultiLevelCacheget()put()方法里。get()的查找顺序我写成了模板方法模式,先查本地,本地miss则查Redis,Redis命中就异步回填本地。这里为什么要异步回填?因为如果同步回填,每次本地miss都要等一次Caffeine写入,虽然开销不大,但在超高QPS场景下会放大延迟。异步回填带来的问题是一致性窗口,但考虑到本地缓存的TTL只有几分钟,这个窗口是可接受的。

Caffeine配置上,我们用了maximumSizeexpireAfterWrite的组合。maximumSize控制条目数上限,防止本地缓存无限膨胀吃掉JVM堆内存;expireAfterWrite控制写入后的过期时间,保证数据不会永久停留在本地。这两个参数需要根据实例数和数据总量来算,我后面单独说。

Redis层面没有做太复杂的事情,就是普通的spring-data-redisRedisTemplate,Key序列化器用String,Value序列化器用GenericJackson2JsonRedisSerializer。这里有个小坑,GenericJackson2JsonRedisSerializer序列化出来的JSON会带@class字段,虽然反序列化方便,但会多占大概30%的存储空间。对积分这种数据量不大的场景无所谓,如果缓存数据量巨大,建议换成自定义序列化器。

3.3 过期策略与淘汰策略的参数计算

缓存参数设计是这次评审里最“硬核”的部分。我先算了一笔账。线上有12个应用实例,每个实例分配2GB堆内存,考虑到正常业务对象占用,缓存最多能分到500MB左右。会员积分数据按每个条目150字节估算(包括Key、Value、元数据),500MB大概能存300万条数据。但实际热点用户远没那么多,我们按顶部活跃用户的10%来缓存,大概需要缓存30万条,占用的内存不到50MB,完全可控。

于是Caffeine的maximumSize设成了50万,留了余量。expireAfterWrite起初设的5分钟,压测后发现P99响应时间稳定,但Redis的QPS还有一定下降空间。后来改成expireAfterWrite=10分钟加refreshAfterWrite=5分钟的搭配,Caffeine在5分钟时会异步刷新缓存,不会阻塞请求线程,有效降低了回源频率。

Redis的TTL设成了30分钟,比Caffeine长得多。为什么?因为Redis是二级缓存,作用是在本地缓存失效后提供快速恢复,TTL太短会导致大量请求回源数据库,太长又会占用过多Redis内存。30分钟是一个折中值,既能覆盖大部分活跃会话,又不会造成数据长期不一致。

参数定好后,我把计算公式和验证表格都留在了评审记录里,方便后续容量变化时重新核算。这里强烈建议,缓存参数一定不要拍脑袋定,每调整一个值都要有数据和场景支撑,否则上线后大概率要返工。

3.4 缓存一致性方案:更新顺序、延迟双删与兜底

多级缓存最棘手的问题不是性能,而是数据一致性。积分这种数据,用户对准确性极其敏感——明明消费了积分,页面还显示旧数字,用户立刻就会投诉。所以我们在评审中就明确:Caffeine和Redis允许短暂不一致,但最终必须一致,而且不一致的时间窗口要控制在秒级以内。

更新链路的处理逻辑是“先更新数据库,再删除Redis,最后删除本地Caffeine”。为什么不更新缓存而是删除缓存?因为更新缓存存在并发写冲突的风险,不如删掉让下次查询回源重建,更简单也更安全。删除顺序上,先把Redis删了,再删所有实例的本地缓存。这里有个问题——本地缓存不止一份,每个实例各存各的,你删了这个实例的,其他实例怎么感知?

我们的方案是用Redis的Pub/Sub广播失效消息。任意实例更新数据后,除了删除自身Caffeine,还往Redis的一个专用channel发一条消息,其他实例订阅该channel,收到消息后删除自己本地的对应key。这套机制简单可靠,不需要引入额外的消息队列。不过跨网络的广播毕竟是异步的,从发消息到所有实例删完本地缓存,存在一个极短的窗口,这也就是为什么我们把Caffeine的TTL设得较短,把它作为最终一致性的兜底防线。

再补充一个“延迟双删”的变体。更新数据库后,先删一次缓存,等500毫秒再删一次。延迟双删是为了解决并发场景下“旧缓存被回填”的问题,但也别神话它,它只是在概率上降低了脏数据风险,真正解决还是要靠版本号或者Binlog订阅。我们的场景里,叠加了较短TTL和广播机制之后,实际脏数据出现概率已经很低,延迟双删作为额外保险保留了下来。

4. 评审后的性能验证与稳定性演练

4.1 压测数据对比:单级缓存与多级缓存的差距

方案落地后,第一件事是压测。我们用相同的流量模型对比了纯Redis架构和多级缓存架构,测试环境4个应用实例,1个Redis。压测模型模拟线上积分首页的真实请求:查询积分余额占70%,查询积分明细占20%,模拟签到写操作占10%。QPS从1000起步,逐步涨到8000,每个档位跑5分钟。

数据显示,QPS在1000时,两种架构差距不明显,响应时间都在5ms以下。QPS涨到4000,纯Redis架构的Redis CPU占用到了60%,P99涨到18ms;多级缓存架构的Redis CPU不到20%,P99是6ms。QPS涨到8000时,纯Redis架构的Redis CPU直接到95%,开始出现超时;多级缓存架构的Redis CPU稳定在35%,P99保持在9ms以内,应用CPU略有上升,但完全可控。

这个结果基本验证了评审阶段的判断。本地缓存的实际意义不在于“更快”,而在于把热点流量从Redis上卸载掉,让Redis从“主力部队”变成“后备部队”。压测数据放到评审记录里后,之前质疑多级缓存“没必要”的同事也认可了方案价值。性能数据的说服力,永远比嘴上争论大得多。

4.2 缓存雪崩、穿透与击穿的防御清单

缓存系统上线前一定要把三类经典问题想清楚,多级缓存架构最怕的不是性能不够,而是被流量打穿后数据库扛不住。我整理了积分场景下的防御清单,这里贴出来给大家参考。

缓存穿透是查询一个不存在的key,缓存里没有,数据库里也没有。积分明细查询最容易触发,比如用户伪造一个不存在的积分流水号。解法是缓存空值,Caffeine和Redis都缓存空对象,并设置较短TTL(比如30秒)。这样同一批恶意请求第二次就会被拦截。

缓存击穿是某个热点key失效的瞬间,大量请求同时回源。比如头部用户签到后积分发生变化,所有缓存被删除,其他用户在接下来一秒内查询这个用户,就可能同时打到数据库。解法是互斥锁。在Caffeine层面我们可以用get(key, loadFunction)的原子加载方式,配合Caffeine的同步加载,它内部会对同一个key的并发加载做排队,天然避免了击穿。

缓存雪崩是大批key同时失效或Redis宕机,导致流量全部打到数据库。解法有两层:第一层是给Redis的TTL加随机偏移,比如30分钟正负10分钟,避免同一批key在同一时刻集体过期;第二层是引入Redis高可用和本地缓存的缓冲,即使Redis宕机,Caffeine还能撑一段时间,不会瞬间把数据库压垮。

4.3 监控指标与告警阈值的设置

上线后,监控是重中之重。我们定义了几个核心指标,全部接入Prometheus + Grafana。第一层是缓存命中率,分三级统计:Caffeine命中率、Redis命中率、数据库回源率。Caffeine命中率正常应该在80%以上,Redis命中率应该在95%以上,如果这两个数字下滑,说明缓存的TTL或淘汰策略可能有问题。

第二层是耗时分布。除了应用整体P99响应时间,还要看Redis操作的平均耗时常驻和分位数,快速判断Redis是否成为瓶颈。第三层是Redis自身的指标,比如CPU使用率、内存使用率、带宽、慢查询数。如果Redis带宽超过80%或CPU超过70%,就要考虑扩容或者调大Caffeine的缓存容量。

告警阈值的设置同样讲究,不是所有指标都要告警,也不是所有告警都值得被处理。我们对数据库回源率设置了阈值——如果单实例一次回源超过500QPS,说明多级缓存可能出现了较大面积失效,需要立刻排查。对Caffeine命中率设置了跌落到60%的告警,对Redis CPU设置了70%的告警。告警一定要有处理动作,否则打了等于没打。

5. 聊聊评审中几个容易被忽视的争议点

5.1 两级缓存是不是过度设计

评审过程中最尖锐的质疑是:“我们这套系统有没有必要搞两级缓存?是不是纯属炫技?”我当时没有直接反驳,而是把压测数据和线上监控调出来,指出一个客观事实:QPS高峰段Redis的CPU持续在70%以上,出现过多次由Redis延迟波动引起的接口超时。积分系统不像商品详情页那样QPS高达成千上万,但在大促、活动、签到高峰期,瞬间流量完全有可能打满Redis。

判断是否过度设计,标准不在方案本身,而在业务是否有对应的极端场景。如果系统QPS常年低于500,Redis完全扛得住,那多级缓存确实多余;但如果像我们一样,活动期间会出现流量尖峰,而且积分数据有典型的热点分布,那多级缓存的价值就不是理论上的,而是可以量化的。评审不能只看平均负载,更要看尖峰负载和故障恢复能力。

我的观点一直是:架构选型追求的不是最好,而是匹配。两级缓存不适合所有系统,但它适合有热点、有峰谷、读多写少、一致性要求不极端敏感的业务。积分系统恰好符合这些条件,所以不是过度设计,而是提前布局。

5.2 一致性要求到底怎么界定

多级缓存引入后,一个绕不开的问题是——怎么跟产品和业务方对齐一致性预期。刚开始我犯过一个错,总觉得技术团队内部把一致性方案做好就行,不需要跟业务方对齐。直到有一次积分变动后,用户在活动页看到的积分还是旧值,产品经理火急火燎跑过来问怎么回事,我才意识到,这个问题必须提前摆到台面上。

我们当时的处理方式是,跟产品、运营对齐了一个“一致性分级”的规则。积分余额查询允许最多10秒的延迟,因为用户实际操作中,从积分变动到刷新页面,中间往往有操作间隔;但积分扣减、过期清零这种涉及资金属性场景,必须做到实时一致。根据这个分级,我们对不同接口设置了不同缓存策略——普通查询走多级缓存,敏感写操作则同时清理所有缓存并强制回源数据库。

这种分级思路值得借鉴。不要跟业务方说“多级缓存是最终一致性”,这是技术语言,业务方听不懂也不关心。你要告诉他“页面上的积分最多延迟10秒刷新”,这是一个可验证的业务承诺。把技术问题翻译成业务语言,评审会上的很多争论就能化解。

5.3 团队能 hold 住这套复杂度吗

任何一个多级缓存方案,最终落地的阻力往往不在技术,而在团队的接受度和维护能力。当时我印象很深,有一位经验丰富的后端同事问了句:“出了问题,你确定我们值班的人能快速定位是Caffeine的问题还是Redis的问题吗?”这句话问得非常好。多级缓存的排查链路比单级缓存长,因为请求可能落在任何一级,也可能在两级之间来回穿透。

为了解决这个可运维性问题,我们没有简单地把方案丢给团队,而是在架构评审记录里附加了一份排障手册。手册覆盖了典型故障场景,比如Caffeine命中率骤降、Redis响应变慢、本地缓存出现脏数据等,每个场景都有对应的排查路径和修复动作。同时,在日志层面给每级缓存命中或未命中都打了标记,一次请求经过Caffeine、Redis、MySQL时留下完整链路,排障时一眼就能看出卡在哪一层。

这套准备工作做完后,团队才真正接受了多级缓存方案。架构评审的意义,从来不在于选一个高大上的技术,而在于让参与的人理解技术的代价和回报,并且有能力去维护它。如果方案落地后没人能接住,再先进的架构也只是给团队埋雷。

多说一句,后续如果要继续演进,可以考虑在两级缓存之上加一层进程内消息通知或者引入分布式追踪中间件,把缓存链路的耗时和命中情况做更细粒度的可视化。不过那是后话了,当前这套Caffeine+Redis的多级缓存方案,在积分系统里跑得很稳,线上验证的结果也支持了评审时的所有判断。希望这篇记录能给正在做类似选型的朋友提供一些参考。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 23:14:10

Pi Agent 环境变量完全指南:进程标记、会话注入与运行时配置

Pi Agent 环境变量完全指南:进程标记、会话注入与运行时配置 【免费下载链接】scientific-agent-skills Turn any AI agent into an AI Scientist. The #1 Agent Skills library for science, used by 190,000 scientists worldwide. 165 ready-to-use validated sk…

作者头像 李华
网站建设 2026/9/12 23:09:09

推客系统佣金规则动态配置技术解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 23:08:41

Beads 多 Agent 协作指南:任务分配、工作交接与冲突序列化

Beads 多 Agent 协作指南:任务分配、工作交接与冲突序列化 【免费下载链接】beads Beads - A memory upgrade for your coding agent 项目地址: https://gitcode.com/GitHub_Trending/beads1/beads 本指南面向使用 Beads 驱动多个 AI Agent 协同工作的场景&a…

作者头像 李华
网站建设 2026/9/12 23:08:30

电池类设备低功耗安全握手方案设计与优化实战

1. 项目概述与核心痛点1.1 为什么“安全握手”会成为电池类设备的头号难题做硬件这么多年,我最怕的不是功能做不出来,而是功能做出来了,设备却活不过一个冬天。电池类智能设备,从蓝牙门锁、温湿度传感器到智能穿戴,几乎…

作者头像 李华
网站建设 2026/9/12 23:08:21

Vibe Coding与LeetCode:AI时代编程学习新范式

1. 从LeetCode到Vibe Coding:编程学习范式的转变Linus Torvalds最近关于Vibe Coding的言论在开发者社区引发了广泛讨论。这位Linux之父表示,虽然自己并不使用AI编程工具,但对Vibe Coding这种新兴编程方式"总体持积极态度"。这不禁让…

作者头像 李华
网站建设 2026/9/12 23:02:59

Python语法全解析:从基础到高级实战技巧

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华