Hatena::ブログ(Diary)

yvsu pron. yas このページをアンテナに追加 RSSフィード

2009-11-20

App Engineでバージョンによる楽観的排他制御

Song of Cloudで送金のトランザクション処理パターンが紹介されていました。

http://songofcloud.gluegent.com/2009/11/blog-post_18.html

同様のpython版がこちら

Distributed Transactions on App Engine - Nick’s Blog

上記のやり方で基本的には問題はないのですが、バージョン管理による楽観的排他制御を行っていないので、送金だけを考えるなら、残高を差分で更新しているので大丈夫ですが、これを一般的なパターン拡張しようとすると、楽観的排他制御は必要になります。


楽観的排他制御とは、エンティティにバージョン番号を持たせておいて、メモリ読み込んだときのバージョン番号と書き込むときのバージョン番号が等しいことを確認する方法で、RDBMSの場合は、次のようなSQLを実行することで実現します。メモリの読み込んだときのkeyは1でバージョンは10だと思ってください

update hoge set version = 11, ... where key = 1 and version = 10

AさんもBさんもkeyが1でversionが10のエンティティを読み込んだとします。Aさんが先に更新したとするとAさんの更新は成功してversionは11になります。次にBさんが更新すると既にversionが変わっているので、更新が失敗します。

もし楽観的排他制御を行っていないとAさんの更新はBさんの更新で上書きされてしまい、なかったことになってしまいます。そんなことは防がなければなりません。

バージョンによる楽観的排他制御は、トランザクションと同様なくらい重要なもので、トランザクションとあわせて必ず理解しておく必要があります。


上記は、RDBMSの時の話で、Bigtableは条件付の更新サポートしていないので、同じようにすることはできません。ぱっと思いつくのは、get()してバージョンを確認する方法です。

Hoge hoge = ...;
if (Datastore.get(Hoge.class, hoge.getKey()).getVersion().equals(hoge.getVersion())) {
    hoge.setVersion(hoge.getVersion() + 1);
    Datastore.put(hoge);
} else {
    throw ...
}

このやり方は、get()からput()までの間に別の人に更新されてしまう可能性があるので、うまくいきません。synchronizedなどを使う方法もApp Engineの場合は、別のサーバーで動いているのでうまくいきません。

実は、まさにこの方法をとっているのが、AppEngineのJDOなんだけどね(笑)。


ではどうすればいいのかというと、トランザクションの中でget()してバージョンを確認します。トランザクション中でget()した場合は、commit()が成功した場合は、get()からcommit()までの間に他のプロセス更新していないことをAppEngineが保証してくれます。

詳しくはApp Engineのユニーク制限を正しく理解しよう - yvsu pron. yas


正しい処理はこんな感じ

Transaction tx = Datastore.beginTransaction();
Hoge hoge = Datastore.get(Hoge.class, key);
if (hoge.getVersion().equals(version)) {
    hoge.setVersion(hoge.getVersion() + 1);
    hoge更新
    Datastore.put(hoge);
} else {
    Datastore.rollback(tx);
    throw ...
}

Slim3にはversionプロパティに@Attribute(version = true)とつけておくと、get()でのversionプロパティの比較とputの時に更新することを自動的にやってくれます。だからこんな感じ。

Transaction tx = Datastore.beginTransaction();
try {
    Person p = Datastore.get(Person.class, key, version);
    p.setSalary(newSalary);
    Datastore.put(p);
    Datastore.commit(tx);
} catch (ConcurrentModificationException e) {
    Datastore.rollback(tx);
    throw e;
}

詳しくはこちら。

Optimistic Locking with version property - Slim3


JDOトランザクションの中で使えば、楽観的排他制御が実現できますが、putの直前にもう一度get()が呼び出されるので、パフォーマンスは悪くなります。JDOのやっていることはこんな感じ。

ただし、これは理解するための擬似的なコードで最新のデータを取ってきてversionを比較しているところはJDO自動的にやってくれます。

PersistenceManager pm = PMF.get().getPersistenceManager();
try {
    Transaction tx = pm.currentTransaction();
    tx.begin();
    try {
        Person p = (Person) pm.getObjectById(Person.class, key);
        p.setSalary(newSalary);
        Person latest = (Persion) pm.getObjectById(Person.class, key);
        if (latest.getVersion().equals(p.getVersion())) {
            p.setVersion(p.getVersion() + 1);
            pm.makePersistence(p);
            tx.commit();
        }
    } finally {
        if (tx.isActive()) {
            tx.rollback();
        }
    }  
} finally {
    pm.close();
}

かなり残念な感じですが、AppEngineのJDOは残念間満載なので仕方ないですね。


ともあれ、楽観的排他制御は、必ず理解しておいたほうがいいです。特にAppEngineの楽観的排他制御はほとんどの人が理解できていないんじゃないかと心配です。公式のドキュメントにないから仕方ないかもしれないけど。Slim3だと公式のドキュメントにきちんと書かれていますよ。

