Redisson RLocalCachedMap 遇上事务就翻车?事务提交后缓存不一致的 3 种实战修复路径
【免费下载链接】redissonRedisson: Valkey & Redis Java Client and Real-Time Data Platform. Sync/Async/RxJava/Reactive API. Over 50 Valkey and Redis based Java objects and services: Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map Reduce, Bloom filter, Spring, Tomcat, Scheduler, JCache API, Hibernate, RPC, local cache..项目地址: https://gitcode.com/GitHub_Trending/re/redisson
Redisson 事务提交成功了,主库数据是对的,但 RLocalCachedMap 在部分实例上仍返回旧值。
先对号入座——你的症状是哪一种?
- 同一实例刚 commit 完,马上读同一个 key 还是旧值,过一会儿又好了
- A 实例提交成功,B 实例持续读旧值,重试也不恢复,只有重启才正常
- 问题偶发,且基本都出现在断连、Redis 主从切换前后
- 高并发扣减后,读接口拿到的还是扣减前的库存,直接超卖
四条症状根因相同,但修复成本差很多,所以先分清"旧值能容忍多久"。命中前两条且写量小的,看场景 A;"持续不恢复"或"断连后必现"的,直接跳到场景 B;扣减链路出问题的,直接看场景 C。
事务后本地缓存为什么变旧?一张图讲清楚
先纠正一个常见误区:Redisson 的事务不是 Redis 原生 MULTI/EXEC,而是客户端事务——写操作先对 key 加分布式锁,操作缓存在客户端内存里,commit() 时才整体下发给 Redis,隔离级别是 READ_COMMITTED,见 docs/transactions.md。RLocalCachedMap 则把一份数据存在每个实例的 JVM 里,远端变更不会主动推新值,而是通过 topic 广播失效消息(INVALIDATE)或完整新值(UPDATE),各实例收到后自行更新本地缓存,见 docs/client-side-caching.md。
源码里还有一个动作:事务内第一次写 local cached map 时,Redisson 会先发 LocalCachedMapDisable 消息"屏蔽"该 key 的失效订阅;commit() 时执行完写入,再广播对应的 enable/失效通知。所以"事务内别的实例看不到"是事务语义的代价,不算 bug;真正的损失窗口只有两个——失效消息经 pub/sub 有毫秒级延迟,以及实例恰好断连时消息丢失,而默认重连策略是 NONE,丢了就丢着。
实例 A(事务) Redis 实例 B(本地缓存) put p_1001=99(仅缓存+加锁,未下发) B 本地缓存=旧值 ← 未提交,不可见,正常 commit() ────────> 执行写入、释放锁 经 topic 广播失效/新值 ────────────────────────> B 收到后刷新本地缓存 (B 恰好断连/消息延迟时:B 一直是旧值)RLocalCachedMap 事务提交后的失效广播时序:延迟窗口与消息丢失点
三种场景,三种打法
小写入量、键少:提交后强制刷新
写 QPS 在一百以内、缓存键只有几百个,比如内部工具、低频库存,别折腾策略,用默认 INVALIDATE 加提交后强制刷新就够,因为 key 少,整份失效的代价可以忽略。
RLocalCachedMap<String, Integer> stockCache = redisson.getLocalCachedMap( "product_stock", LocalCachedMapOptions.defaults()); RTransaction tx = redisson.createTransaction(TransactionOptions.defaults()); try { // 事务代理必须传非事务实例获取,代理的写只进事务队列,不碰本地缓存 tx.getLocalCachedMap(stockCache).put("p_1001", 99); tx.commit(); stockCache.clearLocalCache(); // commit 后再广播一次失效,兜住可能漏掉的提交通知 } catch (TransactionException e) { tx.rollback(); }⚠️ 坑点:RTransaction 没有按名称取 local cached map 的方法,只接受传实例;拿错了代理,写根本进不了事务。
失效边界:commit 频率超过每秒几百次就别用了,每次 clearLocalCache 都是全 key 广播,读放大很快盖过省下的网络开销。
读多写少、多实例共享:选对同步策略
读写比大于十比一、多实例共享同一份数据,比如商品详情、配置中心,核心是让提交后的新值主动送到每个实例,同时保证断连后能补上漏掉的消息。
// UPDATE 把完整 key+value 广播给所有实例,读端无需回 Redis 查 LocalCachedMapOptions<String, Integer> options = LocalCachedMapOptions.<String, Integer>defaults() .syncStrategy(SyncStrategy.UPDATE) .reconnectionStrategy(ReconnectionStrategy.LOAD) .evictionPolicy(EvictionPolicy.LRU) .cacheSize(1000); RLocalCachedMap<String, Integer> stockCache = redisson.getLocalCachedMap("product_stock", options); RTransaction tx = redisson.createTransaction(TransactionOptions.defaults()); tx.getLocalCachedMap(stockCache).put("p_1001", 99); tx.commit(); // 提交时 Redisson 自动经 topic 广播新值,无需手动刷新LOAD 会把失效记录进一份保留 10 分钟的日志,断连后 10 分钟内重连会自动补刷对应 key——这正是把"持续不恢复"症状修掉的配置,默认的 NONE 则什么都不做。
⚠️ 坑点:UPDATE 广播的是完整 key 加 value,value 超过 1KB 时带宽随实例数成倍放大,这种场景退回 INVALIDATE,让读端自己回源。
失效边界:写频率上来后广播消息开始堆积,或业务要求"提交后立刻读到的必须是新值",说明本地缓存已经不适合这条链路,转场景 C。
高并发写、强一致:放弃本地缓存
秒杀、库存扣减、金融类,读接口返回一个旧值就是资损事故,这类链路里 RLocalCachedMap 本身就是错误工具,因为本地缓存的失效是最终一致的,而扣减要求强一致。
RLock lock = redisson.getLock("stock_lock:p_1001"); if (!lock.tryLock(1, 3, TimeUnit.SECONDS)) { throw new IllegalStateException("扣减繁忙,稍后重试"); } try { // 扣减链路用普通 map,彻底绕开本地缓存 RMap<String, Integer> stockMap = redisson.getMap("product_stock"); int current = stockMap.get("p_1001"); if (current <= 0) throw new IllegalStateException("库存不足"); // CAS:即使锁被误用,也不会基于旧值覆盖写 stockMap.fastPut("p_1001", current - 1, current); } finally { lock.unlock(); }锁保证临界区内没有别的写者,fastPut 保证就算锁逻辑写错,旧值也不会被覆盖,两层兜底。读库存的接口同样直接读普通 map,不走任何带本地缓存的实例。
失效边界:锁竞争到几千 QPS 以上、等待队列开始堆积,就该改成队列化预扣减或原子 decrement,别再上锁。
配置速查 & 避坑清单
| 配置项 | 推荐值 | 一句话作用 | 去哪看 |
|---|---|---|---|
| syncStrategy | 读多写少用 INVALIDATE,小 value 用 UPDATE | 决定提交后广播的是失效标记还是完整新值 | client-side-caching.md |
| reconnectionStrategy | LOAD | 断连重连后补刷漏掉的失效消息,修"不恢复" | client-side-caching.md |
| cacheSize | 1000–10000 | 0 表示无上限,-1 表示永不缓存 | collections.md |
| transaction timeout | 5s | 超时未 commit 自动回滚,防锁悬挂 | transactions.md |
| responseTimeout | 3s | commit 等 Redis 响应的超时 | transactions.md |
- ⚠️ 事务 map 只接受传非事务实例,不存在按名称创建
- ⚠️ syncStrategy 设 NONE 后缓存永不失效,只剩 TTL 兜底
- ⚠️ 同一个 key 别混用带本地缓存和不带的实例
- ⚠️ 集群模式用 {} hash tag 让事务相关 key 落同一 slot
延伸阅读
- docs/transactions.md — 事务支持的对象清单、READ_COMMITTED 隔离级别与全部超时参数
- docs/client-side-caching.md — SyncStrategy 三种取值、重连策略与本地缓存的完整配置项
- RedissonTransaction.java — commit 时对本地缓存广播 LocalCachedMapDisable/Enable 的完整处理逻辑
【免费下载链接】redissonRedisson: Valkey & Redis Java Client and Real-Time Data Platform. Sync/Async/RxJava/Reactive API. Over 50 Valkey and Redis based Java objects and services: Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map Reduce, Bloom filter, Spring, Tomcat, Scheduler, JCache API, Hibernate, RPC, local cache..项目地址: https://gitcode.com/GitHub_Trending/re/redisson
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考