Redis+Caffeine太强了!性能原地起飞?

平均响应从 8ms 跳降到 1.5ms,数据库的 QPS 从 20000 砍到 2000,缓存命中率从 85% 提升到 98%——这是一次把 Redis 和 Caffeine 搭在一起后的直观效果。

事情是这样开始的。某次大促,商品详情页访问量瞬间暴增,峰值接近 10 万 QPS。系统原来主要靠单个 Redis 做缓存,带宽和网络延迟被瞬间放大,Redis 返回变慢,甚至出现超时。应用里每个实例都有自己的本地 Caffeine 缓存,但本地内存有限,热点数据远超其承载能力,Caffeine 的命中率暴跌,更多请求穿透到 Redis,形成恶性循环。数据库压力随之爆表,用户体验出现明显退化。被压得喘不过气后,团队决定改架构:把本地高速缓存和分布式缓存做冷热分层,形成两级缓存。

倒着说实现细节,从线上效果回溯到设计思想。改造后,读取路径变成:先看本地 Caffeine(一级),没命中去 Redis(二级),再没命中才打到数据库。命中后有分层回写:从 DB 取回的数据先写入 Redis,再回填到本地 Caffeine。这套流程让绝大多数请求在内存里解决,网络往返和磁盘 I/O 大幅减少。Caffeine 用 W-TinyLFU 算法,纳秒级访问,适合存放最热的那部分数据;Redis 承担次热数据和跨实例共享,保证分布式环境下的数据可见性。数据库则作为最终保底。

写路径和数据一致性是改造的核心风险点。团队选了一套实践:写操作先更新数据库,确认持久化成功后,再按顺序清理缓存。删除缓存的顺序也有讲究——先把本地 Caffeine 的条目删掉,再删 Redis。缘由是:如果先删 Redis,短时间内别的线程可能从本地缓存读到旧数据,继而把旧值重新写回 Redis,导致脏数据。按先写 DB、后删缓存,并在删缓存时优先清本地,能把这类竞态情况降到可控范围内。

在分布式环境下,异步更新和发布订阅也是常用手段。把变更消息写入持久化队列(例如 Kafka、RocketMQ),由后台线程异步更新 Redis 和各实例的本地缓存,能显著提高写性能,但要注意队列持久化以防消息丢失。另一个做法是借助 Redis 的 pub/sub:数据变更时发布事件,订阅者收到后清本地缓存。这个方案保证速度和一致性,但会带来额外的网络消息开销和系统复杂度。根据业务对一致性和延迟的不同容忍度,可以在失效(删除)模式、异步更新、以及发布订阅中选一种或混合使用。

缓存淘汰策略也要讲清楚。Caffeine 可设置 maximumSize 和 expireAfterWrite,团队实践里常用 LRU+TTL 的组合:热点数据给较长 TTL(列如一小时),普通数据走 LRU 淘汰。Redis 层则一般配置 allkeys-lru,当内存到上限时,从整个键空间里淘汰最近最少使用的键。两层配合,把最热的留在本地,次热的放在 Redis,整体命中率提高,内存利用更合理。

实现细节上还有几个容易被忽视的点。序列化方式会影响带宽和响应速度:在 Java 应用里,把对象直接放到 Caffeine 里可以免掉序列化开销;写到 Redis 时必须序列化。JDK 默认序列化体积大、慢,推荐用 Protostuff 或 Kryo,能把数据体积缩小近一半,序列化/反序列化速度提升数倍。这对高并发场景下的网络带宽和 Redis 存储都很关键。

并发控制方面,Caffeine 基于 ConcurrentHashMap(无锁设计)在单进程里表现出色;跨进程并发写入同一键时,需要分布式锁来防止击穿。实践中使用 Redisson 的可重入锁比较常见:写操作获取锁,完成后释放,其他实例等待或采用重试策略。这样能避免大量并发请求同时穿透到 DB。

冷启动问题要提前思考。上线或重启后本地缓存为空,会出现短时间的读放大。解决方法是预热,把热点数据一次性放进 Caffeine,代码里直接调用 caffeineCache.putAll(hotDataMap)。hotDataMap 由离线统计或近实时热度计算得来。预热能把首次峰值缓解不少,尤其是秒杀或大促前的准备工作。

实战中的几个踩坑案例值得记录。先是内存溢出:Caffeine 如果不限制 maximumSize,会把进程内存吃光,最终触发 OOM。把抽屉容量限定好,并配合过期策略,能避免把应用摔死。另一个是更新顺序错乱导致的一致性问题:曾经有团队先删缓存再更新 DB,结果在高并发下出现了旧数据回写的情况。还有回填大对象的问题:如果把 1MB 以上的大对象直接放到本地缓存,回填时会把大包裹从 Redis 拉向多个实例,瞬时占满带宽。实践里常用的解决思路是本地只存轻量对象或 ID,真正的详情按需从 Redis/DB 拉取;必要时把大对象压缩或拆分,减少单次数据传输量。

在性能优化的微观层面,还有些可落地的提议。读多写少的数据适合尽可能把热数据放本地;对序列化频繁的类型预分配对象池或使用高性能序列化库;将热点键做热点隔离,避免聚焦在单个 Redis 分片上;给关键流程加埋点,实时监控本地命中率、Redis 带宽和 DB QPS,便于发现瓶颈并快速调整策略。

回到那次大促后 Observability 的数据,改造部署完成后团队持续跟进:响应时间和数据库压力明显下降,缓存命中率稳定在高位。之后在其它高频场景也复用这套设计,收益持续显现。部署过程并非一帆风顺,但把各类风险点拆开处理后,系统稳定性提升明显,运维压力也降低了不少。

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...