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