Redisson RLocalCachedMap:分布式本地缓存同步机制与实战指南
1. 从一次线上告警说起为什么需要本地缓存映射那天晚上系统监控突然弹出一连串的告警核心服务的接口响应时间从平时的几十毫秒飙升到了几秒。紧急排查后发现问题出在一个高频访问的配置数据查询接口上。这个接口背后是一个存储在Redis里的Hash结构每次请求都要去Redis里查一次。平时QPS不高Redis完全能扛住但那天晚上因为一个运营活动流量瞬间翻了十倍Redis的网络I/O和命令处理成了瓶颈大量请求在等待Redis的响应整个链路就堵死了。这其实就是分布式缓存的一个经典痛点对于读多写少、数据量不大但访问极其频繁的热点数据每一次都走网络去访问远程缓存如Redis网络延迟和Redis自身的处理开销会成为性能天花板。当时我们团队的第一反应是“加一层本地缓存吧比如Caffeine或者Guava Cache。”这个思路没错但马上又引出了新的问题如何保证本地缓存和Redis缓存的数据一致性如果我在A服务实例的本地缓存里更新了数据怎么让B、C、D其他几十个实例的本地缓存失效自己写一套发布订阅来同步想想就头大而且很容易出Bug。就在我们纠结于“造轮子”还是“忍受延迟”的时候Redisson的RLocalCachedMap进入了视野。它本质上是一个实现了java.util.concurrent.ConcurrentMap接口的分布式映射但它的魔法在于为每个连接到Redis的客户端也就是我们的每个服务实例都自动维护了一个本地缓存副本。当你调用get(key)时它会优先从本地的内存里查找命中则直接返回速度堪比操作本地HashMap如果本地没有再去Redis里取并自动载入本地缓存。而对于数据的更新和失效Redisson通过Redis的Topic通道在后台默默地帮所有实例同步了。这简直是为我们这种场景量身定做的解决方案。所以如果你也在为类似的问题烦恼——那些需要极低读取延迟、数据一致性要求又比较宽松最终一致的分布式配置、热点数据、会话信息等那么RLocalCachedMap就是你工具箱里一件值得深入研究的利器。它不是一个简单的缓存包装器而是一套将本地内存与分布式存储无缝衔接的机制。接下来我们就抛开概念直接深入到它的内部看看怎么用以及更重要的怎么用好。2. RLocalCachedMap 核心机制拆解不只是缓存那么简单理解RLocalCachedMap不能只把它当成一个“带本地缓存的Map”。它的设计精巧之处在于平衡了性能与一致性其内部运作机制可以拆解为几个关键部分。2.1 双存储结构本地内存与Redis的协同RLocalCachedMap实例内部持有两个核心引用本地缓存 (Local Cache): 通常是一个基于JVM内存的缓存实现比如Caffeine。它存储了键值对的实际数据副本。这个缓存是每个服务实例独享的访问它没有网络开销。后端映射 (Backing Map): 一个Redisson的RMap实例数据实际持久化存储在Redis服务器中。它是所有实例共享的单一数据源也是数据一致性的最终裁决者。当你进行读写操作时流程是这样的读操作 (get,containsKey): 首先查询本地缓存。如果命中本地缓存有效且未过期直接返回性能最优。如果未命中本地没有或已失效则转向后端RMap从Redis获取数据。获取成功后根据预设的同步策略将数据写回本地缓存供后续快速读取。写操作 (put,remove): 所有的写操作都会直接作用于后端的RMap即直接更新Redis。这是保证数据最终一致性的基础。在更新完Redis后Redisson会根据配置通过消息通知其他所有持有该RLocalCachedMap的实例让它们更新或失效对应的本地缓存条目。2.2 缓存同步的神经基于Redis Pub/Sub的失效机制这是RLocalCachedMap的“灵魂”。如果没有同步本地缓存就变成了“脏缓存”数据完全不可信。Redisson利用Redis的发布订阅(Pub/Sub)功能建立了一个高效的失效通道。主题订阅: 每个RLocalCachedMap实例在初始化时都会订阅一个特定的Redis Topic这个Topic的命名通常与Map的名字相关例如redisson_local_cache_map:{mapName}。失效消息发布: 当任何一个实例对Map进行了修改操作put,putAll,remove,clear等它在完成对Redis的写操作后会立即向这个Topic发布一条消息。这条消息包含了被修改的键或清空指令。本地缓存失效: 所有订阅了该Topic的实例包括发起修改的实例自己都会收到这条消息。收到消息后它们会根据消息内容从自己的本地缓存中移除对应的键值对而不是更新为新值。这是关键点它采用“失效”而非“更新”策略保证了下次读取时能从Redis获取最新值避免了复杂的并发更新冲突。这种基于消息的失效机制实现了跨JVM、跨机器的本地缓存协同使得数据在短暂延迟后网络传输时间达到最终一致。2.3 丰富的本地缓存策略应对不同场景RLocalCachedMap提供了多种缓存策略通过LocalCachedMapOptions来配置这是它灵活性的体现失效策略 (Eviction Policy): 定义本地缓存何时失效。LFU: 最不经常使用。淘汰一段时间内使用次数最少的条目。适合长期热点相对稳定的场景。LRU: 最近最少使用。淘汰最久未被访问的条目。这是最常用的策略符合时间局部性原理。SOFT: 软引用。让垃圾回收器来决定何时清除缓存。可以防止OutOfMemoryError但行为不确定性高生产环境慎用。NONE: 永不失效除非收到同步消息或达到容量上限。适用于数据几乎不变化或你完全依赖同步消息来管理一致性的场景。WEAK: 弱引用。比SOFT更激进GC时可能立即回收。生产环境更不推荐。同步策略 (Sync Strategy): 定义在从Redis加载数据到本地缓存时如何与其他实例同步。INVALIDATE:默认策略。当本地缓存未命中并从Redis加载后不发送任何同步消息。其他实例的本地缓存条目只有在明确的写操作触发时才会失效。一致性最弱性能最好。UPDATE: 当本地缓存未命中并从Redis加载后会向其他实例发送一条消息让它们也更新对应的本地缓存值为刚加载的新值。这能更快地同步数据但增加了网络消息量。NONE: 完全不发送同步消息。本地缓存只通过写操作触发的失效消息来同步。一致性最差仅适用于只读或容忍长期不一致的场景。淘汰算法 (Reconnection Strategy): 定义在客户端与Redis断开连接并重连后本地缓存如何处理。CLEAR: 重连后清空整个本地缓存。最安全因为断开期间可能错过了很多失效消息。LOAD: 重连后不清空但之后对每个缓存项的访问都会触发一次向Redis的重新验证如果配置了storeCacheMiss的话。折中方案。NONE: 重连后什么也不做。风险最高可能导致长期脏数据。理解这些策略的差异是正确使用RLocalCachedMap的前提。比如对于电商的商品库存写频繁你可能要用INVALIDATE策略因为每次扣库存都会发失效消息本地缓存能快速失效。而对于省份编码表几乎只读用UPDATE甚至NONE策略并设置较长的存活时间可以最大化性能。3. 手把手集成与基础使用从Spring Boot开始理论讲完了我们来看怎么把它用起来。这里以Spring Boot项目为例因为这是目前最主流的集成方式。3.1 环境准备与依赖引入首先在pom.xml中引入Redisson的Spring Boot Starter。注意版本匹配特别是如果你用的是Spring Boot 3.x或更新的4.x。dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.27.0/version !-- 请查看官网使用与你的Spring Boot匹配的最新版本 -- /dependency然后在application.yml中配置Redis连接。单机模式是最简单的spring: redis: host: 127.0.0.1 port: 6379 # password: your-password-if-any database: 0Redisson Starter会自动读取这些配置并创建RedissonClient实例注入Spring容器。如果连接失败你会看到经典的Error creating bean with name redisson defined in class path resource [org/redisson/spring/starter/RedissonAutoConfiguration.class]这类错误这时候就要检查你的Redis服务器是否可用了。3.2 配置与创建RLocalCachedMap实例虽然有了RedissonClient但RLocalCachedMap需要更精细的配置。我推荐使用一个Configuration类来集中配置和管理。import org.redisson.api.LocalCachedMapOptions; import org.redisson.api.RLocalCachedMap; import org.redisson.api.RedissonClient; import org.redisson.client.codec.StringCodec; import org.redisson.codec.JsonJacksonCodec; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.concurrent.TimeUnit; Configuration public class RedissonLocalCacheConfig { Bean(userConfigCache) public RLocalCachedMapString, String userConfigCache(RedissonClient redissonClient) { // 1. 创建本地缓存配置选项 LocalCachedMapOptionsString, String options LocalCachedMapOptions.String, Stringdefaults() // 设置淘汰策略为 LRU 最大容量10000条 .evictionPolicy(LocalCachedMapOptions.EvictionPolicy.LRU) .cacheSize(10000) // 设置缓存项存活时间TTL为10分钟 .timeToLive(10, TimeUnit.MINUTES) // 设置最大空闲时间10分钟未被访问则清除 .maxIdle(10, TimeUnit.MINUTES) // 设置同步策略为 INVALIDATE默认 .syncStrategy(LocalCachedMapOptions.SyncStrategy.INVALIDATE) // 设置重连策略为 CLEAR安全 .reconnectionStrategy(LocalCachedMapOptions.ReconnectionStrategy.CLEAR) // 是否存储缓存未命中的键防止缓存穿透 .storeCacheMiss(false) // 编码器根据值类型选择。String用StringCodec复杂对象用JsonJacksonCodec .codec(StringCodec.INSTANCE); // 2. 从RedissonClient获取或创建RLocalCachedMap // 第一个参数是Map在Redis中的名字第二个是配置选项 return redissonClient.getLocalCachedMap(server:user:config, options); } Bean(productCache) public RLocalCachedMapLong, ProductDTO productCache(RedissonClient redissonClient) { // 存储复杂对象示例 LocalCachedMapOptionsLong, ProductDTO options LocalCachedMapOptions.Long, ProductDTOdefaults() .evictionPolicy(LocalCachedMapOptions.EvictionPolicy.LFU) // 商品信息用LFU可能更合适 .cacheSize(5000) .timeToLive(30, TimeUnit.MINUTES) .syncStrategy(LocalCachedMapOptions.SyncStrategy.UPDATE) // 商品信息读多写少用UPDATE加快同步 .codec(new JsonJacksonCodec()); // 使用JSON序列化 return redissonClient.getLocalCachedMap(global:product:cache, options); } }关键配置项解读evictionPolicy和cacheSize: 决定了本地缓存的内存使用行为和上限。一定要根据业务数据量和内存情况设置一个合理的cacheSize防止内存溢出。timeToLive和maxIdle: 提供了两层过期保障。即使没有写操作触发失效缓存项也不会永久驻留。timeToLive是绝对过期时间maxIdle是相对过期时间。syncStrategy: 这是性能与一致性权衡的关键。根据业务场景选择INVALIDATE或UPDATE。storeCacheMiss: 设置为true时连“键不存在”这个结果也会被缓存一段时间可以有效防御缓存穿透攻击。但需要确保你的业务逻辑能正确处理“空值”缓存并设置较短的TTL。codec: 序列化编解码器。简单字符串用StringCodec效率最高。复杂对象推荐JsonJacksonCodec需确保类有无参构造且字段可序列化或KryoCodec性能更高但需注册类。3.3 基础API操作示例创建好Bean之后在Service中注入就可以像操作普通ConcurrentMap一样使用它了。import org.redisson.api.RLocalCachedMap; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import java.util.Map; import java.util.concurrent.ConcurrentMap; Service public class UserConfigService { Autowired Qualifier(userConfigCache) // 注入我们配置的Bean private RLocalCachedMapString, String configCache; /** * 获取用户配置优先走本地缓存 */ public String getUserConfig(String userId, String configKey) { String fullKey userId : configKey; // get操作会自动触发本地缓存逻辑 String value configCache.get(fullKey); if (value null) { // 本地和Redis都没有从数据库加载 value loadFromDatabase(userId, configKey); if (value ! null) { // 存入缓存会同时写入Redis和本地缓存 configCache.put(fullKey, value); } } return value; } /** * 更新用户配置会触发分布式失效 */ public void updateUserConfig(String userId, String configKey, String configValue) { String fullKey userId : configKey; // put操作会更新Redis并通过Pub/Sub使其他实例的本地缓存失效 configCache.put(fullKey, configValue); // 如果需要这里可以再更新数据库 updateDatabase(userId, configKey, configValue); } /** * 批量操作也是支持的 */ public void refreshAllConfigs(MapString, String newConfigs) { configCache.putAll(newConfigs); // 批量putAll会触发批量失效 } /** * 获取本地缓存统计信息监控用 */ public void printCacheStats() { // RLocalCachedMap 提供了本地缓存的一些监控方法 ConcurrentMapString, String localCacheView configCache.getCachedMap(); System.out.println(本地缓存大小: localCacheView.size()); // 注意这里获取的是本地缓存的视图不是全部数据 } private String loadFromDatabase(String userId, String configKey) { // 模拟从数据库加载 return value_from_db_for_ configKey; } private void updateDatabase(String userId, String configKey, String configValue) { // 模拟更新数据库 } }可以看到使用层面非常简单get和put的语义清晰。复杂的同步、失效、淘汰逻辑都被Redisson封装在底层了。4. 进阶配置与性能调优实战基础用法能解决80%的问题但要想发挥RLocalCachedMap的最大威力或者应对一些极端场景就需要深入了解其进阶特性和调优点。4.1 序列化编解码器Codec的选型与坑编解码器负责在JVM对象和Redis存储的字节数组之间转换。选错Codec会导致性能瓶颈、内存浪费甚至功能错误。StringCodec: 仅适用于键和值都是String类型的场景。它是效率最高的因为Redis原生支持字符串。如果你的值是对象千万不要用它否则存进去的是对象的toString()结果取出来就变不回对象了。JsonJacksonCodec: 最通用、最安全的选择。它使用Jackson库将对象序列化为JSON字符串。确保你的类有无参构造函数并且字段有正确的getter/setter或配置了JsonProperty。坑点1对循环引用的对象如双向关联的父子对象默认会序列化失败需要配置ObjectMapper来禁用循环检测或使用JsonIgnore。坑点2修改类的字段结构增删改后旧缓存数据可能反序列化失败。可以考虑为类添加JsonIgnoreProperties(ignoreUnknown true)来忽略未知字段。KryoCodec/FstCodec: 高性能二进制序列化方案。序列化后的体积更小速度更快。但坑点巨大序列化格式是二进制的且不同版本的Kryo序列化结果可能不兼容。一旦你升级了Kryo版本或修改了类结构之前存储在Redis里的数据很可能再也读不出来了。生产环境慎用除非你有严格的版本控制和数据迁移方案。SerializationCodec: 使用JDK原生序列化。最不推荐序列化后体积大、速度慢且同样有类版本兼容性问题。最佳实践建议对于生产环境优先使用JsonJacksonCodec。在创建RedissonClient时全局配置或者在每个RLocalCachedMap的Options中单独指定。Bean public RedissonClient redissonClient(RedisProperties properties) throws IOException { Config config new Config(); config.useSingleServer() .setAddress(redis:// properties.getHost() : properties.getPort()) .setPassword(properties.getPassword()); // 全局默认使用JsonJacksonCodec config.setCodec(new JsonJacksonCodec()); return Redisson.create(config); }4.2 内存管理与容量规划本地缓存是放在JVM堆内存里的管理不当会导致OutOfMemoryError。cacheSize: 这不是一个严格的“条目数”限制而是给底层缓存库如Caffeine的一个建议容量。Caffeine的maximumSize策略是近似LRU/LFU可能会略微超过。你需要根据缓存对象平均大小和可用堆内存来计算。例如对象平均1KB堆内存给缓存预留1G那么cacheSize可以设为(1024*1024) / 1 ≈ 1000000不太激进了要留有余地设为20万或50万更安全。evictionPolicy: 再次强调LRU适合大多数访问模式。如果你的热点数据是“最近发布的文章”LRU很好。如果是“经典商品详情”长期被访问但可能某段时间不访问LFU可能保留得更久。SOFT/WEAK引用策略把控制权交给了GC在内存压力大时行为不可预测生产环境避免使用。监控与预警: 务必通过JMX或Micrometer等工具暴露本地缓存的命中率、淘汰数量、平均加载时间等指标。当命中率持续走低或淘汰数异常高时可能意味着cacheSize设置过小或者数据访问模式发生了变化。4.3 同步策略的深度选择与一致性权衡INVALIDATE和UPDATE的选择本质上是“一致性延迟”与“网络开销/缓存利用率”的权衡。选择INVALIDATE(默认) 的场景:写后立即读的概率低数据更新后不是所有实例都需要立刻读到新值。写操作频繁如果每次写都触发UPDATE广播网络消息量会很大。INVALIDATE只广播一个键名消息体积小。可以接受短暂不一致例如用户更新了自己的昵称其他用户在几秒后看到旧名字是可以接受的。下次点击该用户主页时本地缓存失效从Redis读取新值即可。实战心得这是最常用的策略。它的效果是一个实例更新数据后其他实例的本地缓存中该数据变为“无效”但内存中的旧数据对象还在只是被标记为无效。下次get时发现无效才去Redis拉取新值。这减少了一次网络传输不用传输新值本身。选择UPDATE的场景:对一致性要求稍高希望变更尽快扩散比如全局开关配置希望所有实例在几毫秒内感知到变化。值本身很小但读取非常频繁广播更新值的开销可以接受但可以避免大量实例同时缓存失效去Redis查同一个键造成的“惊群效应”。数据量小但网络不是瓶颈。坑点UPDATE消息携带了完整的值。如果值很大比如一个复杂的DTO频繁更新会导致大量的网络带宽消耗。务必确保用于UPDATE策略的Map其存储的值体积是小的、可控的。一个折中方案对于某些关键配置你可以不用RLocalCachedMap的同步而是直接使用Redisson的RMapCacheRTopic监听。在RMapCache上设置较短的TTL每个实例独立缓存并定时过期。虽然一致性延迟等于TTL但实现更简单没有复杂的同步逻辑。这需要根据业务容忍度来抉择。4.4 连接丢失与重连策略的处理网络是不稳定的。当客户端与Redis断开连接时RLocalCachedMap的行为取决于reconnectionStrategy。CLEAR: 最安全。重连后本地缓存全部清空。因为断开期间可能错过了很多失效消息本地缓存可能已经脏了。清空是最彻底的做法。代价是重连后的第一波请求会全部穿透到Redis可能引发雪崩。如果你的应用重启或网络闪断很频繁需要评估这个风险。LOAD: 重连后不清空但之后每次访问缓存项时如果配置了storeCacheMiss或某些条件会去Redis做一次验证。比CLEAR温和但逻辑复杂一致性状态模糊。NONE: 什么也不做。本地缓存数据被当作“圣旨”继续使用直到被正常的失效策略或写操作失效。风险极高除非你能保证断开连接期间绝对没有写操作否则可能导致分钟级别甚至永久的数据不一致。生产环境建议优先使用CLEAR策略。为了应对清空缓存后的请求雪崩你需要有额外的保护措施应用级限流在服务入口或调用缓存的方法上增加限流防止大量请求瞬间压垮Redis或DB。使用storeCacheMiss缓存空值配合较短的TTL如5秒即使缓存清空对于不存在的键短时间内也不会穿透到DB。设计降级方案当检测到Redis连接异常时直接走降级逻辑如返回默认值、使用一个只读的本地静态配置而不是依赖可能脏掉的本地缓存。5. 生产环境避坑指南与常见问题排查纸上得来终觉浅绝知此事要踩坑。下面是我和团队在大量使用RLocalCachedMap后总结出的“血泪教训”。5.1 内存泄漏未正确关闭资源RLocalCachedMap实例内部持有线程池、监听器、网络连接等资源。如果你在非Spring管理的环境下比如单元测试手动创建它或者动态地创建和丢弃大量不同名称的Map必须记得在不用时调用destroy()方法或者对RedissonClient实例调用shutdown()。// 错误示范在长时间运行的应用中不断创建新的Map而不销毁 for (int i 0; i 10000; i) { RLocalCachedMap map redissonClient.getLocalCachedMap(temp:map: i, options); map.put(key, value); // 用完没有销毁本地缓存、监听器资源会一直累积 } // 正确做法如果是临时使用确保销毁 RLocalCachedMapString, String tempMap null; try { tempMap redissonClient.getLocalCachedMap(temp:map, options); // ... 使用tempMap } finally { if (tempMap ! null) { tempMap.destroy(); // 销毁这个Map实例释放资源 } }在Spring环境中通常将RLocalCachedMap声明为Bean由Spring容器管理生命周期在应用关闭时RedissonClient的shutdown方法会被调用从而清理所有资源。这是最推荐的方式。5.2 脏读时间窗理解最终一致性这是使用任何带本地缓存的分布式系统必须接受的现实。RLocalCachedMap提供的是最终一致性不是强一致性。时间窗产生原因实例A执行put(key, newValue)。实例A更新Redis成功并立即向失效Topic发布消息invalidate(key)。消息通过网络传输到Redis再由Redis广播给其他订阅者实例B、C...。实例B收到消息将本地缓存中的key标记为失效。在步骤2到步骤4之间存在一个极短的时间窗通常几毫秒到几十毫秒取决于网络。在此期间实例B如果读取key会从自己的本地缓存中读到旧值。如何应对业务容忍首先评估业务是否能接受毫秒级的不一致。对于很多场景如用户昵称、文章点赞数这是完全可以接受的。关键业务强一致对于绝对不能接受脏读的场景如支付状态、库存扣减不要使用RLocalCachedMap。应该直接使用RMap或RMapCache甚至考虑使用Redis事务、Lua脚本或分布式锁来保证强一致性读。降低时间窗使用低延迟的网络、部署在同一个可用区、确保Redis服务器性能可以缩短不一致窗口。5.3 缓存穿透、击穿与雪崩RLocalCachedMap主要解决热点数据的高性能读取但它本身也是缓存通用缓存问题依然存在。缓存穿透查询不存在的数据现象大量请求查询一个Redis和本地缓存中都不存在的key请求穿透到数据库。RLocalCachedMap解决方案配置LocalCachedMapOptions.storeCacheMiss(true)。这样连“查询不到”这个结果也会被缓存一段时间遵循timeToLive。后续请求在缓存有效期内会直接返回null保护了数据库。注意需要业务逻辑能正确处理null值并且给这类“空值缓存”设置一个较短的TTL比如30秒防止数据库真的有数据后无法及时更新。缓存击穿热点key过期瞬间现象一个热点key在本地缓存和Redis中同时过期瞬间大量请求涌向数据库。RLocalCachedMap缓解方案由于本地缓存是每个实例独立的它们的过期时间是独立计算的。一个实例的缓存过期不会导致其他实例的缓存也过期。这本身就分散了风险。此外可以为热点key设置永不过期timeToLive0通过后台任务或写操作来更新它。缓存雪崩大量key同时过期现象大量key在同一时刻过期所有请求落库。RLocalCachedMap缓解方案在设置timeToLive时增加一个随机偏移量避免批量key同时创建同时过期。例如基础TTL是10分钟可以设置为10分钟 Random(0, 120秒)。5.4 监控与运维考量没有监控的系统就是在裸奔。本地缓存指标通过Redisson的API或与Micrometer集成监控每个RLocalCachedMap的本地缓存命中率 (Hit Rate): 这是衡量其价值的核心指标。理想情况应在95%以上。如果过低检查cacheSize是否太小或者数据访问是否毫无局部性。本地缓存大小 (Size): 确认是否在预期范围内。淘汰数量 (Eviction Count): 如果淘汰很频繁说明cacheSize可能不足或者evictionPolicy不合适。Redis监控网络输入/输出流量使用UPDATE策略时关注Pub/Sub通道的流量是否异常。内存使用RLocalCachedMap的数据最终存在Redis的Hash结构中监控内存增长。连接数每个Redisson客户端都会保持与Redis的长连接。日志排查为Redisson客户端开启DEBUG或TRACE级别日志在排查同步问题、连接问题时非常有用。可以看到详细的命令执行、消息发布/订阅记录。5.5 与Spring Cache集成谨慎很多人想用Cacheable注解来优雅地使用RLocalCachedMap。Redisson确实提供了RedissonSpringCacheManager。但这里有个大坑Spring Cache的抽象是面向“方法返回值缓存”的它的缓存失效是基于注解声明的Key。当你通过CachePut更新缓存时Spring Cache只会操作你指定的那个缓存管理器如Redis。它完全不知道RLocalCachedMap的分布式失效机制也就是说通过Spring CacheCachePut更新的数据不会触发RLocalCachedMap的Pub/Sub失效消息导致其他实例的本地缓存脏掉。结论如果你决定使用RLocalCachedMap最好直接使用它的APIget,put而不是通过Spring Cache注解。这样才能确保读写路径都走Redisson的完整逻辑保证一致性。将RLocalCachedMap当作一个增强版的、支持分布式的ConcurrentHashMap来直接操作是最稳妥的方式。RLocalCachedMap是一个强大的工具它用复杂的内部机制换来了对开发者极其简单的API。理解其原理根据业务场景谨慎配置避开常见的陷阱它就能成为你解决分布式高性能读取问题的利器。记住没有银弹它最适合的是那些读多写少、容忍最终一致性的热点数据场景。对于需要强一致性的核心数据请选择更基础的分布式原语。