複数のコンシューマがキューからメッセージを取得するシステムがあります(現在データベースで実装されています)。メッセージには順序フィールドがあります。 1つの特定の順序のすべてのメッセージは同期的に処理される必要があります。つまり、同じ注文の2つのメッセージが異なるコンシューマーによって同時に処理されることはありません。彼らは注文フィールドに「ロック」を持っている必要があります。
以前は少数のメッセージしか処理していませんでしたが、数回のスケールアップの後、現在、10のコンシューマーで最大400メッセージ/秒を処理しています。データベースの最大能力に近づいており、さらに拡張したいと考えています。
メッセージキューを使用してこのシステムを実装するのはこのためです。ただし、メッセージキューは、特定のフィールドで特定のコンシューマにメッセージをロックするようには設計されていません。
コンシューマがキュー内の特定のフィールド値に基づいて他のコンシューマのメッセージをロックできる、複数のコンシューマを持つキューソリューションの最適な設計は何ですか?複数のキューを使用しますか?アクターモデルを実装しますか? ...?
最終的に Microsoft Orleans を使用しました。これはアクターモデルの実装であり、私たちに最も適していました。
Theraotに詳細な回答をありがとうございます。あなたは私たちをさらに助けてくれました。
注文を処理していた消費者がクラッシュした場合はどうしますか?
消費者に注文の商品を受け取って処理させたくない。プッシュされるアイテムを使用して部分注文を作成する仲介者が必要です。
したがって、一方の側で注文を開始し、注文にアイテムを追加して注文を完了するプロデューサーがいます。中央には、アイテムを半順序オブジェクトに集約する仲介者があります。反対側には、注文の消費者があります。
実装する設計の決定:
プッシュベースのデザインには、より単純なスレッドモデルが必要です(これに戻ります)。それを誤解することは困難です。
Pushベースのデザインは、消費者に通知するためのメソッド呼び出しを行うだけです。つまり、それらが失敗した場合(たとえば、例外がスローされた場合)に認識されます。そして、これにより、消費者がクラッシュした場合に、別の消費者に値をプッシュすることが容易になります。
注文にプッシュされるアイテムのレベルでそれを行うと、別の消費者に行くのが非常に難しくなります。部分的に完了した注文を追跡する必要があります。仲介者がこの問題を解決します。仲介者を取得したら、注文を集計して単一の値として処理する方がはるかに理にかなっています。
一方、コンシューマがプルベースのデザインに失敗した場合、プロデューサが簡単に知る方法はありません。つまり、プロデューサーが別のコンシューマーに同じ価値を与えることは、ほとんど現実的ではありません。
プルベースの設計を配布すると、クライアントがサーバーをプールすることになりますが、これは適切ではありません。ただし、プッシュベースのデザインは配布がはるかに簡単です。たとえば、メソッドはネットワーク全体で非同期呼び出しを実行します(ソケット、RPC、Webデザイン、メッセージキューなど)。
最後に、コンシューマーがプルする必要がある場合、プルベースのデザインをプルできる場所からコンシューマーごとにキューに追加するのは簡単です(ただし、いくつかの利点は失われます)。一方、プルベースのデザインでコンシューマーが通知をプッシュしたい場合は、コンシューマーごとにスレッドのプルとプッシュを行う必要があり、コストが高くなります。
私はプッシュベースのデザインが優れたオプションだと強く信じています。ただし、私が正しく理解していれば、現在データベースを使用しています。データベースが機能する方法は、プル、つまりクエリを実行することです。したがって、私はあなたがプルベースのデザインを持っていると思います。プッシュベースのデザインに変更すると、コードのリファクタリングにコストがかかります。
中間ステップは同期でなければなりません。非同期にすることは、正しく機能し、維持することを難しくします。
プロデューサー側は非同期にすることができます。ただし、これはおそらく火と忘れられるので、デザインで公開する必要はありません。
コンシューマー側では、プッシュベースの設計を推奨しているため、同期になります。
したがって、APIに関する限り、それは同期になります。
いずれにしても、複数のキューができます。仲介者がないと仮定する場合、次のものが必要です。注文ごとに1つのアイテムのキュー、まだ消費者に割り当てられていない注文の1つのキュー(プルする場合は、ここからオーダーを選択します。プッシュする場合、新しいサブスクライバーはここから取得)、および-デザインに応じて-キューまたは利用可能なコンシューマのセット。仲介者は、注文ごとのアイテムのキューを置き換えます。
注:プッシュベースのデザインは通知するサブスクリプションを見つける必要があるため、利用可能なコンシューマのセットを使用する場合があります。
プルベースのデザインでは、新しい値が利用可能になるまでコンシューマースレッドを待機させる待機ハンドルが必要になります(これが、プッシュベースのデザインのスレッドモデルがより単純である理由です)。分解も検討してください。それらのスレッドがスタックしていないことを確認する必要があります。
プッシュベースの設計では、消費者はスレッドに縛られていません。代わりに、サブスクリプションを使用してそれらを追跡します。サブスクリプションは、値を渡すために呼び出すメソッドへの参照(またはaを持つインターフェースを持つオブジェクト)でなければなりません。
これらのソリューションをコンポーネントに分解することをお勧めします。特に、プルベースの設計には効果的にブロッキングキュー(キューと待機ハンドル)があり、プッシュベースの設計には効果的に一方向チャネル(サブスクリプションとキュー)があります。これらは、ライブラリから取得できるものであり、それらを使用すると、単に実装されます。
注:プッシュベースのデザインでは、プッシュ先へのサブスクリプションがない場合、値はキューに入れられます。
競合状態については、さらに注意する必要があります。特に、プッシュベースのデザインには、2つの注目すべき競合状態があります。
これらにはいくつかの便利なロックソリューションがあります。特に、操作をキューに入れることができることを述べておきます。つまり、スレッドセーフキューに実行する操作(コールバックの場合もあります)を追加します。ロックを取得してみてください。ロックを取得したスレッドは、キュー内のすべての操作を実行します。スレッドがロックを取得しなかった場合は、別のスレッドがロックを取得したことになります...そして、その別のスレッドが操作を実行します。したがって、待機せずに先に進むことができます。これにより、ロックを待機しているスレッドがなくなります。 特定された競合状態でのみ使用します。すべてに対してこれを行うと、ループ内でスレッドが不確定にスタックするのが非常に簡単になります。
注:プルベースのデザインは、タイトすることで競合状態を回避します。トライウェイトループです。