イベントストアではなく「スナップショット」プロジェクションから集計を再水和する


13

そのため、実際のプロジェクトにパターンを適用する機会は一度もありませんでしたが、しばらくの間、イベントソーシングとCQRSをいじっていました。

読み取りと書き込みの懸念を分離することの利点を理解しています。また、イベントソースとは異なり、イベントストアとは異なる「読み取りモデル」データベースに状態の変更を簡単に投影できます。

私があまりはっきりしていないのは、イベントストア自体から集計を再水和する理由です。

「読み取り」データベースへの変更の投影が非常に簡単な場合、スキーマがドメインモデルと完全に一致する「書き込み」データベースへの変更を常に投影しないのはなぜですか。これは、事実上スナップショットデータベースになります。

これは、ES + CQRSアプリケーションでかなり一般的だと思います。

この場合、イベントストアは、スキーマの変更の結果として「書き込み」データベースを再構築するときにのみ役立ちますか?それとも、もっと大きなものが欠けていますか?


書き込みモデルの状態ストアに非同期的に書き込み、それを排他的にエンティティの読み込みに使用しても問題はありません。あなたがそうするかどうかにかかわらず、同じ正確な一貫性の問題が存在します。これらの一貫性の問題を修正する鍵は、エンティティを異なる方法でモデル化することです。これらの一貫性の問題を解決するEvent Sourcingには魔法はありません。魔法は、モデリングではなく、思いやりにあります。そのレベルで一貫性を必要とする特定のアプリケーションがあり、それらはどのようにモデル化しても非常に論争の激しいエンティティを持ち、関係なく特別な注意が必要です。
アンドリューラーソン

イベントの配信を保証できる限り。これを行うには、アプリケーションでイベントを永続的なイベントバスに同期的に発行するだけです。公開後、アプリケーションのジョブは完了しました。その後、バスはそれをさまざまなイベントハンドラーに配信します。1つはイベントストアを更新し、もう1つは状態ストアを更新し、その他は読み取りストアの更新に必要です。Event Sourcingを使用している理由は、即時の一貫性をもう気にしないためです。それを受け入れます。
アンドリューラーソン

イベントストアからエンティティを常にロードする必要がある理由はありません。それはその目的ではありません。その目的は、システムで発生したすべての生の永続的な元帳を提供することです。エンティティ状態ストアと非正規化読み取りモデルは、読み込みと読み取り用です。
アンドリューラーソン

回答:


13

私があまりはっきりしていないのは、イベントストア自体から集計を再水和する理由です。

「イベント」は記録の本だからです。

「読み取り」データベースへの変更の投影が非常に簡単な場合、スキーマがドメインモデルと完全に一致する「書き込み」データベースへの変更を常に投影しないのはなぜですか。これは、事実上スナップショットデータベースになります。

はい; 毎回その状態をゼロから再生成するのではなく、キャッシュされた集約状態のコピーを使用することがパフォーマンスの最適化に役立つ場合があります。覚えておいてください:パフォーマンスの最適化の最初のルールは「しない」です。ソリューションはさらに複雑になりますが、ビジネスの動機付けが得られるまで、それを避けることをお勧めします。

この場合、イベントストアは、スキーマの変更の結果として「書き込み」データベースを再構築するときにのみ役立ちますか?それとも、もっと大きなものが欠けていますか?

もっと大きなものが足りない。

最初のポイントは、イベントソースソリューションを検討している場合、何が起こったかの履歴を保存する価値があると期待するためです。つまり、非破壊的な変更を行いたいということです。

そのため、イベントストアに書き込みを行うのはこのためです。

特に、これはすべての変更をイベントストアに書き込む必要があることを意味します。

競合するライターは、互いの編集を認識していない場合、互いの書き込みを破壊したり、意図しない状態にシステムを駆動したりする可能性があります。したがって、一貫性が必要な場合の通常のアプローチは、ジャーナル内の特定の位置(HTTP APIの条件付きPUTに類似)に書き込みをアドレス指定することです。失敗した書き込みは、ジャ​​ーナルに対する現在の理解が同期していないこと、および回復する必要があることをライターに伝えます。

