目次
[この記事は redisson-3.17.6 バージョンのソースコードの分析に基づいています]
Redis ベースの Redisson の分散セマフォ RSemaphore は、java.util.concurrent.Semaphore と同様のインターフェイスと使用法を採用しています。
1.RSemaphoreの使用
@Test
public void testRSemaphore() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redissonClient = Redisson.create(config);
RSemaphore rSemaphore = redissonClient.getSemaphore("semaphore");
// 设置5个许可,模拟五个停车位
rSemaphore.trySetPermits(5);
// 创建10个线程,模拟10辆车过来停车
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
try {
rSemaphore.acquire();
System.out.println(Thread.currentThread().getName() + "进入停车场...");
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(100));
System.out.println(Thread.currentThread().getName() + "离开停车场...");
rSemaphore.release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "A" + i).start();
}
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
2. RSemaphore はライセンス数を設定します
RSemaphore を初期化するには、trySetPermits() を呼び出してアクセス許可の数を設定する必要があります。
/**
* 尝试设置许可数量,设置成功,返回true,否则返回false
*/
boolean trySetPermits(int permits);
trySetPermits() は内部で trySetPermitsAsync() を呼び出します。
// 异步设置许可
@Override
public RFuture<Boolean> trySetPermitsAsync(int permits) {
RFuture<Boolean> future = commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 判断分布式信号量的key是否存在,如果不存在,才设置
"local value = redis.call('get', KEYS[1]); " +
"if (value == false) then "
// set "semaphore" permits
// 使用String数据结构设置信号量的许可数
+ "redis.call('set', KEYS[1], ARGV[1]); "
// 发布一条消息到redisson_sc:{semaphore}通道
+ "redis.call('publish', KEYS[2], ARGV[1]); "
// 设置成功,返回1
+ "return 1;"
+ "end;"
// 否则返回0
+ "return 0;",
Arrays.asList(getRawName(), getChannelName()), permits);
if (log.isDebugEnabled()) {
future.thenAccept(r -> {
if (r) {
log.debug("permits set, permits: {}, name: {}", permits, getName());
} else {
log.debug("unable to set permits, permits: {}, name: {}", permits, getName());
}
});
}
return future;
}
ライセンス数を設定する最下層では LUA スクリプトが使用されていることがわかります。このスクリプトは実際に Redis の String データ構造を使用して、指定したライセンス数を保存します。以下に示すように:
パラメータの説明:
- KEYS[1]: 指定した分散セマフォ キー (redissonClient.getSemaphore("semaphore") の "semaphore" など)
- KEYS[2]: ロックを解放するチャネルの名前、redisson_sc:{分散セマフォ キー}、この例では redisson_sc:{semaphore}
- ARGV[1]: 設定されているライセンス数
権限設定の実行プロセスをまとめると次のようになります。
- セマフォの取得、セマフォの現在値を取得します。
- 最初のデータは 0 ですが、次に set semaphore 3 を使用して、このセマフォに対して同時にロックを取得できるクライアントの数を 3 に設定します。(セマフォが以前に設定されている場合、再度設定されることはなく、直接 0 が返されることに注意してください。セマフォの総数を変更したい場合は、addPermits メソッドを使用できます。)
- 次に、redis はいくつかのメッセージをパブリッシュし、1 を返します。
3.RSemaphoreのロック処理
ライセンス数を設定した後、acquire() メソッドを呼び出して取得できます。ライセンス数が渡されない場合は、デフォルトでライセンスが取得されます。
public void acquire() throws InterruptedException {
acquire(1);
}
public void acquire(int permits) throws InterruptedException {
// 尝试获取锁成功,直接返回
if (tryAcquire(permits)) {
return;
}
// 对于没有获取锁的那些线程,订阅redisson_sc:{分布式信号量key}通道的消息
CompletableFuture<RedissonLockEntry> future = subscribe();
semaphorePubSub.timeout(future);
RedissonLockEntry entry = commandExecutor.getInterrupted(future);
try {
// 不断循环尝试获取许可
while (true) {
if (tryAcquire(permits)) {
return;
}
entry.getLatch().acquire();
}
} finally {
// 取消订阅
unsubscribe(entry);
}
// get(acquireAsync(permits));
}
ライセンス取得のコア ロジックは tryAcquire() メソッドにあることがわかります。tryAcquire() が true を返した場合は、ライセンスが正常に取得されたことを意味し、直接返されます。false が返された場合は、ライセンスが存在することを意味します。現在利用可能なライセンスがありません。ロックを取得していないスレッドについては、redisson_sc: {distributed semaphore key} チャネル メッセージをサブスクライブし、無限ループを通じてロックの取得を試行し続けます。
tryAcquireAsync() メソッドを内部的に呼び出す tryAcquire() メソッドのロジックを見てみましょう。
// 异步获取许可
@Override
public RFuture<Boolean> tryAcquireAsync(int permits) {
if (permits < 0) {
throw new IllegalArgumentException("Permits amount can't be negative");
}
if (permits == 0) {
return new CompletableFutureWrapper<>(true);
}
return commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 获取当前剩余的许可数量
"local value = redis.call('get', KEYS[1]); " +
// 许可不为空,并且许可数量 大于等于 当前线程申请的许可数量
"if (value ~= false and tonumber(value) >= tonumber(ARGV[1])) then " +
// 通过decrby减少剩余可用许可
"local val = redis.call('decrby', KEYS[1], ARGV[1]); " +
// 返回1
"return 1; " +
"end; " +
// 其它情况,返回0
"return 0;",
Collections.<Object>singletonList(getRawName()), permits);
}
ソースコードからもわかるように、ライセンスの取得はredis上のデータを操作することになりますので、まずredis上のライセンスの残り数を取得します ライセンスの残り数がスレッドで適用されているライセンス数より大きい場合のみ、取得が成功した場合は 1 が返され、そうでない場合は取得は失敗します。
ロック実行プロセスを要約すると次のようになります。
- セマフォを取得し、現在の値を取得します。たとえば、3、3 > 1
- Decrby セマフォ 1、セマフォがロックを取得できるクライアントの数は 1 から 2 ずつ減ります。
- decrby セマフォ 1
- decrby セマフォ 1
- 3 回のロックを実行した後、セマフォ値は 0 になります
- このとき、再度ロックしたい場合は直接0を返し、その後無限ループに入りロックを取得します。
4.RSemaphoreのロック解除プロセス
RSemaphore によるロックの取得に関する以前の分析から、ロックの解放はライセンス数を Redis に返すだけであることが容易に推測できます。具体的なソースコードを見てみましょう。
public RFuture<Void> releaseAsync(int permits) {
if (permits < 0) {
throw new IllegalArgumentException("Permits amount can't be negative");
}
if (permits == 0) {
return new CompletableFutureWrapper<>((Void) null);
}
RFuture<Void> future = commandExecutor.evalWriteAsync(getRawName(), StringCodec.INSTANCE, RedisCommands.EVAL_VOID,
// 通过incrby增加许可数量
"local value = redis.call('incrby', KEYS[1], ARGV[1]); " +
// 发布一条消息到redisson_sc:{semaphore}中
"redis.call('publish', KEYS[2], value); ",
Arrays.asList(getRawName(), getChannelName()), permits);
if (log.isDebugEnabled()) {
future.thenAccept(o -> {
log.debug("released, permits: {}, name: {}", permits, getName());
});
}
return future;
}