Google App EngineでGlobal Transaction

Google App EngineにはTransactionは1つのEntity Group内でしかできないという制限があります。詳しくは、App EngineのEntityGroupを理解しよう - yvsu pron. yasを参照してください。


そうするとある口座から別の口座にお金を振込むような送金のパターンで、Transactionを利用することができません(すべての口座を1Entity Groupに押し込むと更新がぶつかって現実的ではないから)。送金パターンで整合性を保つためには、理論的には次のようになります。
http://songofcloud.gluegent.com/2009/11/blog-post_18.html
実装するとこんな感じ。
http://blog.notdot.net/2009/9/Distributed-Transactions-on-App-Engine
コードの量的にはそんなにたいしたことありませんが、質的な難易度はかなり高く、間違いなく実装するのはかなり大変です。


そこで、Slim3で複数のEntityGroupにまたがるTransactionをサポートするGlobalTransactionの機能を実装しました。使い方は簡単で、次のようにGlobalTransactionクラスを使うだけ。


GlobalTransaction gtx = Datastore.beginGlobalTransaction();
Acount acount = gtx.get(Acount.class, key);
Acount acount2 = gtx.get(Acount.class, key2);
if (acount.getAmount() >= 1000) {
acount.setAmount(acount.getAmount() - 1000);
acount2.setAmount(acount2.getAmount() + 1000);
} else {
throw new IllegalStateException(...);
}
gtx.put(acount, acount2);
gtx.commit();


EntityGroupのTransactionしかサポートしていないApp Engineで、Slim3はどのようにGlobal Transaction(今後はgtxと省略)を実装しているのでしょうか。


Slim3は、gtx中にget, put, deleteしたデータに対してEntity Groupごとにロックをかけます。ロックに失敗したらConcurrentModificationExceptionを投げ、既に確保したロックがあれば自動的に開放します。ロックが成功した場合は、他から操作される心配がないので、安心してデータをいじることができます。
put, deleteしたデータは、Datastoreに直接反映されるわけではなく、gtx内にJournalとして記録されています。Journalには後からput, deleteした内容を再現できるようなデータが保存されています。


commit()が呼び出されると、最初にメモリ内のJournalをDatastoreに格納します。永続化するのは、この後のJournalの適用フェーズでプロセスが落ちてしまった場合でも後からもう一度再実行出来るようにするためです。Journalの保存に失敗した場合は、既に保存したJournalがあれば削除し、ロックも開放します。


Journalの保存が成功したら、GlobalTransactionのEntityをDatastoreに格納します。格納に成功したら、このgtxは論理的にはcommit済みで成功したとみなされます。それでは、gtxを保存した後に、プロセスが落ちてしまったらどうなるのでしょうか。


Slim3ではcommit済みでまだ処理されていないgtxがないかcronで5分毎にチェックしています。存在していれば、Journalの内容をDatastoreに自動的に反映させます。
プロセスが落ちてからcronでリカバリーされるまで若干タイムラグがある可能性はありますが、データはロックされたままなので、他から書き換えられる心配はありません。タイムラグが気になる場合には、cronの間隔を短くすれば大丈夫ですが、プロセス(App Server)が落ちることはまずないので、あまり神経質にならなくても大丈夫だと思います。神経質な方は、cronの間隔を短くしてくださいw。


gtxの保存に成功したら、Journalの内容をDatastoreに反映させます。この処理の途中でプロセスが落ちた場合でも上記と同様cronでリカバリーされます。


Journalの反映が終わったら、ロックを解除し、gtxのEntityをDatastoreから削除して、このgtxは終了します。


App Engineでは30秒以内ですべての処理を終えないといけないというルールがあり、30秒を超えるとDeadlineExceedExceptionが発生します。gtx中にDeadlineExceedExceptionが発生するとどうなってしまうのでしょうか。


Slim3ではFilterが設定されていて、DeadlineExceedExceptionが発生したときに、gtxの保存前なら、rollbackするようになっています。gtxの保存後なら、cronによって自動的にリカバリーされます。


今回のGlobalTransactionのロジックは、@asigeru(あらかわさん)にかなりアドバイスをもらいました。本当にありがとう。

ソースコードはこんな感じ
GlobalTransaction.java
Lock.java
Journal.java
GlobalTransactionServlet.java
DatastoreFilter.java



App Engineの最大に懸念点であったTransaction問題はこれで解決されました。もうこれで、バリバリ開発できますね。でも、集合演算だとかFilterに制限があるとか、joinができないとかまだ、問題が残っているじゃないかだって?


集合演算に関しては、count, min, maxはSlim3でサポートされています。App Engineで集合演算は直接サポートされていませんが、ちょっとした工夫でなんとかなります。
例えば、countはどうやっているかというと、App EngineにはKeyだけを取ってくるKeys Only Queryがあるので、Keys Only Queryを実行してその件数をcountすればいいのです。1万件でも1秒くらいでcountできます。
minはどうするかというと、minを取りたい列の昇順でソートし、offset=0, limit=1で取ってくれば、最小値が一瞬で取ってこれます。maxはその逆です。


QueryによるFilterにはいろいろ制限がありますが、Slim3ではin-memoryのFilterを用意しているので、その制限を超えることができます。QueryではできないLikeの%XXX%や%XXXに相当するcontains, endsWithも実行できます。


EmployeeMeta e = EmployeeMeta.get();
List list = Datastore.query(e)
.filterInMemory(e.name.endsWith("ABC"), e.name.notEqual("SCOTT"), e.salary.ge(1000))
.sortInMemory(e.name.desc)
.asList();


joinができない問題は、joinしたくなる場合は、モデルの設計が悪いというのは、ちょっと言い過ぎで、Keyを持たせてlazy loadingすればいいのです。最近のO/R Mapperは基本的にjoinを明示的に書くのではなくlazy loadingされますよね。あれと同じです。RDBMSを使っている場合は、lazy loadingするとパフォーマンスの問題が出る場合もありますが、BigtableはKeyでgetするのは速いのでそれほど問題にはなりません。


ほら、Bigtableは制限が多すぎて使い辛いというのは、過去の話だということが理解できましたよね。時代は進化しているのです。


Slim3はGlobalTransactionのドキュメントを書いたらRC1をリリースします。たぶん来週のどこか。RC1リリース後は、新機能は追加せず、バグフィックに徹し、一月後くらいに正式版をリリースする予定。


GAE/Jの歴史は、去年の4月から始まったわけですが、結構苦労の連続でした。何故かっていうと、既存のフレームワークをApp Engineに対応させようとしたから。
今後は、Slim3のようなApp Engine nativeなフレームワークが出てきてどんどん使いやすくなることでしょう。これは、Javaだけでなく、Pythonも同じ流れです。kay-frameworkとかでてきてるしね。
http://kay-docs-jp.shehas.net/


2月20日(土)Java Cloud Meeting Fukuoka 2010をやります。GlobalTransaction以外にもいろいろ面白いねたがあるので、ぜひお申込みください。よろしくお願いします。

詳細はこちらを
http://event.seasarfoundation.org/jcmf2010/about/
申し込みはこちらから。会場代とかあって有料になって申し訳ありませんが、がんばって面白い話します。学生は無料になる予定です。貧乏なイベントなので予算が出なかったら、学生分は私が持つので大丈夫。
https://event.seasarfoundation.org/jcmf2010/attend/


@kisもなんとか度メーカーの話をするようです。
http://d.hatena.ne.jp/nowokay/20100208#1265596892