阿里雲Redis代理模式對於Redisson鎖的限制
阿新 • • 發佈:2022-03-27
之前做內部的支付系統,考慮使用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服務版本和規格,不知道這個有沒有影響。