1. 程式人生 > 其它 >阿里雲Redis代理模式對於Redisson鎖的限制

阿里雲Redis代理模式對於Redisson鎖的限制

之前做內部的支付系統,考慮使用Redisson來做分散式鎖。由於生產環境使用的是阿里雲的Redis叢集架構版,文件中有說對於命令和Lua指令碼有一定限制,所以寫個測試程式放上去跑。

測試程式

由於業務系統一直使用的都是阿里雲Redis的代理模式,直接當成單節點使用,所以當時直接使用代理模式去跑。

public class RedissonTest {

    private static final RedissonClient CLIENT = buildSingletonClient();

    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            service.execute(RedissonTest::testLock);
        }
    }

    private static void testLock() {
        System.out.println(Thread.currentThread().getName() + ": begin");
        RLock lock = CLIENT.getLock("lock");
        lock.lock(30, TimeUnit.SECONDS);
        System.out.println(Thread.currentThread().getName() + ": get lock");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
            System.out.println(Thread.currentThread().getName() + ": unlock");
        }
    }

    private static RedissonClient buildSingletonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        return Redisson.create(config);
    }
}

這段程式執行結果:一個執行緒釋放鎖後,其他等待鎖的執行緒無法馬上獲得鎖,而是需要等鎖過期後才能獲得鎖。因為Redisson預設的鎖是基於訂閱/釋出來實現的,所以當時猜測可能和訂閱/釋出有關。然後在阿里雲文件中也找到關於Lua指令碼中使用釋出/訂閱命令的限制。

加鎖

	private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {
            return;
        }

        // 訂閱並同步執行
        RFuture<RedissonLockEntry> future = subscribe(threadId);
        if (interruptibly) {
            commandExecutor.syncSubscriptionInterrupted(future);
        } else {
            commandExecutor.syncSubscription(future);
        }

        try {
            // 嘗試獲取鎖,如果獲取失敗,則阻塞直到收到訂閱頻道的通知訊息,即鎖被釋放,然後再次嘗試獲取鎖,迴圈往復直到超時或者執行緒中斷。
            while (true) {
                ttl = tryAcquire(-1, leaseTime, unit, threadId);
                // lock acquired
                if (ttl == null) {
                    break;
                }

                // waiting for message
                if (ttl >= 0) {
                    try {
                        future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    } catch (InterruptedException e) {
                        if (interruptibly) {
                            throw e;
                        }
                        future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    }
                } else {
                    if (interruptibly) {
                        future.getNow().getLatch().acquire();
                    } else {
                        future.getNow().getLatch().acquireUninterruptibly();
                    }
                }
            }
        } finally {
            unsubscribe(future, threadId);
        }
    }

釋放鎖

    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                        "return nil;" +
                        "end; " +
                        "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                        "if (counter > 0) then " +
                        "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                        "return 0; " +
                        "else " +
                        "redis.call('del', KEYS[1]); " +
                        // 此處在Lua指令碼中使用了publish命令
                        "redis.call('publish', KEYS[2], ARGV[1]); " +
                        "return 1; " +
                        "end; " +
                        "return nil;",
                Arrays.asList(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
    }

自旋鎖

Redisson提供了多種鎖,其中包括自旋鎖。自旋鎖不使用釋出/訂閱機制,而是通過不斷地獲取鎖,失敗則重試。為了進一步正式,將測試程式的鎖換成了自旋鎖getSpinLock(),結果也正式了猜想。

直連模式

阿里雲Redis除了代理模式還有直連模式,可以像連線原生Redis叢集一樣使用。Redisson官方文件中也說了支援阿里雲,所以應該在直連模式下是沒有Lua指令碼的這個限制的。但是由於條件不允許無法繼續使用直連模式來證實這一點。

總結

阿里雲Redis叢集架構版,在代理模式下似乎只能使用Redisson的自旋鎖。阿里雲提供了多種Redis服務版本和規格,不知道這個有沒有影響。