Redisson 如何用 RFencedLock 获取围栏令牌保护外部资源
【免费下载链接】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
当你用分布式锁保护一个外部资源(数据库、对象存储、下游系统)的写入时,会面临一个普通RLock无法覆盖的问题:持锁的客户端可能因长时间 GC 暂停或网络故障被挂起,期间锁过期、其他客户端拿到锁并写入;等第一个客户端恢复后,它仍然以为自己持有着锁,继续写入就会造成脏写。Redisson 的RFencedLock就是为这个场景设计的:它在每次成功获取锁时返回一个单调递增的 fencing token,被保护的资源端记录自己接受过的最高令牌,凡是携带更低令牌的操作一律拒绝。这样即使持锁客户端"失忆"复活,它的写入也会被资源端挡下(fenced out)。
适用前提:Java 项目已通过 Redisson 客户端连接到 Valkey 或 Redis;被锁保护的写入方是能够接收并校验 token 的外部系统。如果锁只是用于进程间/节点间的协调,不涉及外部系统强制校验,文档给出的建议是直接用普通 RLock 即可。
为什么普通锁不够:令牌机制解决的场景
Redisson 的分布式锁是协作式(advisory)的,安全性依赖部署拓扑。文档用这样一个序列说明故障窗口:客户端 A 在主节点上成功获取锁后,主节点在锁写入尚未同步到副本时宕机,副本被提升为新主且没有该锁的记录,客户端 B 随即成功获取"同一把锁",两个客户端都以为自己持锁。
Redisson 默认通过副本同步检查(checkLockSyncedSlaves,默认开启;等待超时slavesSyncTimeout默认 1000 毫秒,超时后释放锁并让本次获取失败)来收窄这个窗口。而 fencing token 解决的是剩余风险:把"我认为我持锁"变成资源端可以核验的事实。RFencedLock每次获取都返回一个新令牌,资源端只接受不低于已见最高值的令牌。文档同时指出,已弃用的RedLock由RLock(配合副本同步检查)和RFencedLock(需要围栏令牌时)取代。
准备:创建客户端并获取 RFencedLock
先在项目中添加 Redisson 依赖(源文档中版本号以xVERSIONx占位,替换为你实际使用的版本):
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>xVERSIONx</version> </dependency>然后按 configuration.md 的 Single mode 一节创建客户端(以单节点为例,地址替换为你自己的 Valkey/Redis 地址):
Config config = new Config(); // use "redis://" for Redis connection // use "valkey://" for Valkey connection // use "rediss://" for Redis SSL connection config.useSingleServer().setAddress("redis://myredisserver:6379"); RedissonClient redisson = Redisson.create(config);RedissonClient是线程安全的,创建一次后在应用内复用即可,应用退出时调用redisson.shutdown()。
获取围栏锁对象:
RFencedLock lock = redisson.getFencedLock("myLock");getFencedLock(name)在 RedissonClient 中的说明为:实现的是 non-fair 锁,不保证线程间的获取顺序。
主路径:获取锁、拿到令牌、交给资源端校验
同步 API 的完整流程如下,取自 locks-and-synchronizers.md 的 Fenced Lock 专节:
RFencedLock lock = redisson.getFencedLock("myLock"); // 阻塞获取锁,成功时返回递增后的 fencing token Long token = lock.lockAndGetToken(); try { // 将 token 传给被保护的资源,资源端必须拒绝任何 // token 低于其已接受最高值的写入 storage.write(data, token); } finally { lock.unlock(); }要点逐步说明:
lockAndGetToken()在锁被占用时阻塞等待,直到获取成功,返回Long类型的令牌;无需判空。被保护资源的校验规则由文档明确给出:资源端"记录已见最高令牌,拒绝携带更低令牌的操作",即校验
token >= 已接受的最大值,不满足则拒绝本次操作。文档示例中这一逻辑以注释形式给出(check if token >= old token),下面是一个按该规则实现的最小示例,具体存储操作由你自己的系统完成:// 示例:资源端对令牌做单调性校验(规则来自文档,存储操作自行替换) long lastAcceptedToken = 0; void write(Object data, long token) { if (token < lastAcceptedToken) { // 令牌低于已接受最大值:持锁方已过期,拒绝操作 throw new IllegalStateException("stale fencing token: " + token); } lastAcceptedToken = token; // ... 这里执行真实的写存储操作 }unlock()放在finally中,确保异常路径也能释放锁。注意只有获取锁的线程可以解锁,否则抛出IllegalMonitorStateException。
判断本次获取是否成功:lockAndGetToken()返回非空令牌即成功;改用tryLockAndGetToken(...)时,返回null表示在等待窗口内没有拿到锁,此时不能执行受保护操作,也无需调用unlock()。
验证令牌行为:单调递增
仓库自带测试 RedissonFencedLockTest 展示了可核对的验证方式:对同一把锁连续获取,令牌依次为 1、2、3、4、5;同时验证了普通lock()之后调用getToken()能拿到当前值(该测试中为 3、4),tryLockAndGetToken()成功时返回令牌 5。据此可以做两项检查:
- 递增检查:同一把锁连续获取两次,第二次
lockAndGetToken()返回的令牌应大于第一次;getToken()在不获取锁的情况下返回当前令牌。 - 围栏检查:用一个低于资源端已接受最高值的令牌调用写入,资源端应拒绝该操作——这正是被挂起后复活的过期持锁方会被挡下的效果。
可选的获取变体
文档为同一把锁提供了多个获取入口,按场景选用:
带租约时间,到期自动释放(对应leaseTime参数):
// 获取锁并在 10 秒后自动解锁 Long token = lock.lockAndGetToken(10, TimeUnit.SECONDS);限时尝试获取,拿不到就放弃:
// 最多等待 100 秒,持有 10 秒后自动释放;未获取到时返回 null Long token = lock.tryLockAndGetToken(100, 10, TimeUnit.SECONDS); if (token != null) { try { // 执行带令牌校验的资源写入 ... } finally { lock.unlock(); } }异步 API 下返回RFuture<Long>,方法名带Async后缀;文档示例中unlockAsync(threadId)需要显式传入线程 id:
RFencedLock lock = redisson.getFencedLock("myLock"); RFuture<Long> lockFuture = lock.tryLockAndGetTokenAsync(100, 10, TimeUnit.SECONDS); long threadId = Thread.currentThread().getId(); lockFuture.whenComplete((token, exception) -> { if (token != null) { try { // 执行带令牌校验的资源写入 ... } finally { lock.unlockAsync(threadId); } } });Reactive 与 RxJava3 的对应方法为lockAndGetToken()/lockAndGetToken(leaseTime, unit)/tryLockAndGetToken(waitTime, leaseTime, unit),返回Mono<Long>/Single<Long>。文档对锁类对象有一段通用提示:在异步、Reactive、RxJava3 API 中,一次逻辑操作可能跨多个线程执行,而锁的归属绑定线程 id,因此要捕获Thread.currentThread().getId()并把同一个threadId传给lock、tryLock、unlock调用。
行为细节与限制
- 可重入、非公平:
RFencedLock是可重入锁(接口 RFencedLock 的说明),但不保证跨线程的获取顺序。 - 解锁归属:只有所属线程能解锁,否则抛
IllegalMonitorStateException。 - 看门狗:持锁的 Redisson 实例崩溃时锁可能一直处于已获取状态;看门狗在持有方存活期间续期锁的过期时间,默认 30 秒,可通过
Config.lockWatchdogTimeout修改。 - 副本同步检查:
checkLockSyncedSlaves(默认开启)与slavesSyncTimeout(默认 1000 毫秒)作用于RLock、RSemaphore、RPermitExpirableSemaphore;RFencedLock继承RLock,共享同一套锁机制。开启检查后,若必需副本未在规定时间内确认同步,Redisson 会释放锁并让本次获取失败,客户端不会保留一把可能因故障转移丢失的锁。 - 适用边界:文档明确 fenced lock 用于"锁保护的是能够强制校验 token 的外部系统写入";纯进程内协调用普通
RLock。令牌本身只是数值,Redisson 不会替你调用资源端做校验,拒绝低令牌的逻辑必须由被保护方实现。
下一步
令牌获取与校验的完整方法签名见 RFencedLock.java(含lockAndGetToken、tryLockAndGetToken、getToken各重载),锁类型选型对比表见 locks-and-synchronizers.md 开头的 "Choosing a lock",客户端创建与连接模式见 getting-started.md。
【免费下载链接】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),仅供参考