You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

基于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脚本重写forceAcquirerelease方法:

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();
    }
}

优化点说明

  1. 减少网络IO:原来的release可能需要多次CAS重试,现在一次Lua脚本调用就完成所有逻辑,彻底消除了客户端和Redis之间的多次往返。
  2. 更强的原子性:整个forceAcquirerelease的逻辑在Redis端原子执行,完全避免了客户端竞态条件,比如多个线程同时调用forceAcquire时不会出现计数错误。
  3. 性能稳定:高并发场景下不会出现大量重试,Redis的负载更可控,客户端延迟更稳定。

额外注意事项

  • Lua脚本缓存:Redisson会自动缓存Lua脚本的SHA1值,后续调用会直接用SHA1执行,避免重复传输脚本内容,进一步优化性能。
  • 命名空间隔离:如果你的应用中有多个这类信号量,要给semaphoreNameoverflowCounterName加上唯一的命名空间,避免键冲突。
  • 监控与告警:可以定期监控overflowCounter的值,如果溢出过多,可能意味着你的信号量初始许可设置不合理,需要调整或者触发告警。
  • 测试覆盖:虽然是自定义实现,但基于Lua脚本的逻辑比客户端CAS重试更易测试,你可以用Redisson的测试框架模拟高并发场景,验证原子性和正确性。

如果实在不想完全自定义,也可以考虑基于Redisson的RCountDownLatch或其他组件组合,但本质上还是需要类似的原子逻辑处理,不如直接用Lua脚本的方案简洁高效。

内容来源于stack exchange

火山引擎 最新活动