SpringBoot二级缓存3种高并发落地方案

3 阅读10分钟

前言 不知道作为互联网软件开发同行的你,有没有遇到过这样的场景:项目上线初期,接口响应速度还能稳定在 100ms 以内,但随着用户量增长、数据量变大,数据库压力越来越大,就算加了 Redis 缓存,偶尔还是会出现 “缓存穿透”“缓存击穿” 的问题,甚至有次我负责的订单模块,因为缓存失效导致数据库瞬间扛不住压力,接口直接超时 504,差点影响用户下单 —— 这种 “明明加了缓存却还是掉链子” 的情况,是不是让你也很崩溃?

其实不止我,身边不少开发同事都吐槽过类似问题:要么是只依赖本地缓存,分布式部署时数据不一致;要么是单靠 Redis,高并发下网络开销大、响应变慢;还有的配置了缓存却没考虑失效策略,最后还是绕回数据库查数据。今天就结合实战经验,跟大家聊聊怎么用 Spring Boot 二级缓存解决这些痛点,从原理到落地步骤,一步步讲清楚,看完你就能直接用到项目里。

为什么单靠 “一级缓存” 解决不了问题? 在说二级缓存之前,咱们得先明确一个前提:为什么之前常用的 “一级缓存”(不管是本地缓存还是分布式缓存),在高并发场景下会不够用?这就要从咱们开发中常见的两种缓存方案痛点说起。

第一种是只依赖本地缓存,比如用 Caffeine 、Guava Cache。这种方案的优势是读取快,没有网络开销,但问题也很明显 —— 如果项目是分布式部署,多台服务器的本地缓存数据无法同步。就像我之前做的用户中心项目,用了 Caffeine 缓存用户信息,结果一台服务器更新了用户数据,其他服务器的缓存还是旧的,导致用户登录后看到的信息不一致,最后只能加定时任务刷新,既麻烦又耗资源。

第二种是只依赖分布式缓存,比如 Redis。Redis 能解决分布式数据一致性问题,但高并发下的网络开销是硬伤。有次我们做秒杀活动,QPS 冲到 5000 的时候,Redis 虽然没崩,但每次从应用服务器到 Redis 的网络请求,平均耗时从 20ms 涨到了 80ms,接口整体响应时间直接翻倍,最后不得不临时扩容 Redis 集群,才勉强顶住压力。

而 Spring Boot 二级缓存,本质是 “本地缓存 + 分布式缓存” 的组合:优先读本地缓存,本地没有再读分布式缓存,同时通过一定策略保证两者数据一致。这种方案既兼顾了本地缓存的速度,又解决了分布式缓存的网络开销问题,刚好能补上咱们之前遇到的那些漏洞。

Spring Boot 二级缓存 3 种落地方案,从简单到进阶 结合我参与过的 3 个项目实战,总结出了 3 种二级缓存落地方案,分别适合不同的业务场景,你可以根据自己的项目需求选。

方案 1:基于 Spring Cache + Caffeine + Redis,快速上手 这种方案的核心是用 Spring Cache 做缓存抽象层,本地缓存用 Caffeine(性能比 Guava Cache 更好),分布式缓存用 Redis,优点是集成简单,不用自己写太多缓存逻辑,适合快速迭代的项目。

具体落地步骤很简单,咱们一步步来:

第一步,引入依赖。在 pom.xml 里加 3 个关键依赖:spring-boot-starter-cache(Spring Cache 核心)、 com.github.ben-manes.caffeine(Caffeine 缓存)、spring-boot-starter-data-redis(Redis 支持),版本可以根据自己的 Spring Boot 版本匹配,比如 Spring Boot 2.7.x 的话,Caffeine 用 3.1.1,Redis starter 用 2.7.0 就行。

第二步,配置缓存。在 application.yml 里分别配置 Caffeine 和 Redis:Caffeine 主要配置过期时间(比如 300 秒)、最大缓存条数(比如 1000 条,避免内存溢出);Redis 配置地址、端口、密码,还有缓存序列化方式(建议用 Jackson2JsonRedisSerializer,避免默认的 JDK 序列化出现乱码)。

第三步,写配置类。创建一个 CacheConfig 类,用 @EnableCaching 开启缓存,然后分别定义 CaffeineCacheManager(本地缓存管理器)和 RedisCacheManager(分布式缓存管理器),再通过 CacheResolver 把两个缓存管理器组合起来,指定二级缓存的顺序是 “Caffeine 优先,Redis 兜底”。

