基于Redis的支持溢出(强制获取)的分布式信号量解决方案咨询(Spring Boot 3 + Java 17)
基于Redis的支持溢出(强制获取)的分布式信号量解决方案咨询(Spring Boot 3 + Java 17)
嗨,我完全理解你的需求——想要一个类似信号量但允许“超额抢占”的分布式组件,哪怕许可耗尽也能强制获取,同时要保证所有操作原子性,还要适配Spring Boot 3 + Java 17环境。先直接给你梳理下情况:
有没有现成的开箱即用方案?
很遗憾,目前主流的Redis分布式信号量实现(比如Redisson的RSemaphore、Jedis的Semaphore相关组件)都没有直接支持这种“溢出”特性。Redisson的RSemaphore是严格基于许可数的,不允许超额获取,所以确实没有现成的、经过充分测试的官方组件能直接满足你的需求,自定义实现是绕不开的,但我们可以优化你的初始方案,解决效率问题。
你的初始方案的问题分析
你用RSemaphore加RAtomicLong的思路是对的,但核心问题出在release()方法的循环CAS逻辑上:在高并发场景下,多个客户端同时调用release时,会频繁触发CAS重试,导致大量的Redis往返调用,既增加了Redis的负载,也会让客户端的延迟不稳定。
优化方案:用Redis Lua脚本原子化处理逻辑
Redis的Lua脚本是解决这类复杂原子操作的黄金方案——因为Lua脚本在Redis端是原子执行的,能把原来客户端需要多次往返的逻辑打包成单个Redis命令执行,彻底消除竞态条件和多次调用的开销。
下面是基于Redisson的优化实现,用Lua脚本重写forceAcquire和release方法:
import org.redisson.api.RAtomicLong; import org.redisson.api.RSemaphore; import org.redisson.api.RScript; import org.redisson.api.RedissonClient; import org.redisson.client.codec.StringCodec; import java.util.Collections; public class OptimizedOverflowableSemaphore { private final RSemaphore semaphore; private final RAtomicLong overflowCounter; private final RedissonClient redissonClient; // 预定义Lua脚本,缓存SHA1避免重复传输 private static final String FORCE_ACQUIRE_SCRIPT = """ local semaphoreKey = KEYS[1] local overflowKey = KEYS[2] -- 尝试获取信号量许可 local acquired = redis.call('semaphore.tryAcquire', semaphoreKey, 1, 0) if acquired == 1 then return 1 end -- 获取失败,增加溢出计数器 redis.call('incr', overflowKey) return 0 """; private static final String RELEASE_SCRIPT = """ local semaphoreKey = KEYS[1] local overflowKey = KEYS[2] -- 先检查溢出计数器 local currentOverflow = redis.call('get', overflowKey) if currentOverflow and tonumber(currentOverflow) > 0 then redis.call('decr', overflowKey) return 1 end -- 没有溢出,释放信号量许可 redis.call('semaphore.release', semaphoreKey, 1) return 0 """; public OptimizedOverflowableSemaphore(RedissonClient redissonClient, int initialPermits, String semaphoreName, String overflowCounterName) { this.redissonClient = redissonClient; this.semaphore = redissonClient.getSemaphore(semaphoreName); this.overflowCounter = redissonClient.getAtomicLong(overflowCounterName); // 初始化信号量许可(仅第一次创建时生效) this.semaphore.trySetPermits(initialPermits); // 初始化溢出计数器为0(仅第一次创建时生效) this.overflowCounter.compareAndSet(0, 0); } public boolean acquire() { return semaphore.tryAcquire(); } public void forceAcquire() { // 执行Lua脚本,原子完成"尝试获取+溢出计数"逻辑 redissonClient.getScript(StringCodec.INSTANCE).eval( RScript.Mode.READ_WRITE, FORCE_ACQUIRE_SCRIPT, RScript.ReturnType.INTEGER, Collections.singletonList(semaphore.getName()), overflowCounter.getName() ); } public void release() { // 执行Lua脚本,原子完成"优先减少溢出+释放许可"逻辑 redissonClient.getScript(StringCodec.INSTANCE).eval( RScript.Mode.READ_WRITE, RELEASE_SCRIPT, RScript.ReturnType.INTEGER, Collections.singletonList(semaphore.getName()), overflowCounter.getName() ); } // 可选:获取当前溢出数量 public long getOverflowCount() { return overflowCounter.get(); } }
优化点说明
- 减少网络IO:原来的
release可能需要多次CAS重试,现在一次Lua脚本调用就完成所有逻辑,彻底消除了客户端和Redis之间的多次往返。 - 更强的原子性:整个
forceAcquire和release的逻辑在Redis端原子执行,完全避免了客户端竞态条件,比如多个线程同时调用forceAcquire时不会出现计数错误。 - 性能稳定:高并发场景下不会出现大量重试,Redis的负载更可控,客户端延迟更稳定。
额外注意事项
- Lua脚本缓存:Redisson会自动缓存Lua脚本的SHA1值,后续调用会直接用SHA1执行,避免重复传输脚本内容,进一步优化性能。
- 命名空间隔离:如果你的应用中有多个这类信号量,要给
semaphoreName和overflowCounterName加上唯一的命名空间,避免键冲突。 - 监控与告警:可以定期监控
overflowCounter的值,如果溢出过多,可能意味着你的信号量初始许可设置不合理,需要调整或者触发告警。 - 测试覆盖:虽然是自定义实现,但基于Lua脚本的逻辑比客户端CAS重试更易测试,你可以用Redisson的测试框架模拟高并发场景,验证原子性和正确性。
如果实在不想完全自定义,也可以考虑基于Redisson的RCountDownLatch或其他组件组合,但本质上还是需要类似的原子逻辑处理,不如直接用Lua脚本的方案简洁高效。
内容来源于stack exchange




