SpringBoot整合Caffeine-集群本地缓存如何保证分布式一致性

Spring Boot整合Caffeine-集群本地缓存如何保证分布式一致性

问题现场: Caffeine 适合保存访问频繁、体积有限的本地数据,但每个应用实例都拥有自己的缓存副本。单机缓存一旦进入集群,更新问题就从一次 put 变成多个节点怎样同时失效。

只设置过期时间意味着窗口期内读取旧值;只依赖消息通知又要处理丢消息、节点重启和并发回填。企业级本地缓存必须明确一致性等级,而不是把"速度快"当成完整方案。

MetaLite 在本地缓存之外保留统一失效与二级缓存协作入口。本文先比较几种集群失效策略,再结合缓存封装说明它解决了哪些一致性问题、仍接受哪些短暂不一致。

一、本地缓存一致性问题从哪里产生

假设用户信息被缓存到两个实例:

text 复制代码
A.local[user-1] = 旧数据
B.local[user-1] = 旧数据

更新请求落到 A:

java 复制代码
@CacheEvict(cacheNames = "local", key = "#userId")
public void updateUser(String userId) {
    // 更新数据库
}

Spring 调用 A 的 Caffeine Cache 执行 evict(userId)。如果没有额外机制,B 的本地内存不会发生任何变化。

TTL 最终可以让旧值过期,但在过期前仍然存在陈旧窗口。

二、第一步必须先删除当前实例

CaffeineCache.evict 当前先操作本地 native cache:

java 复制代码
public void evict(Object key) {
    this.cache.invalidate(key);
    caffeineHelper.cacheEvict(this.cacheName, key);
}

这个顺序意味着更新请求所在实例立即失效,不等待网络通知完成。

随后 CaffeineHelper 记录缓存调用日志,并发起其他实例通知。

当前实例删除与远端通知不是一个分布式原子操作。设计目标是让本实例快速完成更新路径,再尽力缩短其他实例的陈旧窗口。

三、为什么通知使用同服务内部 HTTP

通知参数只有两个字段:

text 复制代码
cacheName
cacheKey

框架将它们包装成 InternalBizParamReq<CacheEvictParam>,然后调用:

java 复制代码
internalServiceClient.callAllInstance(
    RpcRequest.builder()
        .provider(EnvUtil.APPLICATION_NAME)
        .endpoint("/api/cache/caffeine/evict")
        .timeoutMillis(3000)
        .param(internalReq)
        .build(),
    true
);

provider 使用当前应用名,表示只通知同一个服务的其他实例;true 表示忽略自身。

这种方式复用了已有能力:

  • Nacos 服务发现;
  • 内部请求协议;
  • HttpClient 连接池和超时;
  • RPC 调用日志。

它不需要为缓存通知额外引入 Redis Pub/Sub 或 MQ,但也继承了内部 HTTP 广播的可用性边界。

四、callAllInstance 实际上是顺序广播

Nacos 代理先调用 getAllInstances(provider) 获取实例列表,再逐个执行 HTTP 调用:

java 复制代码
for (ServiceInstance instance : instances) {
    rpcRequest.setProvider(instance.getApiPrefix());
    callOneInstance(provider, rpcRequest, httpClient);
}

因此,当前广播是顺序 fan-out,不是并行请求。

实例数量较少时实现简单;实例很多时,总耗时可能累积。外层通知使用异步执行,不会让业务线程同步等待整个广播,但异步任务本身仍然逐个调用。

另一个边界是"忽略自身"当前只比较 IP。如果同一主机使用不同端口部署多个同名实例,同机其他实例也可能被一并过滤。

五、远端为什么不能再次调用普通 evict

其他实例收到内部接口:

text 复制代码
POST /api/cache/caffeine/evict

CaffeineApi 将请求交给 CaffeineCacheManager.evictCaffeine。远端代码直接操作 native cache:

java 复制代码
caffeineCache.getNativeCache()
        .invalidate(cacheEvictParam.getCacheKey());

它没有再次调用 CaffeineCache.evict()

如果远端也走普通 evict,B 收到 A 的通知后会再次广播给 A、C;C 又继续广播,最终形成通知风暴甚至循环。

"发起端走带广播的入口,接收端走纯本地失效入口"是这条链路最关键的职责分离。

六、为什么通知放到异步任务中

notifyCaffeineEvict 使用:

java 复制代码
CompletableFuture.runAsync(() -> {
    // 枚举并通知其他实例
});

业务请求完成本地删除后,不需要同步等待全部实例响应。这降低了缓存通知对更新接口延迟的影响。

