トランザクションの直列化可能性と分離

このページでは、トランザクション データの競合、直列化可能性、分離について説明します。トランザクション コードのサンプルについては、トランザクションとバッチ書き込みをご覧ください。

トランザクションとデータ競合

トランザクションが正しく行われるには、読み取りオペレーションによって取得したドキュメントが、そのトランザクションの外部のオペレーションによって変更されないようにする必要があります。別のオペレーションがそれらのドキュメントのいずれかを変更しようとすると、そのオペレーションと該当のトランザクションの間でデータ競合状態となります。

データ競合
2 つ以上のオペレーションが同一のドキュメントを制御しようとして競合する状態です。たとえば、あるトランザクションでドキュメントの一貫性を維持する必要がある一方で、同時実行中の別のオペレーションでそのドキュメントのフィールド値を更新しようとする場合などです。

Firestore では、いずれかのオペレーションを遅延または失敗させることで、データ競合を解決します。データ競合が原因で失敗したトランザクションは、Firestore クライアント ライブラリによって自動的に再試行されます。一定回数の再試行が行われると、トランザクション オペレーションは失敗し、エラー メッセージが返されます。

ABORTED: Too much contention on these documents. Please try again.

どのオペレーションを失敗または遅延させるかは、クライアント ライブラリのタイプによって異なります。

  • モバイル SDK とウェブ SDK は、オプティミスティック同時実行制御を使用します。

  • サーバー クライアント ライブラリは、ペシミスティック同時実行制御を使用します。

モバイル SDK とウェブ SDK のデータ競合

モバイル SDK とウェブ SDK(iOS、Android、ウェブ、C++)では、オプティミスティック同時実行制御を使用してデータ競合を解決します。

オプティミスティック同時実行制御
データ競合の可能性が低い、またはデータベースをロックしたままにするのは効率的でないという前提に基づきます。オプティミスティック トランザクションでは、他のオペレーションによるデータ変更をブロックするためにデータベースをロックすることはありません。

モバイル SDK とウェブ SDK は、レイテンシが高く信頼性が低いネットワーク接続環境でも動作できるようにするため、オプティミスティック同時実行制御を使用します。高レイテンシ環境でドキュメントをロックすると、データ競合による失敗があまりにも多く発生することになります。

モバイル SDK とウェブ SDK では、トランザクション内で読み取ったすべてのドキュメントをトランザクションが追跡します。トランザクションの実行時にこれらのドキュメントが変更されていない場合にのみ、トランザクションによって書き込みオペレーションが行われます。ドキュメントが変更されていた場合は、トランザクション ハンドラはトランザクションを再試行します。何度か再試行してもトランザクションがクリーンな結果を得られなかった場合、データ競合によりトランザクションは失敗します。

サーバー クライアント ライブラリでのデータ競合

サーバー クライアント ライブラリ(C#、Go、Java、Node.js、PHP、Python、Ruby)は、ペシミスティック同時実行制御によってデータ競合を解決します。

ペシミスティック同時実行制御
データ競合の可能性があることを前提とします。ペシミスティック トランザクションでは、データベースのロックを使用して、他のオペレーションによるデータの変更を防止します。

サーバー クライアント ライブラリはペシミスティック同時実行制御を使用します。これは、低レイテンシであることと、データベースとの接続の信頼性が高いことを前提としているためです。

サーバー クライアント ライブラリでは、トランザクションは読み取ったドキュメントをロックします。あるトランザクションがドキュメントをロックすると、他のトランザクション、バッチ書き込み、トランザクション以外の書き込みによるドキュメントの変更がブロックされます。トランザクションは commit 時にドキュメントのロックを解除します。また、なんらかの理由でタイムアウトまたは失敗した場合にもロックが解除されます。

トランザクションがドキュメントをロックすると、他の書き込みオペレーションは、そのトランザクションによってロックが解除されるのを待つ必要があります。トランザクションは時系列順にロックを取得します。

直列化可能な分離

トランザクション間のデータ競合は、データベースの分離レベルと密接に関連しています。データベースの分離レベルとは、同時実行オペレーション間の競合をシステムが適切に処理する度合いを指します。競合は、データベースにおける次のような要件によって発生します。

  • トランザクションには正確で一貫性のあるデータが必要である。
  • リソースを効率的に使用するため、データベースはオペレーションを同時実行する。

分離レベルが低いシステムでは、あるトランザクション内の読み取りオペレーションが、同時実行されている別のオペレーションの変更がまだ commit されていない不正確なデータを読み取ってしまう可能性があります。

直列化可能な分離による、最大の分離レベルが定義されます。直列化可能な分離とは、次のことを意味します。

  • データベースがトランザクションを順に実行していると仮定できる。
  • トランザクションは、同時実行されているオペレーションの commit されていない変更の影響を受けない。

データベースが複数のトランザクションを並列に実行している場合でも、これらが保証される必要があります。データベースは同時実行制御を実装して、この保証を脅かす競合を解決する必要があります。

Firestore は、トランザクションの直列化可能な分離を保証します。Firestore 内のトランザクションは、commit 時間によって直列化され、分離されます。

commit 時間によるシリアル化可能な分離

Firestore は各トランザクションに単一の時点を表す commit 時間を割り当てます。Firestore がトランザクションによる変更をデータベースに commit するときには、トランザクション内のすべての読み取りと書き込みが commit 時間ちょうどに行われると仮定できます。

ただし、トランザクションを実際に実行するには一定の時間の幅が必要です。commit 時間の前にトランザクションの実行が開始され、複数のオペレーションの実行が重なって実行される可能性があります。Firestore は直列化可能な分離を維持し、以下のことを保証します。

  • Firestore は、commit 時間の順番でトランザクションを commit します。
  • Firestore は、commit 時間が後の同時実行オペレーションからトランザクションを分離します。

同時実行オペレーション間でデータ競合が発生した場合、Firestore は、オプティミスティック同時実行制御とペシミスティック同時実行制御を使用して競合を解決します。

トランザクション内の分離

トランザクション分離は、トランザクション内の書き込みオペレーションにも適用されます。トランザクション内のクエリと読み取りでは、そのトランザクション内の以前の書き込みの結果が読み取られることはありません。あるトランザクション内でドキュメントを変更または削除しても、そのトランザクション内のすべてのドキュメント読み取りでは、トランザクションの書き込みオペレーションが行われる前の、commit 時間のドキュメントのバージョンが返されます。その時点でドキュメントが存在していなかった場合、読み取りオペレーションは何も返しません。