既知の良好な位置に戻ってから、追加のイベントを再生することは、そのポイントが一般的な回復戦略であるためです。既知の適切な位置は、ローカルキャッシュにあるもののコピー、またはスナップショットストア内の表現です。

幸福なパスでは、集計のスナップショットをメモリに保存できます。利用可能なローカルコピーがない場合にのみ外部ストアに連絡する必要があります。

さらに、記録帳にアクセスできる場合は、完全に追いつく必要はありません。

そのため、通常のアプローチ(スナップショットリポジトリを使用する場合)は、非同期に維持することです。そうすることで、回復する必要がある場合、集約の履歴全体をリロードしてリプレイすることなく回復できます。

有効期間が有効な細粒度の集計では、通常、スナップショットキャッシュの維持コストを上回るメリットを得るのに十分なイベントが収集されないため、この複雑さが重要ではない場合が多くあります。

しかし、それが問題に対して適切なツールである場合、古いモデルの集計を書き込みモデルにロードし、それを追加のイベントで更新することは、完全に合理的なことです。


これはすべて理にかなっています。しばらく前にESのダミー実装で遊んだとき、一貫性を保証するためにEventStoreを使用しましたが、「スナップショットリポジトリ」と呼ばれるものにも同期して書き込みました。これは、イベントを再生することなく、現在の状態を常に読み取る準備ができていたことを意味します。これはうまくスケールしないと思いましたが、それは単なる練習であったので気にしませんでした。
メタファイト

6

「書き込み」データベースの目的が何であるかを指定しないので、ここであなたがこれを意味すると仮定します。イベントストアからアグリゲートを再構築する代わりに、アグリゲートに新しい更新を登録するとき、 「書き込み」データベースからそれを持ち上げ、変更を検証し、イベントを発行します。

これがあなたの言いたいことである場合、この戦略は不整合の条件を作成します。最後の更新が「書き込み」データベースに入る前に新しい更新が発生すると、新しい更新は古いデータに対して検証され、したがって、「不可能」(つまり「許可されない」)イベントを発行し、システム状態を破壊する可能性があります。

たとえば、劇場の座席を予約する常設例を考えてみましょう。二重予約を防ぐには、予約されている座席がすでに空いていないことを確認する必要があります。これが「検証」と呼ばれるものです。そのためには、すでに予約されている座席のリストを「書き込み」データベースに保存します。次に、予約リクエストが届くと、リクエストされた座席がリストに含まれているかどうかを確認し、含まれていない場合は「予約済み」イベントを発行します。次に、投影プロセスを実行します。このプロセスでは、「予約済み」イベントをリッスンし、「書き込み」データベースのリストに予約済みの座席を追加します。

通常、システムは次のように機能します。

1. Request to book seat #1
2. Check in the "already booked" list: the list is empty.
3. Issue a "booked seat #1" event.
4. Projection process catches the event, adds seat #1 to the "already booked" list.
5. Another request to book seat #1.
6. Check in the list: the list contains seat #1
7. Respond with an error message.

ただし、リクエストがあまりにも早く届き、ステップ4の前にステップ5が発生した場合はどうなりますか?

1. Request to book seat #1
2. Check in the "already booked" list: the list is empty.
3. Issue a "booked seat #1" event.
4. Another request to book seat #1.
5. Check in the list: the list is still empty.
6. Issue another "booked seat #1" event.

同じ座席を予約するための2つのイベントがあります。システム状態が破損しています。

これを防ぐには、予測に対して更新を検証しないでください。更新を検証するには、イベントストアから集計を再構築してから、更新を検証します。その後、イベントを発行しますが、タイムスタンプガードを使用して、ストアから最後に読み取ってから新しいイベントが発行されないようにします。これが失敗した場合は、再試行します。

