阿里云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服务版本和规格,不知道这个有没有影响。

posted on 2022-03-27 19:36  暗巷点灯  阅读(740)  评论(0编辑  收藏  举报