但当前 runAsync 没有显式传入专用 Executor,因此使用的是默认异步执行设施,不能根据类注释宣传成"缓存通知一定使用专用线程池"。

异步也意味着:接口返回成功时,其他实例可能还没删除完成。

七、通知失败以后会发生什么

广播过程被 try/catch 包裹:

java 复制代码
catch (Throwable throwable) {
    log.warn("notifyCaffeineEvict failed ...");
}

当前实现没有:

  • 自动重试;
  • 失败实例列表持久化;
  • MQ 补偿;
  • 缓存版本号校验;
  • 定时全量对账。

所以它不是强一致协议。

通知失败时,更新接口不会回滚,其他实例可能继续读取旧缓存,直到本地 TTL 到期、进程重启或后续同 Key 删除再次成功通知。

更准确的定位是:异步尽力通知,缩短最终一致窗口。

八、当前只覆盖单 Key 删除

evict(key)evictIfPresent(key) 都会触发通知。

clear()invalidate() 当前只执行:

java 复制代码
cache.invalidateAll();

没有发现对应的跨实例全量清理广播。

因此,文章不能笼统声称"所有 Caffeine 清理操作都会自动同步集群"。当前设计覆盖的是单 Key 失效链路。

全量清理需要额外考虑接口保护、广播规模和误操作风险,不能简单复用单 Key 参数。

九、哪些数据适合这种策略

异步最终一致的本地缓存更适合:

  • 允许短暂旧值的展示数据;
  • 更新频率较低的配置;
  • 有合理 TTL 兜底的数据;
  • 读取性能价值明显的数据。

以下数据应更谨慎:

  • 权限刚被撤销后必须立即生效;
  • 账户余额、库存扣减等强一致数据;
  • 安全黑名单和强制下线状态;
  • 旧值会导致不可逆业务操作的数据。

这些场景可能需要共享缓存、版本校验、同步确认或直接绕过本地缓存。

十、本地缓存集群化的核心不是"发个通知"

完整设计必须回答:

  1. 当前实例何时删除?
  2. 怎样找到同服务的其他实例?
  3. 接收端怎样避免再次广播?
  4. 通知同步还是异步?
  5. 失败是否重试,陈旧窗口由什么兜底?
  6. 单 Key 与全量清理是否使用同一策略?

MetaLite 当前给出了一个轻量答案:本地先删、内部 HTTP 异步顺序广播、远端 native cache 失效、失败只记录警告。

把这些边界同时写清楚,比简单宣称"解决了多实例缓存一致性"更有工程价值。


框架简介

MetaLite 是面向企业生产环境的新一代 Java 微服务技术底座。系列文章重点分享代码背后的设计思路、技术取舍与工程实践。

源码基线

JDK 21、Spring Boot 3.2.9、Spring Cloud 2023.0.1、Spring Cloud Alibaba 2023.0.1.3,具体组件版本以项目 backend-bom 为准。

作者简介

15 年 Spring 体系企业级开发经验,专注于 Java 微服务架构、工程治理与生产实践。

持续更新

MetaLite 系列内容将持续更新,围绕核心设计、源码链路、技术取舍与生产实践展开。欢迎关注作者,及时获取后续内容。

在线演示

演示地址: https://admin.metalite.top/

演示账号: guess

演示密码: admin@2026

相关推荐
李昊哲小课3 小时前
Spring Boot SSE 实时推送教程
spring boot·websocket·flux·sseemitter
wno7044 小时前
Spring Boot WebFlux增删改查
java·spring boot·后端
拼图2095 小时前
Git常用语法
分布式·git·学习
行百里er7 小时前
Spring Insight 里如何把 Span 收成一条链路
spring boot·spring cloud·监控
xcl09257 小时前
24小时自助健身房系统软件开发实战指南:从架构到部署
java·spring boot
吉甫作诵9 小时前
Redis 常用命令大全:11 大类命令速查手册
运维·数据库·redis·缓存·nosql
ID34610744209 小时前
【课程设计】基于Spring Boot+Vue的游戏账号租赁系统的设计与实现-计算机毕设 附源码50345
javascript·vue.js·spring boot·python·node.js·php·课程设计
hweiyu009 小时前
Redis命令:MGET
redis·缓存
小蒜学长12 小时前
基于RFID技术的仓储管理系统设计(代码+数据库+LW)
java·数据库·spring boot·后端·rfid技术·仓储管理
墨天梦12 小时前
07-KVCache与缓存友好架构
缓存·架构