第四步,业务代码中使用。在需要缓存的 Service 方法上,加 @Cacheable 注解,指定缓存名称(比如 “orderCache”)、key(可以用 SpEL 表达式,比如 “#orderId”),这样方法调用时,会先查 Caffeine 本地缓存,没有的话再查 Redis,都没有才查数据库,最后把结果同步到两个缓存里。

这个方案我在一个中小型电商项目里用过,接口响应时间从原来的 200ms 降到了 50ms 以内,而且开发成本低,不到 1 天就能集成完。不过要注意一点:如果业务中有更新或删除数据的操作,要加 @CachePut 或 @CacheEvict 注解,同步更新或清除两个缓存,不然会出现数据不一致的问题。

方案 2:基于 Redisson + Caffeine,解决分布式锁问题 如果你的项目是高并发场景,比如秒杀、抢购,那方案 1 可能会有个隐患:当本地缓存过期时,多台服务器会同时去查数据库,然后同步到 Redis,出现 “缓存击穿” 的问题。这时候就需要方案 2—— 用 Redisson 的分布式锁,保证同一时间只有一台服务器去查数据库,避免数据库压力骤增。

Redisson 是 Redis 的 Java 客户端,自带分布式锁功能,用法很简单,咱们还是结合步骤说:

第一步,引入 Redisson 依赖。在 pom.xml 里加 redisson-spring-boot-starter,版本和 Spring Boot 匹配就行,比如 2.7.x 的 Spring Boot,用 3.16.8 版本的 Redisson。

第二步,配置 Redisson。在 application.yml 里配置 Redis 地址、端口、密码,Redisson 会自动创建客户端,不用自己写太多配置。

第三步,改造缓存逻辑。在原来的 Service 方法里,先判断 Caffeine 本地缓存是否存在,如果不存在,就用 Redisson 获取分布式锁(比如锁的 key 是 “lock:order:{orderId}”),获取到锁之后,再查 Redis 缓存;如果 Redis 也没有,就查数据库,然后把结果同步到 Redis 和本地缓存,最后释放锁。

这里有个细节:获取锁的时候,要设置超时时间(比如 30 秒),避免服务器宕机导致锁无法释放;而且要在 finally 块里释放锁,保证无论是否出现异常,锁都能正常释放。

我之前做的一个秒杀项目,用方案 2 解决了缓存击穿问题。之前没加分布式锁的时候,秒杀开始后 1 分钟内,数据库 QPS 冲到了 2000,加了 Redisson 锁之后,数据库 QPS 降到了 200 以内,而且接口响应时间稳定在 30ms 左右。不过这个方案比方案 1 稍微复杂一点,需要自己写锁的逻辑,适合对并发要求高的项目。

方案 3:基于 Canal + 二级缓存,实现缓存自动同步 如果你的项目数据更新频繁,比如订单状态实时变化、商品库存实时调整,那手动用 @CacheEvict 或 @CachePut 更新缓存会很麻烦,而且容易遗漏。这时候可以用方案 3—— 通过 Canal 监听 MySQL binlog,当数据发生变化时,自动更新或清除二级缓存,实现 “数据变,缓存自动变”。

Canal 是阿里巴巴开源的一款数据同步工具,能模拟 MySQL 主从复制的过程,监听 binlog 日志,咱们来看看怎么结合二级缓存用:

第一步,部署 Canal 服务。先在 MySQL 里开启 binlog(配置 my.cnf,设置 log_bin=mysql-bin,binlog_format=ROW),然后下载 Canal 的压缩包,修改配置文件,指定 MySQL 的地址、端口、用户名、密码,还有要监听的数据库和表(比如 order 库的 order_info 表),然后启动 Canal 服务。

第二步,项目中集成 Canal 客户端。引入 canal-spring-boot-starter 依赖,在 application.yml 里配置 Canal 服务的地址和端口,然后创建一个 Canal 监听器类,用 @CanalEventListener 注解,写一个 onEvent 方法,监听数据变化事件(比如 INSERT、UPDATE、DELETE)。

第三步,缓存自动同步逻辑。当监听到数据变化时,根据变化的表和主键,生成对应的缓存 key(比如 “orderCache:123”,123 是订单 ID),然后先清除本地 Caffeine 缓存(调用 cacheManager.getCache ("orderCache").evict (key)),再清除 Redis 缓存(调用 redisTemplate.delete (key))。

这个方案我在一个物流项目里用过,之前每次更新订单状态,都要手动调用清除缓存的方法,偶尔会忘记,导致用户看到旧状态;用了 Canal 之后,数据更新后缓存会自动清除,再也没出现过数据不一致的问题。不过这个方案需要部署 Canal 服务,运维成本稍微高一点,适合数据更新频繁的项目。

3 种方案怎么选?一张表帮你理清 可能你看到这里会纠结:到底该选哪个方案?我整理了一张对比表,从适用场景、开发成本、运维成本、并发能力四个维度,帮你快速决策:

方案

适用场景

开发成本

运维成本

并发能力

方案 1(Spring Cache + Caffeine + Redis)

中小型项目、迭代快、数据更新不频繁

低(1 天内集成)

低(只需维护 Redis)

中(适合 QPS 1000 以内)

方案 2(Redisson + Caffeine)

高并发项目(秒杀、抢购)、需要避免缓存击穿

中(需要写分布式锁逻辑)

中(维护 Redis + Redisson)

高(适合 QPS 5000 以内)

方案 3(Canal + 二级缓存)

数据更新频繁(实时订单、库存)、需自动同步缓存

中(集成 Canal 客户端)

高(需部署维护 Canal 服务)

高(适合 QPS 5000 以内)

简单来说:如果你的项目小、赶进度,选方案 1;如果是高并发场景,选方案 2;如果数据更新频繁,选方案 3。

最后说两句:技术落地要结合实际,别盲目追求 “高大上” 作为开发,咱们都知道技术没有 “最好”,只有 “最合适”。我见过有些同事,为了用 “高大上” 的技术,在小项目里硬上分布式锁、消息队列,最后导致项目复杂度飙升,运维成本增加,反而影响了上线效率。

二级缓存也是一样,不是所有项目都需要用。如果你的项目 QPS 很低,数据库压力不大,那单靠 Redis 甚至本地缓存就够了,没必要强行上二级缓存。只有当你遇到了 “本地缓存数据不一致”“Redis 网络开销大”“缓存击穿” 这些实际问题时,再考虑用二级缓存来解决。

今天分享的 3 种 Spring Boot 二级缓存落地方案,都是我在实际项目中踩过坑、验证过的,你可以根据自己的项目情况参考。如果在集成过程中遇到问题,比如 Redisson 锁没释放、Canal 监听不到数据变化,欢迎在评论区留言,咱们一起讨论解决;如果有更好的方案,也期待你分享出来,咱们互相学习,共同进步! ———————————————— 版权声明:本文为CSDN博主「合鸟啊」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:blog.csdn.net/2501_938738…