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

相关推荐
黑马程序员毕设32 分钟前
基于B/S架构的“指尖乡味”助农电商小程序系统设计与实现
spring boot·微信小程序·小程序·架构·课程设计·毕设
卓怡学长11 小时前
w176基于SpringBoot的医院管理系统
java·spring boot·spring·maven·intellij-idea
摇滚侠14 小时前
《SpringBoot 3:入门与应用实战》第 7 章 AOP 思想与实现 阅读笔记 15
java·spring boot·笔记
01传说15 小时前
redis开机自启脚本
数据库·redis·缓存
摇滚侠15 小时前
《SpringBoot 3:入门与应用实战》第 7 章 AOP 思想与实现 阅读笔记 14
spring boot·笔记·后端
脉动数据行情115 小时前
Java SpringBoot 对接知名台股 TWSE|批量采集上市股票行情实践
java·开发语言·spring boot
凤山老林17 小时前
Spring Boot 集成 ShardingSphere-Encrypt 实现字段级实时脱敏
java·spring boot·后端·数据脱敏
摇滚侠18 小时前
《SpringBoot 3:入门与应用实战》第 8 章 AOP 的进阶机制和应用 阅读笔记 16
java·spring boot·笔记