nanashinanashi 2009/11/21 11:06 こんにちわ。
上記のサンプルはpさんのサラリーについての一つの要素なのですが、前述の送金のトランザクション処理パターンはaさんとbさんの口座の二つの要素についてですよね?
単純に下記のようにすると、aさん、bさんの口座が同じEntityGroupに属さなければならなく、そうすると、aさんからzzさんまでの口座が複数あったときに、更新する場合は常に一組ずつしか更新できないのが残念だということかと思いましたが、これでいいのでしょうか?

Transaction tx = Datastore.beginTransaction();
try {
Account a = Datastore.get(Account.class, key, version);
Account b = Datastore.get(Account.class, key, version);
a.setAmount(a.amount - 100); //aさんの口座から100円をbさんの口座に振り込み
b.setAmount(b.amount + 100);
Datastore.put(a);
Datastore.put(b);
Datastore.commit(tx);
} catch (ConcurrentModificationException e) {
Datastore.rollback(tx);
throw e;
}

安東@日経SYSTEMS安東@日経SYSTEMS 2009/11/21 11:51 ひがさま

http://code.google.com/intl/ja/appengine/docs/java/datastore/transactions.html#Using_Transactions
などを読み,JDOでもバージョン管理はDatastoreサービス側が自動的にやってくれると理解しています。

上記のJDOの例は,Datastoreサービス内部の実装を表しているのでしょうか? slim3の内部ではより効率的な実装があるのでしょうか。

higayasuohigayasuo 2009/11/21 20:36 > nanashi さん

私が書いたのは、バージョンによる楽観的排他制御の話で、送金処理とは、また違います。
送金処理は、もともとあるあのやり方でいいのですが、分散トランザクションの観点だと、楽観的排他制御も考慮したほうがいいということです。

higayasuohigayasuo 2009/11/21 20:39 > 安東@日経SYSTEMSさん

JDOのバージョン管理とDatastoreの楽観的排他制御は無関係です。上記のJDOの例は、JDOの内部実装を擬似的に書いたものです。

Slim3はputの直前にもう一度getするようなことはしないので、JDOに比べると効率的です。

nanashinanashi 2009/11/22 00:18 higayasuoさん、ご回答ありがとうございます。

よく読み直したところ、higaさんの意図は理解しました。
ではトランザクションをバージョンによる楽観的排他制御で置き換えた場合にリスクがありますか?

内部的に、versionの比較を行ってくれるので、もし操作中に、他のユーザがデータを書き換えた場合、version値が変わるので、rollbackされますね?
最初にgetVersionしてから、データ操作してsetVersionする間に、他のユーザがデータ操作を終了し、versionが書き換わる可能性は、かなり希ではありますが、ありえるかと思いますので、やはり置き換えではなく併用ということでしょうか?

あとentity groupは異なるkindで(例えば、accountの例でいうと、accout kindと入出金処理kind)構成できるということでいいですか?

また、複数のentity groupに所属できない、EntityGroupが一度構成されたら変更できないとのことですが、削除して再構成することもできないのでしょうか?
再構成できるなら、話は簡単なように思いますが。

higayasuohigayasuo 2009/11/22 10:25 > nanashiさん

Bigtableに対するバージョンによる楽観的排他制御は、トランザクションと組み合わせないと動かないので、置き換えるものではありません。

EntityGroupはキーの親子関係だけなので、Kindは自由に組み合わせることができます。

EntityGroupはキーによって構成されているので変更はできません。もちろん、キーを作り直してプロパティをコピーすれば再構成はできますが、それはEntityGroupに限らずどれに対してもいえることです。
キーを作り直すと、その作り直されるキーが別のところで参照されているかもしれないので、漏れなく変更するのは手間が多い割にはメリットがない気がします。
作り直されたキーを変更するにもまた別のトランザクションがいるので複雑になるだけです。

nanashinanashi 2009/11/22 15:59 higaさん、ご丁寧な説明をいただきまして、ありがとうございました。
もうひとつだけ、例えば、accountと入出金処理について、入出金処理はどんどん増えていきますよね?txの都度、entity groupに追加していくということは、entity groupはどんどん肥大化していくことになるかと思うのですが、そうすると、巨大なentity groupに対する処理のリスクがいずれ起こりえると考えていいでしょうか?またこのことを回避するにはどのようにすべきでしょうか?

higayasuohigayasuo 2009/11/23 13:44 > nanashiさん

EntityGroupは大きくなっても同時に更新されることがなければあまり問題にはなりません。
Accountの例だと、もともと同時に更新すると問題が起きるので同時に更新できなくても大丈夫。つまり、EntityGroupが大きくなってもそれほど問題はありません。

投稿したコメントは管理者が承認するまで公開されません。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証