1. 機能紹介
redis streamはredis5.0以降に登場した新しいデータ型で、主に追記可能なデータ構造です。
以前のredisのサブスクリプションと公開機能の不具合をある程度改善できます。
1.1Redis のサブスクリプションと公開モデル
1.1.1 欠点 1: 購読者は最新のメッセージを受信できない
Redis サブスクライバーがサブスクリプション操作を実行するときに、パブリッシャーがこの期間内にメッセージを公開すると、サブスクライバーはメッセージを受信しません。これは、Redis のサブスクリプションおよび公開システムがメッセージの送受信に基づいているためで、サブスクライバーが Redis サーバーに登録されていない前にメッセージが送信された場合、サブスクライバーはメッセージを取得できません。
1.1.2 欠点 2: サブスクリプション情報を永続化できない
Redis サブスクリプション公開モジュールのもう 1 つの欠陥は、サブスクリプション情報を永続化できないことです。現在の Redis サーバーに多数のパブリッシャーとサブスクライバーが存在すると仮定すると、Redis サーバーをシャットダウンして再起動すると、すべてのサブスクリプション情報がクリアされます。これは、チャンネルを再登録する必要があることを意味します。
1.1.3 欠点 3: メッセージ送信の信頼性が低い
Redis のサブスクリプション発行モジュールのメッセージ配信は信頼できません。ネットワーク障害が発生した場合、またはクライアントがクラッシュした場合、サブスクライバはすべてのメッセージを受信できません。さらに、多くのアプリケーションは、パブリッシャーによってパブリッシュされたすべてのメッセージがすべてのサブスクライバーによって完全に受信されることを保証する必要があるため、より信頼性が高く耐久性のあるモデルが必要です。
この問題を解決するには、サブスクリプション発行の新しいモデルである Redis Stream を使用できます。各メッセージは Redis Stream を使用して保存でき、このモデルはストリーミングも提供するため、チャネル内に完全なメッセージ ストリームを作成できます。
1.2 ストリームの利点
- ストリーム メッセージは完全で順序付けされており、多くのメッセージを 1 つのストリームに重ね合わせることができます。
- ストリーム情報は aof または rdb ファイルにも保存されます。 Redis サービスは再起動後に自動的に読み取ります
- このデータ構造では、タイムスタンプに従って内部で並べ替えられるため、ユーザーは一定期間内にメッセージを取得できます。
- 対応するストリームキーのすべてのメッセージを受信できます。時刻の後に 1 つ以上のメッセージを指定することもできます
- パラメータを通じてストリームの長さを設定でき、マップ オブジェクトにシリアル化されたデータを保存できます。
Stream は、Redis データ型の中で最も複雑です。データ型自体は非常に単純ですが、必須ではない追加の機能が実装されています。タイムスタンプでソートされたストリーミング メッセージの順序付きセットが保存されます。
2. 設計思想と技術フレームワークの説明
注釈:
クラス名 |
属性 |
関数 |
ストリームリスナーキー |
@StreamListenerKey(streamKeys = {"mystream"}) |
streamKey ストリーム名に対応するメッセージをリッスンします。 |
インターフェースとメソッド:
(インターフェース) |
方法 |
関数 |
ストリームリスナー |
onメッセージ |
対応するストリームをリッスンするコールバック メソッド |
ストリームメッセージサービス |
sendStreamMsg(); |
メッセージを送ります |
readRangeStreamMessage(); |
時間範囲内でメッセージを読む |
|
readFirstMsgInStream(); |
指定されたストリームの最初のメッセージを読み取ります |
|
下流(); |
指定されたIDのメッセージを削除します |
|
グループを作る(); |
消費者グループの作成 |
|
readGroup(); |
コンシューマーを使用してストリームから未消費のメッセージを読み取る |
|
xAck(); |
メッセージが消費されたことを確認する |
|
…… |
他のメソッドについては、コード内の導入部分を参照してください。 |
3. ストリームメッセージサービスの基本的な使い方
3.1 メッセージ監視
対応するストリームのメッセージをリアルタイムで監視する必要がある場合は、まず StreamListener インターフェイスを実装し、実装クラスに StreamListenerKey アノテーションを追加します。アノテーション付き属性 streamKey は、監視されている対応するストリームの名前です。
例: mystream という名前のストリーム内のメッセージをリッスンし、消費後にメッセージを削除するメソッドを呼び出します。
@Slf4j
@Component
@StreamListenerKey(streamKeys = {"mystream"})
public class TestStreamListenerImpl implements StreamListener {
@Autowired
private StreamMessageService streamMessageService;
@Override
public void onMessage(String key, List<StreamEntry> messages) {
//获取消息进行消费
ArrayList<StreamEntryID> ids = new ArrayList<>();
for (StreamEntry message : messages) {
ids.add(message.getID());
}
//消费后删除对应的消息
Long aLong = streamMessageService.delStream(key, ids);
}
}
3.2 メッセージフロー方式の使用
3.2.1 メッセージの送信とフローの作成
メッセージを送信し、対応する名前でストリームを作成します
/**
* description:发送指定长度的消息
*
* @param streamKey 指定消息流的key
* @param EntryID 指定发送消息传入的id,可为null,默认为redis自动配置ID 例如:new StreamEntryID(1691460419394L,0L);前面一个为时间戳,后面一个可不传或为0
* @param field 消息的字段key
* @param content 消息的内容
* @param maxLen 指定消息流的长度
* @author litong
* @date 2023/8/8
*/
StreamEntryID sendStreamMsg(String streamKey, StreamEntryID EntryID, String field, String content, int maxLen);
3.2.2 指定したIDのストリーム名またはメッセージに対応する時間範囲を読み取る
メッセージはストリーム内のタイムスタンプに従って保存されるため、タイムスタンプを使用して時間範囲内のすべてのメッセージを読み取ることができます。経過時間が同じであれば、この時間 ID に対応するメッセージを読み取ることを意味します。
/**
* description: 读取时间范围内的消息
*
* @param streamKey 读取的消息的key
* @param streamEntryIDList 读取的时间范围
* 例如:
* List<StreamEntryID> streamEntryIDList = new ArrayList<>();
* streamEntryIDList.add(new StreamEntryID(1691460419394L,0L));
* streamEntryIDList.add(new StreamEntryID(1691460498008L,0L));
* 如果传递两个相同的ID,代表查询这个指定ID的消息
* @author litong
* @date 2023/8/8
*/
List<StreamEntry> readRangeStreamMessage(String streamKey, List<StreamEntryID> streamEntryIDList);
3.2.3 指定したIDのメッセージを削除する
メッセージを送信して消費するだけの場合は、消費完了後に対応するメッセージを直接削除できます。
/**
* description: 删除对应key的消息
*
* @param streamKey 指定要删除的key
* @param streamEntryID 指定对应的时间戳的key
* @return Long
* @author litong
* @date 2023/8/8
*/
Long delStream(String streamKey, List<StreamEntryID> streamEntryID);`
3.3 ストリーム消費者グループ
ストリームには、kafka のメッセージ キューに似た概念があります。
3.3.1 消費者団体の概念
コンシューマ グループは、ストリームからデータをフェッチし、実際に複数のコンシューマにサービスを提供し、特定の保証を提供する疑似コンシューマのようなものです。
- 各メッセージは異なるコンシューマに提供されるため、同じメッセージを複数のコンシューマに配信することはできません。
- コンシューマは、コンシューマ グループ内で名前によって識別されます。名前は、コンシューマを実装するクライアントが選択する必要がある大文字と小文字を区別する文字列です。これは、クライアントが同じコンシューマになるために再申請するため、切断後でもコンシューマ グループは依然としてすべての状態を保持することを意味します。ただし、これはクライアントが一意の識別子を提供することも意味します。
- 各コンシューマ グループには、最初の ID は決して消費されないという概念があるため、コンシューマが新しいメッセージを要求すると、これまでに配信されたことのないメッセージを提供できます。
- メッセージを消費するには、メッセージが正しく処理され、コンシューマ グループから削除できることを示す、特定のコマンドを使用した明示的な確認が必要です。
- コンシューマ グループは、現在保留中のすべてのメッセージ、つまり、コンシューマ グループ内の一部のコンシューマに配信されたものの、まだ処理が確認されていないメッセージを追跡します。この機能により、ストリームの履歴にアクセスすると、各コンシューマには配信されたメッセージのみが表示されます。
ある意味、コンシューマ グループはストリームに関する何らかの状態と考えることができます。
3.3.2 基礎となる原則:
コンシューマ グループ内のコンシューマがメッセージを消費すると、Redis はコンシューマ グループの「保留中」リストからメッセージ ID を削除し、メッセージ ハッシュ テーブルからメッセージを削除します。コンシューマがコンシューマ グループ内の最後のコンシューマである場合、コンシューマ グループの「保留中」リストは削除されます。
コンシューマ グループ内のすべてのコンシューマが特定のメッセージを消費しない場合、メッセージの ID はコンシューマ グループの「保留中」リストに保存されます。 「保留中」リストは順序付けされたコレクションであり、各要素はメッセージのタイムスタンプでソートされたメッセージの ID です。順序付きセット内のスコアはメッセージのタイムスタンプであり、メンバーはメッセージの ID です。
3.3.3 消費者グループの基本的な使用法
3.3.3.1 コンシューマグループの作成
対応するフロー名でコンシューマ グループを作成し、ID に null を渡します。
/**
* description: 创建消费组
*
* @param streamKey 流名曾
* @param groupName 消费组名称
* @param streamEntryID 消息ID,可为null
* @param makeStream 在没有对应的streamKey时,是否创建流
* @return java.lang.String
* @author litong
* @date 2023/8/15
*/
String createGroup(String streamKey, String groupName, StreamEntryID streamEntryID, boolean makeStream);
3.3.3.2 コンシューマ・グループ内のメッセージの読み取り
コンシューマ・グループ内のまだ消費されていない最初のメッセージを読み取ります。
/**
* description: 消费组读取消息,需要手动ack
*
* @param groupname 消费组名称
* @param consumer 消费者的名曾
* @param count 读取多少条,可不传
* @param streamKey 流名称
* @return java.util.List<java.util.Map.Entry < java.lang.String, java.util.List < redis.clients.jedis.StreamEntry>>>
* @author litong
* @date 2023/8/15
*/
List<Map.Entry<String, List<StreamEntry>>> readGroup(String groupname, String consumer, int count, String streamKey);
3.3.3.3 メッセージ確認確認応答
消費されたメッセージを確認する
/**
* description: ack消息确认
*
* @param streamKey 流名称
* @param groupName 消费组名称
* @param streamEntryIDs 消息ID
* @return java.lang.Long
* @author litong
* @date 2023/8/15
*/
Long xAck(String streamKey, String groupName, List<StreamEntryID> streamEntryIDs);
3.3.4 消費者グループの例:
まず、mystream ストリームを作成し、test1 という名前のコンシューマ グループを作成しました。
String serviceGroup = streamMessageService.createGroup(streamMsgReq.getStreamKey(), streamMsgReq.getGroupName(), streamMsgReq.getEntryID(), streamMsgReq.getMakeStream());
次に、メッセージをストリームに送信します。
StreamEntryID streamEntryID = streamMessageService.sendStreamMsg(streamMsgReq.getStreamKey(), streamMsgReq.getEntryID(),
streamMsgReq.getField(), streamMsgReq.getContent(), streamMsgReq.getMaxLen());
次に、コンシューマ グループを使用して読み取り、最初に testconsumer1 を使用してメッセージを読み取ります
List<Map.Entry<String, List<StreamEntry>>> entries = streamMessageService.readGroup(streamMsgReq.getGroupName(), streamMsgReq.getConsumer(), streamMsgReq.getCount(), streamMsgReq.getStreamKey());
次に、consumer2 を使用して消費し、最初のメッセージが消費されたことを確認します。これは、グループ消費の単純な概念です。同じコンシューマ グループ内のすべてのメッセージが繰り返し消費されることはありません。
手動でメッセージを確認する
メッセージが消費された後、消費されたすべてのメッセージが確認応答されていることを確認するために、メッセージを確認する必要があります。
Long ack = streamMessageService.xAck(streamMsgReq.getStreamKey(), streamMsgReq.getGroupName(), streamMsgReq.getStreamEntryIDS());