App Engineでのトランザクションの分離

http://code.google.com/appengine/articles/transaction_isolation.html

はじめに

Wikipediaによるとデータベース管理システムのトランザクションの分離レベルは「ある操作による変更が並列に実行される他の操作から見えるようになるタイミングとその手段によって定義される」そうだ。この記事ではGoogle App Engineデータストアでのトランザクションの分離について説明したい。記事を読めば並列な読み書きがどのように実行されるかより深い理解が得られるだろう。

Read Committed

データベースによってサポートされる分離レベルは4つ(Serializable, Repeatable Read, Read Committed, Read Uncommitted)だが、データストアの分離レベルはそのうちのRead Committedとほとんど同じだ。データストアからクエリまたはget()で読み込まれたエンティティはコミットされたデータのみからなる。読み込まれたデータが部分的にコミットされたデータ、つまりいくつかのデータがコミット前のデータでそれ以外がコミット後、ということはありえない。ただし、クエリとトランザクションのやりとりはもう少し分かりにくい。それを理解するにはcommit()のプロセスについて細かく見る必要があるだろう。

commit()プロセス

commit()にはマイルストーンが二つある。エンティティの変更が適用されるときと、エンティティのインデックスの変更が適用されるときだ。最初のポイントをマイルストーンA、次のポイント、commit()が終了するとき、をマイルストーンBと呼ぼう。マイルストーンAに到達したときにはエンティティには全ての変更が適用されており、マイルストーンBに到達するとエンティティのインデックスの変更が適用されている。

http://code.google.com/appengine/articles/transaction_iso.png

マイルストーンAのあとで更新されたエンティティをキーを使って検索すると、エンティティの最新バージョンが得られることは保証されている。ただし、もし同時に届いたリクエストが実行するクエリの述語(SQL/GQL的に言えば`WHERE句')が更新前のエンティティに合致せず、更新後のエンティティに合致するなら、マイルストーンBに到達したcommit()操作の後だった場合だけそのエントリはリザルトセットに含まれる。つまり、 ホンの短い間、もう一度キーを使って検索してもクエリの条件を満たさないエンティティがリザルトセットに含まれることがある。クエリがどのエンティティを返すか決定する際にはマイルストーンAとマイルストーンBの間にあるアカウントの変更は見えないが、クエリが特定のエンティティを返すことを決定したあとは常にマイルストーンAバージョンのエンティティが戻されると言うことに注意しよう。

例

並列な更新とクエリがどのように相互作用するか一般的な説明をしてきたけど、私なら具体的な例を一通り眺めた方がこのコンセプトを簡単に理解できると思う。ちょっと見てみよう。単純なケースから初めて、最終的により興味深い例を提供したい。

Personエンティティを持つアプリケーションを考えよう。Personには次のような属性がある:

  • 名前
  • 身長

また、アプリケーションは次のような操作ができる:

  • updatePerson()
  • getTallPeople(), 72インチよりも背の高い人を全て返す

データストアには2つのPersonエンティティが存在する:

  • アダム, 身長68インチ
  • ボブ, 身長73インチ

例1 − アダムを大きくする

アプリケーションが全く同じタイミングでリクエストを二つ受け取ったとしよう。最初のリクエストではアダムの身長を68インチから74インチにする。爆発的な成長!で、二つ目のリクエストはgetTallPeople()を呼び出す。さてgetTallPeople()が返す値はどうなる?

その回答はリクエスト1によって起動される二つのcommit()マイルストーンと、リクエスト2によって実行されるgetTallPeople()クエリの関係による。次のようだとすると:

このシナリオだとgetTallPeople()はボブだけを返す。なぜかって?それはアダムの身長を高くするための変更がまだコミットされていなくて、リクエスト2が発行するクエリからはまだ見えないからだ。

次にこのようなものを考えてみる:

このシナリオではリクエスト1がマイルストーンBに届く前にクエリが実行されている。つまりPersonのインデックスへの変更はまだ適用されていない。そのため結局getTallPeople()はボブだけを返すことになる。これがクエリの条件を満たすプロパティを持つエンティティがリザルトセットに含まれない場合の例だ。

例2 − ボブを小さくする(ごめん、ボブ)

こちらの例ではリクエスト1がちょっと違う。アダムの身長を68インチから74インチに増やすのではなくボブの身長を73インチから65インチに減らす。この場合、getTallPeople()はどうなるだろう

このシナリオではgetTallPeople()はボブを返す。なぜか?ボブの身長を減らす更新がまだコミットされていないので、リクエスト2に発行されるクエリにはまだ変更が見えないからだ。

次にこのような例を考えよう:

このシナリオだとgetTallPeople()は誰も返さない。ボブの身長を減らす変更はリクエスト2のクエリが発行される時点でコミットされているからだ。

次にこのような例を考えよう:

このシナリオではマイルストーンBの前にクエリが実行されるので、Personのインデックスの変更はまだ適用されていない。そのためgetTallPeople()はまだボブを返すが、Personのheightプロパティは更新された値、65を返す。これがプロパティがクエリの条件を満たさないエンティティがリザルトセットに含まれる場合の例だ。

まとめ

上記の例から分かるように、Google App Engineデータストアのトランザクション分離レベルはRead Committedと極めて近い。もちろん重要な違いもあるがすでに君はその違いと理由を理解しただろう。きっと君のアプリケーションについてより賢明でデータストアに適した決定を下せるようになったはずだ。