仮に送信側のアプリケーションが一度しか送信しなかったとしても、受信側のアプリケーションがメッセージを何度も受信してしまうかもしれない。
メッセージの受信者は、メッセージの重複にどう対処すればいい?
多くのB2Bインテグレーションは、HTTPを使ってメッセージをインターネット上でやりとりする必要がある。 しかし、そもそもHTTPは信頼性に欠けるプロトコルだ。 そんな環境でGuaranteed Deliveryが達成するには、受信者から受信確認を受け取るまで延々とメッセージを送り続けなければいけない。
…っていうか、その受信確認だってきちんと届くとは限らないよね?そんな場合は、すでに相手が受け取っているメッセージを再送してしまうことになる。それを示したのが、次の図。
受信者側で重複に対応できるようにしておけば、メッセージングシステム側で特別な対策は不要だ。 そのほうがスループットも向上するので、重複の排除をアプリケーション任せにしてメッセージングシステム側では何もしないというシステムもある。 あるいは、その両者を設定で切り替えられるものもある。
受信者をIdempotent Receiver(冪等受信者)になるよう設計する。つまり、同じメッセージを複数回受け取ってもうまく対応できるようにする。
冪等(idempotent)というのは、もともとは数学用語。ある関数を自分自身に適用しても同じ結果になるとき(つまり、f(x) = f(f(x))を満たすとき)、その関数は冪等であると言う。
メッセージングの世界での「冪等」は、同じメッセージを一度受信した場合と何度も受信した場合で結果が変わらないことを表す。冪等名メッセージは、何も気にせずに再送できる。
冪等性を満たすための手段は次のふたつに分類できる。
受信者側で明示的にメッセージの重複を排除するには、受け取り済みのメッセージを覚えておけばいい。一意なメッセージIDがあれば話は簡単だ。 JMS準拠のメッセージングツールも含め、多くのメッセージングシステムは、各メッセージに自動的に一意なメッセージIDを割り当てる。
この方式を採用するときの設計上のポイントとなるのは、次の二点。
送信者と受信者の間での通信の取り決めによって、どんな設計にするかが決まる。
最もシンプルなパターンは、「送信側は一度にひとつのメッセージを送って、すべてのメッセージについて各受信者からの受領確認を待つ」方式。 この場合受信者側では、やってきたメッセージのIDが直近のメッセージのIDと同じかどうかだけを調べれば済む。過去のメッセージIDの履歴は、1件だけ持っていればいい。ただ、現実的に考えるとこの手のやり取りは非常に効率が悪い。ので、「複数のメッセージを一括送信して個別の受領確認は省略する」方式をとることもある。この場合は、受信済みメッセージの履歴をより長めに取っておかなければいけないだろう。このあたりのさじ加減は、Resequencerの検討事項と似ている。
重複メッセージの排除については、TCP/IPの仕組みを調べてみると参考になるだろう。キーワードは「ウィンドウサイズ」。参考書としては『マスタリングTCP/IP 入門編』など。
場合によっては、メッセージID用に新たなフィールドを用意するのではなく、業務上のキー(発注番号とか)をメッセージIDとして扱いたくなるかもしれない。そうすれば、重複の排除は永続化レイヤー(RDBMSなど)にお任せにできる。ぱっと見はすばらしいソリューションに思えるけど、ひとつのフィールドに複数の意味を持たせてしまっているので(ry
#前にもどこかで同じネタが出てましたね :-)
たとえば「口座番号12345に1万円入金する」じゃなくて「口座番号12345の残高を101万円にする」みたいなメッセージにすればいい。 現在の残高が100万円なら、これらのメッセージはどちらも同じ結果になる。前者のメッセージは冪等ではないけど、後者のメッセージは冪等になる。
…はい。確かに、同時並行性の問題には敢えて目をつぶりましたとも。後者のメッセージを何度も再送しているときに、別のところから「口座番号12345の残高を200万円にする」なんていうメッセージが届いたら、話は別。
Microsoft Interface Definition Language (MIDL)でリモートプロシージャを冪等として宣言するには、[idempotent]属性を使う。MIDLの仕様では、
[idempotent]属性が表すのは、その操作がステートを変更しないこと、そして何度実行しても同じ結果を返すということである。その処理を複数回実行しても、一度だけ実行したときと効果は同じになる
とされている。
interface IFoo; [ uuid(5767B67C-3F02-40ba-8B85-D8516F20A83B), pointer_default(unique) ] interface IFoo { [idempotent] bool GetCustomerName ( [in] int CustomerID, [out] char *Name ); }