人間のあるべき姿の探索

思索・人形・エンジニアリング

イベントソーシングを用いた結果整合性による在庫管理の実装

はじめに

ここ十数年、顧客向けサービスが大量のリクエストを扱う必要が出てきました。例えば、Amazonのような通販サービスは24時間稼働し、常に大量の注文が行われます。ライブの予約サービスであれば、予約開始時点で大量の予約が殺到し、在庫の引き当てを行い結果を返す必要があります。

こういった整合性の担保を必要とする大量のリクエストを扱う場合、以前はSQLで厳密にトランザクション管理をしていました。しかし、この方法ではリクエストの増大に対してスケールアップしか選択肢が取れず、ロックを確保する関係上リクエストが大量に来るとそれだけ負担がかかります。

一方で、クラウドの台頭により、スケールアウト方式でのスケーリングが可能になったため、トランザクション管理をしない方式でこれを実現することを検討します。より具体的には、予約を一つのトランザクションではなく、”在庫確保リクエスト”と”在庫確保可否判定”の二つの処理に分離します。

前提知識

イベントソーシング

ドメインに、データの現在の状態だけを格納する代わりに、追加専用ストアを使用して、そのデータに対して実行された一連のすべてのアクションを記録します。 ストアは、レコードのシステムとして機能し、ドメイン オブジェクトを具体化するために使用できます。 これにより、データ モデルとビジネス ドメインの同期の必要性を避けることで、パフォーマンス、スケーラビリティ、および応答性を向上させながら、複合ドメインでのタスクを簡略化できます。 さらに、トランザクション データの整合性を提供し、補正アクションを有効にできる完全な監査証跡と履歴を保持することもできます。

イベント ソーシング パターン - Azure Architecture Center | Microsoft Learn

通常オブジェクトを中心にその処理や永続化を考えると、オブジェクト単位でのクラスやテーブルが作成されます。例えば、商品在庫は商品オブジェクトのプロパティや在庫増減のメソッドを用いて以下の様に表現し、これをそのまま永続化します。本当は発注/受注は実行タイミングで実際の在庫の数が増えるわけではないのですが、一旦簡易化してnum分だけ増減するイメージとします。現物の在庫数と受注可能数が異なる点は与信の概念に近いかもしれません。

また、発注と受注に対応する英語については仮でOrder及びReceiptを割り当てています。Orderは外部から在庫を確保し在庫を増やす、Receiptは顧客から受注して在庫の引き当てを行い、引き当てができたら在庫を減らします。

class Product
{
  public string Name {get; private set; }
  public int UnitPrice {get; private set; }
}

class ProductStock
{
  private List<Product> products;
  public void OrderStock(int num){} // 省略
  public void ReceiptStock(int num){} // 省略
}

それに対して、イベントソーシングでは在庫をProductオブジェクトではなく、発注や受注といったイベントに注目して定義して、これを永続化します。

class StockEvent
{
  public EventType Type {get; set; } 
  public int Number  {get; set; } 
}

現在及び各時点での在庫を確認したい場合、以下の様に履歴を追っていきます。そして、履歴を追った結果在庫数が正であれば、受注確定イベントを記録して、ユーザーに通知します。

var events = {
  new StockEvent(EventType.Order, 10),
  new StockEvent(EventType.Receipt, 5),
  new StockEvent(EventType.Order, 15)
}
foreach(var event in events)
{
  ProductStock.Apply(event);
}
if(ProductStock.Num >= 0)
{
  var completionEvent = StockDeterminEvent(EventType.Complete, TransactionId);
  _repository.Add(completionEvent);
}

ProductStock.Applyの処理では、在庫がマイナスになるようなイベントは切り捨てて、在庫をマイナスにしません。余談として、Sagaパターンを参考に在庫がマイナスになるイベントに対しては補正イベントを発行するパターンを検討していたのですが、該当イベントと補正イベントの間に本来受注に成功していたはずのイベントが存在することになり、それらのイベントに対しての補正イベントも大量に発行する必要が出て負荷がかかるため、筋が良くなさそうです。

CQRS

イベントソーシングと共に登場することが多い概念です。

CQRS はコマンド クエリ責務分離を表し、データ ストアの読み取りと更新の操作を分離するパターンです。 アプリケーション内に CQRS を実装すると、そのパフォーマンス、スケーラビリティ、セキュリティが最大化される場合があります。

CQRS パターン - Azure Architecture Center | Microsoft Learn

データストアの読み取りと更新の操作を分離する、と言ってもイメージがしづらいので、先の在庫を例にとって説明します。読み取りと更新についてはCRUDのR及びCUDのイメージで問題ありません。在庫に関するR操作としては、現在の在庫数を取得したい場合があります。一方で、在庫に関するCUD操作としては、在庫の発注、受注といった在庫状況が変化する操作です。そして、CUD操作は特にRDBの場合、既存レコードとの整合性を取るためにロックを確保する必要があったり、時間のかかる操作になります。そこで、読み取りと更新をアーキテクチャレベルで行うことで、高速な読み取りと整合性を保った永続化を実施できます。また、読み取り操作の場合は集計のようなデータの保存時の形式とは異なる形でクエリしたい場合も多いため、DBレベルで分離することも可能になります。

実装

ドメインモデリング

メインはStockEventとStockConfirmEventの分離です。在庫イベントが発生したのちに、その種別が受注イベントの場合には都度その在庫が引きあてを確認します。受注の成否を記録するStockConfirmEventに結果が記録されたのち、引き当てに成功していれば発送イベントが発生といった流れになります。



アーキテクチャ構成

 

今までの説明をDDDの4層に置いてみたものです。今回の特徴としてはCQRSの採用があり、Infrastructureで読み取りと更新用でDBとのマッパークラスが分離されています。加えてCosmosDBの方でもデータストア自体が疑似的に分離されています。購入者や商品の情報自体はRDBでデータの整合性を厳密に管理されます。

処理設計

受注/発注イベントが発生すると、ApplicationがEventStockオブジェクトを作成し、CommandModelにマッピングしてそのままEventStoreに配置します。そうすると自動的に今までの履歴が集計されたものがViewに適用されます。Viewには商品ごとの残り在庫数が記録され、Applicationで取得できるようになります。この在庫数の使用方法は二つ存在し、一つは単純に残り在庫数をユーザーがUIで確認する場合ですが、もう一つが受注の成否判定です。ここで、注文した対象商品の在庫数が0未満になっている場合、補正イベントを発行して在庫数を元に戻します。

終わりに

いかがでしたか?結果整合性で在庫管理をしたい!と誰でも一度は考えたことがあるはずですが、実際に実現するにあたってどうすれば良いか?と思いブレークダウンしてみると、意外に複雑になることを思い知りました。勿論実際のワークロードで動作するか確認する必要はありますが、イベントソーシング形式で在庫に関するイベントに注目する方法は、方針の一つとして検討できるのではないかと思います。