RabbitMQとMS SQL間の分散トランザクション

2つのシステム間で非同期通信を実装するには、メッセージキューを使用することが非常に有益です。 システムの1つが存在しても、他のシステムはそれに気づかず、静かにメッセージを送信し続けます。メッセージは2番目のシステムが解除されると処理されます。 MS SQLテーブルをメッセージキューとして使用できますが、これは特にスケーラブルなソリューションではありません。



ただし、メッセージキューを格納するための別のシステムが用意されるとすぐに(RabbitMQを使用します)、すぐにトランザクションの問題が発生します。 たとえば、Rabbitにメッセージを送信したデータベースにマークを保存する場合、メッセージが正常に送信された場合にのみマークが保存されることを保証するのはそれほど簡単ではありません。 猫の下でこの問題にどのように対処したかについて読んでください。



一部のシナリオでは、SQLトランザクションの完了後にメッセージをRabbitMQに送信するだけで済みます。 たとえば、登録中にパスワード付きの電子メールを送信する必要があり、登録後に表示されるページに「電子メールを再送信」ボタンがある場合、トランザクションを一切行わず、メッセージの送信エラーが発生した場合、ユーザーに通知を表示するだけで十分です。



SQLトランザクションをコミットする直前にメッセージを送信できます。 この場合、メッセージの送信が失敗した場合、SQLトランザクションをロールバックできますが、メッセージの送信が成功した後、SQLトランザクションのコミットが落ちる可能性があります。 ただし、まれなメッセージが受信者システムに配信される状況が許容できるものの、送信システムがそれを忘れる場合は、実装が非常に簡単なので、この方法を使用することをお勧めします。



倒れたトランザクションが必然的に繰り返されるシナリオでは、送信システムに送信の記録がないことを恐れることはできません(さらに、コミットの直前だけでなく、トランザクションのいつでもメッセージを送信できます)。 ただし、送信システムの観点から同じメッセージが受信システムで2回処理されないように、メッセージ処理操作をべき等にする必要があります。



たとえば、電子メールを消費者に送信し、データベースにメモを入れる必要があります。 消費者データはCRMシステムに保存されます。 CRMシステムは、RabbitMQのキューを介して電子メールゲートウェイと通信します。 メッセージの送信は、一意の識別子とメッセージの送信先のコンシューマーのリストを持つタスクによって実行されます。 消費者へのレターの送信処理が低下した場合(たとえば、SQLタイムアウトなど)、しばらくすると、タスクは再びメッセージの送信を試みます。 このシナリオでは、トランザクションが完了する前にメッセージをRabbitMQに送信できますが、電子メールゲートウェイでメッセージを処理する場合、一意のタスク識別子とコンシューマー番号をリストに保存する必要があります。 このタスク識別子とコンシューマー番号を含むメッセージが電子メールゲートウェイデータベースに既にある場合、それを再度送信しません。



電子メールゲートウェイがCRMがメッセージを送信する方法を正確に抽象化するために、CRMはリスト内のタスク識別子と消費者番号ではなく、べき等キー(このデータに基づいて生成される一意の値)を送信する必要があります。 電子メールを送信する他の方法では、べき等キーは異なる方法で生成されます。 このアプローチでは、電子メールゲートウェイはメッセージの送信方法について何も知らない必要があります。主なことは、送信者がメッセージを一意に識別するキーを送信することです。



すべての場合において、SQLトランザクションがクラッシュした場合に、しばらくしてからそれが繰り返されることを保証できるわけではありません。 また、一意のi等性キーを生成できるデータに基づいているとは限りません。 また、重複メッセージがない場合でも、Ack RabbitMQメソッドの呼び出しがドロップされると、1つのメッセージが複数回処理される可能性があるため、常にキューのべき等からメッセージ処理操作を行うことをお勧めします。 一般的な場合の問題を解決するには、RabbitMQとMS SQL間の分散トランザクションのようなものと、自動生成されるべき等キーが必要です。 これらのタスクは両方とも次のように解決できます。

  1. SQLトランザクションの一部として、一意のメッセージ識別子がデータベースの特別なテーブルに保存されます。
  2. INSERTクエリの実行後、SQLトランザクションが完了する前に、メッセージは中間キューに保存されます。 このメッセージでは、とりわけ、データベースに保存された一意の識別子が送信されます。
  3. 別のタスクが中間キューを処理し、メッセージにデータベース内の一意の識別子があることを確認します。
  4. 存在する場合、メッセージはキューに転送され、受信者システムによってすでに処理されています。 古い識別子を補助テーブルに保存しないために、メッセージが移動された後、その識別子はデータベースから削除されます(識別子を削除しても、システムのパフォーマンスには影響しません-データベースに余分なレコードがあります)。
  5. 一意の識別子によってデータベース内のレコードを要求するときにトランザクションがまだ完了していない場合、要求はこのトランザクションの完了を待機し、その後のみレコードを返します。 つまり、トランザクションの完了を待つために追加のロジックは必要ありません。
  6. 一意の識別子がデータベースにない場合、これはトランザクションが拒否され、メッセージが破棄されたことを意味します。
  7. メッセージの一意の識別子は、受信側システムでべき等キーとして使用されます。


画像

このアプローチにより、送信されたメッセージに関する情報が送信システムに保存されることが保証されます。 メッセージ送信トランザクションで一意の識別子を持つレコードが作成された場合、それを使用して補助テーブルなしで実行できます。



ここで問題が発生する可能性があります。「データベースのプレートをキューとして使用するよりも優れているのは何ですか? とにかく、データベースに対して補助クエリを実行する必要があります。「事実、データベース内のテーブルをキューとして使用すると、「SELECT TOP 1 * FROMメッセージWHERE Status = 'New'」のようなクエリが実行され、最後の生メッセージが受信されます。 複数のスレッドでメッセージを処理する場合、1つのメッセージが2つの異なるスレッドで処理されないようにするには、シリアル化可能なトランザクションを使用して最後のメッセージを受信し、そのステータスを変更する必要があります。 Serializableトランザクションを使用する場合、最後の未処理メッセージを受信する要求は、「新規」ステータスのすべてのレコードをブロックし、メッセージ受信トランザクションが完了するまで誰も新しいメッセージを追加できません。



ただし、このようなトランザクションでは、2つのスレッドが最後の未処理のメッセージを同時に読み取り、同時に共有ロックを課すことができるため、デッドロックが常に発生します。メッセージのステータスを更新しようとすると、このロックのレベルを排他的に上げることができず、トランザクションの1つが拒​​否されます。 したがって、メッセージを読むときは、更新ロックをかける必要があります。 その結果、キューは一度に1つのスレッドだけがアクセス(書き込みと読み取りの両方)できるため、ボトルネックになります。



上記のアプローチを使用する場合、補助テーブルへのすべてのクエリ(挿入、検索、および削除)は、既知の一意キーを使用して実行され、データベース内の1つのレコードのみをブロックします。 したがって、メッセージのマルチスレッド処理では、メッセージを追加または受信するためにロックが解放されるまで複数のスレッドが待機するボトルネックはありません。



All Articles