メッセージ キューに関しては、Rabbitmq と Kafka をメッセージ キュー ミドルウェアとして使用することに慣れています。しかし、消費キューにコンシューマーのグループが 1 つしかない場合、それほど高い信頼性は必要なく、ミドルウェアを使用するのは非常に面倒です. このとき、redis の特性を利用して単純なメッセージ キューを実装できます。
非同期メッセージ キュー
Redisのリストデータ構造は非同期メッセージキューとしてよく使われます. キューなどの操作には rpush と lpush を使います. デキューには lpop と rpop を使います. 複数のプロデューサと複数のコンシューマが同時にメッセージを送受信することをサポートしています
.以下に示すように、メッセージはすべて個別のリスト要素です。
> rpush notify-queue apple banba pear
> lpop notify-queue
“apple”
> lpop notify-queue
"banana"
> lpop notify-queue
"pear"
> lpop notify-queue
(nil)
上記は rpush と lpop の組み合わせですが、lpush と rpop を組み合わせて使用することもでき、効果は同じです。
キューが空だった場合の処理
クライアントは、キューの pop 操作によってメッセージを取得し、それを処理します。処理後、メッセージを取得して処理します。このようなサイクルは、キュー コンシューマーとしてのクライアントのライフ サイクルです。
しかし、キューが空の場合、クライアントはポップの無限ループに陥り、ポップし続けます. この種の空のポーリングは、クライアントの CPU 消費を増加させるだけでなく、Redis の QPS も増加させます. client ノードが数十個ある場合、Redis の遅いクエリが大幅に増加する可能性があります。
通常、この問題を解決するには sleep を使用します. キューが空の場合、スレッドをしばらくスリープさせます. これにより、クライアントの CPU 消費が削減されるだけでなく、Redis の QPS も削減されます.
読み取りのブロック
上記のスリープ方法で問題を解決できます。しかし、別の小さな問題があります。つまり、睡眠によってメッセージの遅延が増加します。コンシューマーが 1 つしかない場合、この遅延は 1 秒です。複数のコンシューマがある場合、コンシューマのスリープ時間が発散されるため、この遅延は減少します。
消費の問題と遅延の問題の両方を解決する方法はありますか? もちろんありますが、それは blpop/brpop です.
これら 2 つの命令の接頭文字 b は、ブロッキング、つまりブロッキング読み取りを表します。
ブロッキング読み取りに列のデータがない場合、すぐにスリープ状態になり、データが到着するとすぐに復帰します。メッセージのレイテンシはほぼゼロです。
アイドル接続は自動的に切断されます
上記の解決策は空の列の問題を解決しますが、完全ではなく、接続がアイドル状態になるという問題がまだ残っています。
スレッドが常にブロックされている場合、Redis クライアント接続はアイドル状態の接続になります. アイドル状態が長すぎる場合、サーバーは通常、アイドル状態のリソースの占有を減らすために積極的に接続を切断します. この時点で、blpop/brpop は例外をスローします。そのため、クライアント側のコンシューマーを作成するときは注意して、例外がキャッチされた場合は再試行してください。
遅延キューの実装
遅延キューは、Redis の zset を介して実装できます。メッセージを zset の値として文字列にシリアル化し、メッセージの有効期限の処理時間をスコアとして、複数のスレッドを使用して zset をポーリングし、期限切れのタスクを取得して処理します。可用性を確保するために複数のスレッドが使用され、1 つのスレッドがハングアップしても、処理を続行できる他のスレッドが存在します。複数のスレッドがあるため、タスクが複数回実行されないように、同時競合タスクを考慮する必要があります。
Redis の zrem メソッドは、タスクのマルチスレッドおよびマルチプロセス競合の鍵です. その戻り値は、現在のインスタンスがタスクをつかんだかどうかを決定します.複数のプロセスが呼び出され、複数のスレッドがそれを取得します。
同時に、個々の文字が問題を処理し、ループが失敗して異常終了するのを防ぐために、handle_msg をキャプチャする必要があります。
public class RedisDelayingQueue<T> {
static class TaskItem<T> {
public String id;
public T msg;
}
private Type TaskType = new TypeReference<TaskItem<T>>(){
}.getType() ;
private Jedis jedis;
private String queueKey;
public RedisDelayingQueue(Jedis jedis,String queueKey) {
this.jedis = jedis;
this.queueKey = queueKey;
}
public void deplay(T msg) {
TaskItem<T> task = new TaskItem<>();
task.id = UUID.randomUUID().toString();
task.msg = msg;
String s = JSON.toJSONString(task);
jedis.zadd(queueKey,System.currentTimeMillis() + 5000, s);
}
public void loop() {
while (!Thread.interrupted()) {
Set<String> values = jedis.zrangeByScore(queueKey,0,System.currentTimeMillis(),0 , 1);
if (values.isEmpty()) {
try {
Thread.sleep(50);
}catch (InterruptedException e) {
break;
}
continue;
}
// 取出
String s = values.iterator().next();
if (jedis.zrem(queueKey,s) > 0) {
TaskItem<T> task = JSON.parseObject(s,TaskType);
this.handleMsg(task.msg);
}
}
}
public void handleMsg(T msg) {
System.out.println(msg);
}
public static void main(String[] args) {
Jedis jedis = new Jedis("124.221.52.57",6379);
jedis.auth("li12345...");
jedis.select(2);
final RedisDelayingQueue<String> queue = new RedisDelayingQueue<>(jedis,"q-demo");
Thread producer = new Thread() {
public void run() {
for (int i = 0; i < 10; i++) {
queue.deplay("codehole"+i);
}
}
};
Thread consumer = new Thread() {
public void run() {
queue.loop();
}
};
producer.start();
consumer.start();
try {
// producer.join();
Thread.sleep(6000);
consumer.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}