之前做內部的支付系統,考慮使用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服務版本和規格,不知道這個有沒有影響。