月曜日, 11月 16, 2009

【Google App Engine】 レコードのシーケンス番号をカウンタを使わずにつける このエントリーを含むはてなブックマーク


カウンタとレコードのシーケンス番号

 レコードにシーケンス番号(連番)をつけると、全体の件数を取得できたり、Pagingできたりするので、いろいろと好都合である。Pagingだけであれば、わざわざシーケンス番号を付ける必要はないが、大量のレコードを分割してTaskQueueで並列処理させたい場合には、シーケンス番号で範囲指定を行うとよい。(全体の件数はStatics APIでも取得できる)

 シーケンス番号を付けるには、カウンタを管理するEntityを用意して、挿入のたびにインクリメントする方法が一般的だと思うが、これだと前記事で述べたように、カウンタのEntityとデータのEntityをEntityGroupとして括ってしまうことが大きなボトルネックとなってしまう。しかし、トランザクションで括らないと挿入に失敗することがあるため歯抜けのシーケンス番号となってしまう。
 そこで、カウンタのEntityを使わないで、うまくシーケンス番号をつける方法がないものか考えてみた。
 
EntityGroupを使わないでシーケンス番号をつける方法


 すぐに思いつくのが、既存データのシーケンス番号の最大値を取得して、それに+1したものを、新規に追加するレコード番号としてセットする方法。

 まず、シーケンス番号をidとすると、
 query.addSort("id", Query.SortDirection.DESCENDING);


 とし、最初の1件を取得することでレコードの最大値を取得する。(これにかかる実行時間は10万件レコードでも約0.6秒と高速である。)そして、この値に+1したものを新規レコードのidとしてセットする。

 これは一見うまくいくように思えるが、取得して更新する間に他の人が追加してしまう場合があるので意味がない。トランザクションで括ればいいと思うかもしれないが、やっかいなことに、queryはトランザクションに参加することができないので、一旦、最大値をqueryで取得しておいて、ユニーク制限を行ったPUTを実行することになる。これは、少々冗長かもしれないが、EntityGroupを使う場合に比べ、排他対象がKindからレコードに狭まるので、その分並行実行可能になって高速になると思われる。 ユニーク制限を行ったPUTについては、詳しくは、ひがさんのBlog、App Engineのユニーク制限を正しく理解しよう)を参照してもらいたい。


レコード挿入時
 1.queryでシーケンス番号idの最大値を得る
 2.+1したidをセットした新規レコードを作成してputUniqueValue()する
 3.エラーで返ってきた場合、さらにidを+1してputUniqueValue()を再実行

putUniqueValue(String uniqueIndexName, String value)
 1.valueを元にKeyを生成
 2.トランザクション開始
 3.生成したKeyでGET。既に存在していればエラーで返す。
 4.もしなかったらPUTしてコミット


 これは、どんなにデータが増えても、Query(0.62s)とKeyによるGET(0.03s)の2回の検索および、PUT(0.25s)でシーケンス番号を作成できる。しかし、リクエストが増えてしまうと、コンテンションが多発してリトライ回数が増えるため、もう少し時間はかかる。それは、QueryからPUTまでの0.62s+0.03s+0.25s)の間にどれだけのリクエストが入ってくるかによる。リトライは2回目以降はQueryはしないので、GET(0.03s)を繰り返すことになる。また、GETで成功してもPUTまでの間に他によって更新されていると、ConcurrentModificationExceptionが発生してリトライとなるが、よほどのリクエストがこない限り大丈夫だと思う。たぶん。ちなみに、件数が増えてもQueryが速いのはPropertyIndexのおかげである。

さらなる応用、最新レコードに件数を格納する

 履歴をレコードとして持つ場合、idの最大値やDatastore statisticsのcountだと履歴分も全て件数に含まれてしまうので注意が必要である。また論理削除する場合も同様に削除データが件数に含まれる。履歴を持つ、または論理削除する場合では、条件を指定してKeyの個数を検索する方法もアリだが、10万件で約17秒かかるので、十数万件になると30秒ルール(あるいはTaskQueueの10秒ルール)にひっかかって使えなくなってしまうだろう。
 これには、以下のように最新のレコードに件数を格納することで、対応することができる。(もちろん、カウンタEntityを別途用意してもよい)

レコード挿入時に最新の件数を格納する
 1.queryでシーケンス番号idの最大値を得る
 2.+1したidをセットした新規レコードを作成する。その際、新規レコードのcountプロパティの値を更新する。(追加であれば+1、削除であれば-1、更新であれば何もしない)
 3.putUniqueValue()する
 4.エラーで返ってきた場合、queryを再実行してidやcountを更新してputUniqueValue()を再実行


 ただし、これをやるには、レコードをRevision番号で管理するという前提で考える必要があり、更新において新しいRevision番号でレコードを追加する必要がある。

詳しくは、疎結合とバージョニングについてを参照のこと。

0 件のコメント:

 
© 2006-2015 Virtual Technology
当サイトではGoogle Analyticsを使ってウェブサイトのトラフィック情報を収集しています。詳しくは、プライバシーポリシーを参照してください。