イベントストアから集計を再構築すると、パフォーマンスが低下する可能性があります。これを軽減するために、スナップショットの作成元のイベントのIDでタグ付けされたイベントストリームに集計スナップショットを直接保存できます。この方法では、常に時間の初めからイベントストリーム全体を再生するのではなく、最新のスナップショットをロードし、その後に発生したイベントのみを再生することで、集計を再構築できます。


ご回答いただきありがとうございます(また、ご返信に長い時間を割いて申し訳ありません)。書き込みデータベースに対する検証についてあなたが言うことは必ずしも真実ではありません。別のコメントで述べたように、私が遊んでいたサンプルES実装では、書き込みデータベースを同期的に更新する(およびconcurrencyId / timestampを保存する)ようにしました。これにより、EventStoreから準備する必要なく、楽観的な同時実行違反を検出できました。確かに、同期書き込みだけではデータの破損を防ぐことはできませんが、シングルアクセス(シングルスレッド)書き込みも行っていました。
メタファイト

そのため、一貫性の問題を整理しました。ただし、これはスケーラビリティを犠牲にしていると思いました。
メタファイト

書き込みデータベースへの同期書き込みは依然として破損の危険を伴います。イベントストアへの書き込みは成功したが、書き込みデータベースへの書き込みが失敗した場合はどうなりますか?
フョードルソイキン

1
読み取りプロジェクションが失敗した場合、成功するまで再試行します。完全にクラッシュした場合、ウェイクアップしてクラッシュした場所から続行します。つまり、再試行します。外部で観察可能な効果は、少し遅いだけの場合と変わりません。プロジェクションが失敗し続け、一貫して失敗する場合、それはバグがあることを意味し、修正する必要があります。修正後、最後の正常な状態から実行を再開します。バグの結果として読み取りデータベース全体が破損した場合、イベント履歴を使用してデータベースをゼロから再構築するだけです。
フョードルソイキン

1
データが失われることはありません。それが大きなポイントです。しばらくの間、データが不便な(読み取りのために)形のままになることがありますが、決して失われることはありません。
フョードルソイキン

3

主な理由はパフォーマンスです。コミットごとにスナップショットを保存できます(commit = 1つのコマンドで生成されるイベント、通常は1つのイベントのみ)が、これにはコストがかかります。スナップショットに沿って、コミットも保存する必要があります。そうしないと、イベントソースになりません。そして、これらはすべて原子的に、またはすべてなしで行われなければなりません。質問は、個別のデータベース/テーブル/コレクションが使用されている場合にのみ有効です(そうでない場合は正確にイベントソーシングになります)。したがって、一貫性を保証するためにトランザクションを使用する必要があります。トランザクションはスケーラブルではありません。追加専用のイベントストリーム(イベントストア)は、スケーラビリティの母です。

2番目の理由は、集計カプセル化です。あなたはそれを保護する必要があります。これは、集計がいつでも内部表現を自由に変更できることを意味します。それを保存し、それに大きく依存している場合、バージョン管理に非常に苦労します。最適化としてのみスナップショットを使用する状況では、スキーマの変更時にそれらのスナップショットを単に無視します(単に?私は本当にそうは思いません。効率的な方法とその管理)。


集計スキーマが変更された場合、更新された「書き込み」データベースを生成するためにイベントを再生するだけの簡単な問題ではないでしょうか?
メタファイト

問題はその変化を検出していることです。Aggregateは非常に大きく、多くのファイル/クラスがあります。
コンスタンティンガルベヌ

分かりません。変更はソフトウェアリリースで発生します。このリリースには、おそらく「書き込み」データベースを再生成するデータベーススクリプトが付属しています。
メタファイト

移行スクリプトを作成するのは大変な作業です。実行中はアプリを停止する必要があります。
コンスタンタンガルベヌ

@MetaFightストリームが非常に大きい場合、新しい集計スキーマを再構築するのに時間がかかります...私は今、新しい集計のリリース前に実行できるライブ投影の状態であるスナップショットを考えていますスキーマ
Narvalex
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.