Transaction Puzzlers
appengine ja night #4
あらかわ (@ashigeru)
講演者について
名前
あらかわ
(@ashigeru)
所属
株式会社グルージェント
開発部
普段の業務
研究開発
(コンパイラ系)
教育 (Computer Aided Education)
ブログ書き (Song of Cloud Blog)
2010/01/22
appengine ja night #4 - @ashigeru
2
Song of Cloud Blog
会社ブログ
http://songofcloud.gluegent.com
App Engineのポータルサイトをコンセプトに
By arakawa (App Engine関連)
Slim3 Datastoreに乗り換える
テキスト部分一致検索
送金のトランザクション処理パターン
分散トランザクション処理の最適化
SDK 1.2.8 Release Notesで語られなかったこと
App Engine SDK 1.3.0 (overview)
App Engine JDP Tips
グローバルトランザクション処理のパターン
2010/01/22
appengine ja night #4 - @ashigeru
3
今日の内容
トランザクション処理の考え方
トランザクション処理のパターン
2010/01/22
appengine ja night #4 - @ashigeru
4
トランザクション処理の考え方
リソースを一時的に独占できる技術
同時に変更して不整合が起こる、などを回避
今回は悲観的/楽観的をあまり気にしない
App
Engineは楽観的並行性制御
いずれも一時的にリソースを独占できる
設計/実装時には考慮する必要がある
2010/01/22
appengine ja night #4 - @ashigeru
5
App Engineのトランザクション
トランザクションはEntity Group (EG)単位
同一EG内のエンティティに対する操作はACID
複数EGにまたがる操作は対応していない
2010/01/22
appengine ja night #4 - @ashigeru
6
Entity Groupの構成
同じルートキーを持つエンティティ群
データストア上で近くに配置される
例
Foo(A)
Foo(A)/Hoge(B)
EG: Foo(B)
Foo(B)
Bar(A)/Foo(A)
Bar(A)/Foo(B)/Hoge(D)
2010/01/22
EG: Foo(A)
EG: Bar(A)
appengine ja night #4 - @ashigeru
7
Entity Groupの特徴
ポイント
トランザクションの範囲はエンティティ作成
時に決まり、変更できない
EGを大きくするとトランザクションで独占す
るエンティティが多くなる
EGの設計が非常に重要に
間違えると並列性が極端に低下する
うまくやればスケールアウトする
2010/01/22
appengine ja night #4 - @ashigeru
8
ここまでのまとめ (1)
App EngineのトランザクションはEG単位
EG内ではACIDトランザクション
EGをまたぐトランザクションは未サポート
EGの設計によっては並列性が落ちる
EGを大きくすると独占範囲が広がる
EGを分断すると整合性を保つのが困難
2010/01/22
appengine ja night #4 - @ashigeru
9
トランザクション処理のパターン
App Engineのトランザクションはやや特殊
パターンで対応したほうがよさそう
本日紹介するもの
Read-modify-write
トランザクションの合成
ユニーク制約
冪(べき)等な処理
Exactly
Once
BASE Transaction
2010/01/22
appengine ja night #4 - @ashigeru
10
注意点
プログラムの説明に擬似コードを多用
言語はJavascriptライク
APIはJavaのLow-Level
APIライク
見慣れない言語要素
キーリテラル
– KEY:…
KEY:Foo(A), KEY:Foo(A)/Bar(B), など
データストア
get(tx, key), put(tx, entity), beginTransaction()
タスクキュー
enqueue([tx,] statement)
2010/01/22
appengine ja night #4 - @ashigeru
11
パターン: read-modify-write
エンティティのプロパティを変更する
例:
カウンタの増加
ショッピングカートに商品を追加
現在の値をもとに次の値が決まる
読む、変更、書き戻す、の3ステップが必要
途中で割り込まれると不整合が起こる
2010/01/22
appengine ja night #4 - @ashigeru
12
read-modify-write (1)
考え方
読んでから書き戻すまでエンティティを独占
100 + 1
100
2010/01/22
101
appengine ja night #4 - @ashigeru
13
read-modify-write (2)
var tx = beginTransaction()
try {
var counter = get(tx, KEY:Counter(C))
counter.value++
put(tx, counter)
tx.commit()
}
finally {
if (tx.isActive()) tx.rollback()
}
2010/01/22
appengine ja night #4 - @ashigeru
14
read-modify-write (3)
var tx = beginTransaction()
try {
var counter = get(tx, KEY:Counter(C))
counter.value++
put(tx, counter)
読んでから書き戻す
tx.commit()
までをACIDに行う
}
finally {
if (tx.isActive()) tx.rollback()
}
2010/01/22
appengine ja night #4 - @ashigeru
15
DSL: atomic (tx) { … }
以後は下記のように省略
トランザクションの開始と終了を簡略化
atomic(tx) {
var counter = get(tx, KEY:Counter(C))
counter.value++
put(tx, counter)
}
2010/01/22
appengine ja night #4 - @ashigeru
16
パターン: トランザクションの合成
同じEGに対する複数のトランザクション
処理を合成
例:
2つのカウンタを同時に変更
(恣意的)
非正規化した2つの情報を同時に更新
注意点
分断したトランザクションでは、途中で失敗
した際に修復が大変
2010/01/22
appengine ja night #4 - @ashigeru
17
トランザクションの合成 (1)
考え方
同じEGのトランザクションが2つあったら、
一度に処理してしまう
15
30
2010/01/22
16
31
appengine ja night #4 - @ashigeru
18
トランザクションの合成 (2)
atomic(tx) {
var a = get(tx, KEY:Eg(C)/Counter(A))
a.value++
put(tx, a)
var b = get(tx, KEY:Eg(C)/Counter(B))
b.value++
put(tx, b)
}
2010/01/22
appengine ja night #4 - @ashigeru
19
トランザクションの合成 (3)
atomic(tx) {
var a = get(tx, KEY:Eg(C)/Counter(A))
a.value++
put(tx, a)
var b = get(tx, KEY:Eg(C)/Counter(B))
b.value++
put(tx, b)
同じEGのエンティティ
}
に対する操作
2010/01/22
appengine ja night #4 - @ashigeru
20
トランザクションの合成 (4)
atomic(tx) {
var a = get(tx, KEY:Eg(C)/Counter(A))
a.value++
put(tx, a)
var b = get(tx, KEY:Eg(C)/Counter(B))
b.value++
put(tx, b)
複数のトランザクション
}
を合成, 全体がACIDに
2010/01/22
appengine ja night #4 - @ashigeru
21
パターン: ユニーク制約
重複するエンティティの登録を防止する
例:
同じIDを持つユーザの登録を防ぐ
ダブルブッキングを防ぐ
注意点
データストアは制約機能を組み込んでいない
クエリはトランザクションに参加できない
2010/01/22
appengine ja night #4 - @ashigeru
22
ユニーク制約 (1)
考え方
エンティティの入れ物ごと独占
入れ物が空なら追加するエンティティは一意
@hoge
2010/01/22
@hoge
@hoge
appengine ja night #4 - @ashigeru
23
ユニーク制約 (2)
var key = KEY:User([email protected])
atomic(tx) {
var user = get(tx, key)
if (user != null) {
throw new NotUniqueException()
}
user = new User(key, ...)
put(tx, user)
}
2010/01/22
appengine ja night #4 - @ashigeru
24
ユニーク制約 (3)
var key = KEY:User([email protected])
atomic(tx) {
var user = get(tx, key)
if (user != null)
{
ユニーク制約をキーで
throw new NotUniqueException()
表す(メールアドレス)
}
user = new User(key, ...)
put(tx, user)
}
2010/01/22
appengine ja night #4 - @ashigeru
25
ユニーク制約 (4)
var key = KEY:User([email protected])
atomic(tx) {
var user = get(tx, key)
if (user != null) {
throw new NotUniqueException()
}
user = new User(key, ...)
そのエンティティが
put(tx, user)
すでにあれば制約違反
}
2010/01/22
appengine ja night #4 - @ashigeru
26
ユニーク制約 (5)
var key = KEY:User([email protected])
atomic(tx) {
var user = get(tx, key)
if (user != null) { 存在しなければ
throw new NotUniqueException()
ユニークなので追加
}
user = new User(key, ...)
put(tx, user)
}
2010/01/22
appengine ja night #4 - @ashigeru
27
ユニーク制約 (6)
var key = KEY:User([email protected])
atomic(tx) {
var user = get(tx, key)
if (user != null) {
throw new NotUniqueException()
}
user = new User(key, ...)
put(tx, user)
getからputまでを独占
}
2010/01/22
appengine ja night #4 - @ashigeru
28
ここまでのまとめ (2)
read-modify-write
最初に読んでから書き戻すまで独占
トランザクションの合成
同一EGに対する操作をまとめる
ユニーク制約
入れ物を独占してからエンティティを作成
すでにあったらユニークじゃないので失敗
2010/01/22
appengine ja night #4 - @ashigeru
29
パターン: 冪(べき)等な処理
1回分しか効果を出さない処理
2回以上成功しても、1回分しか反映しない
例:
フォームの多重送信を防止
お一人様一点限り。
注意点
英語でidempotentだけど覚えにくい
2010/01/22
appengine ja night #4 - @ashigeru
30
冪等な処理 (1)
考え方
「処理がユニークに成功する」ということ
まだ成功していなかったら成功させる
一度成功していたら何もしない
成功
成功
成功
結果
2010/01/22
appengine ja night #4 - @ashigeru
31
冪等な処理 (2)
var key = KEY:Counter(C)/Flag(unique)
atomic(tx) {
var flag = get(tx, key)
if (flag != null) {
return
}
put(tx, new Flag(key))
var counter = get(tx, KEY:Counter(C))
counter.value++
put(tx, counter)
}
2010/01/22
appengine ja night #4 - @ashigeru
32
冪等な処理 (3)
var key = KEY:Counter(C)/Flag(unique)
atomic(tx) {
var flag = get(tx, key)
「ユニークなキー」を表す
if (flag
!= null) {
→ db.allocate_ids()
return
} → DatastoreService.allocateIds()
put(tx, new Flag(key))
var counter = get(tx, KEY:Counter(C))
counter.value++
put(tx, counter)
}
2010/01/22
appengine ja night #4 - @ashigeru
33
冪等な処理 (4)
var key = KEY:Counter(C)/Flag(unique)
atomic(tx) {
var flag = get(tx, key)
if (flag != null) {
return
}
put(tx, new Flag(key))
var counter = get(tx, KEY:Counter(C))
counter.value++
ユニーク制約をユニークなキーで。
put(tx, counter)
1回目は確実に成功、
}
キーを使いまわせば2回目は失敗
2010/01/22
appengine ja night #4 - @ashigeru
34
冪等な処理 (5)
var key = KEY:Counter(C)/Flag(unique)
atomic(tx) {
var flag = get(tx, key)
if (flag != null)それ以降の処理は
{
return
一度だけしか行われない
}
put(tx, new Flag(key))
var counter = get(tx, KEY:Counter(C))
counter.value++
put(tx, counter)
}
2010/01/22
appengine ja night #4 - @ashigeru
35
冪等な処理 (6)
var key = KEY:Counter(C)/Flag(unique)
atomic(tx) {
var flag = get(tx, key)
if (flag != null) {
return
}
put(tx, new Flag(key))
var counter = get(tx, KEY:Counter(C))
counter.value++
put(tx, counter)
全体を合成してACIDに
}
2010/01/22
appengine ja night #4 - @ashigeru
36
冪等な処理 (まとめ)
冪等な処理
「1回分しか効果を出さない」パターン
やりかた
「成功」済みかどうかについてユニーク制約
トランザクションを合成
ユニーク制約で成功フラグを立てる
OKなら続きの処理を行う
注意点
ごみ(Flag)が残るが、これを消すのは一手間
2010/01/22
appengine ja night #4 - @ashigeru
37
パターン: Exactly Once
確実にぴったり1回成功する処理
冪等な処理では0回の場合もある
(最大1回)
例:
カウンタの値を正確に更新する(恣意的)
注意点
「確実に失敗する」処理には適用できない
2010/01/22
appengine ja night #4 - @ashigeru
38
Exactly Once (1)
考え方
1度しか反映されない操作を執拗に繰り返す
いつかは成功するはず
間違えて2回以上成功しても効果は1回分
2010/01/22
appengine ja night #4 - @ashigeru
39
Exactly Once (2)
var key = KEY:Counter(C)/Flag(unique)
while (true) {
try {
atomic(tx) {
var flag = get(tx, key)
if (flag != null) {
return
}
put(tx, new Flag(key))
var counter = get(tx, KEY:Counter(C))
counter.value++
put(tx, counter)
}
} catch (ignore) {}
}
2010/01/22
appengine ja night #4 - @ashigeru
40
Exactly Once (3)
var key = KEY:Counter(C)/Flag(unique)
while (true) {
try {
atomic(tx) {
var flag = get(tx, key)
冪等な処理の
if (flag != null) {
パターン
return
}
put(tx, new Flag(key))
var counter = get(tx, KEY:Counter(C))
counter.value++
put(tx, counter)
}
} catch (ignore) {}
}
2010/01/22
appengine ja night #4 - @ashigeru
41
Exactly Once (4)
var key = KEY:Counter(C)/Flag(unique)
while (true) {
try {
冪等な処理を
atomic(tx) {
無限に繰り返す
var flag = get(tx, key)
if (flag != null) {
return
}
put(tx, new Flag(key))
var counter = get(tx, KEY:Counter(C))
counter.value++
put(tx, counter)
30秒ルールがあるので
}
確実とはいえない
} catch (ignore) {}
}
2010/01/22
appengine ja night #4 - @ashigeru
42
Exactly Once (5)
var key = KEY:Counter(C)/Flag(unique)
enqueue(atomic(tx) {
var flag = get(tx, key)
if (flag != null) {
return
}
put(tx, new Flag(key))
var counter = get(tx, KEY:Counter(C))
counter.value++
put(tx, counter)
代わりにTask Queueで
})
成功するまで繰り返し
2010/01/22
appengine ja night #4 - @ashigeru
43
Exactly Once (まとめ)
Exactly Once
「確実にぴったり1回成功する」パターン
ただし、いつ成功するかは不明
やりかた
冪等な処理を無限に繰り返す
一度成功したらあとは無駄なので打ち切る
App EngineのTask Queueを使える
成功するまで無限に繰り返す、という性質
30秒ルールがあるからwhile(true)は不適切
2010/01/22
appengine ja night #4 - @ashigeru
44
パターン: BASE Transaction
複数のEGにまたがるゆるいトランザク
ション
ACIDほど強い制約がない
例:
口座間の送金処理
注意点
途中の状態が外側に見える
ACIDよりアプリケーションが複雑
2010/01/22
appengine ja night #4 - @ashigeru
45
BASE Transaction (1)
送金処理で本当にやりたいことは2つ
Aの口座からX円引く
Bの口座にX円足す
「トランザクションの合成」は困難
Aの口座とBの口座を同じEGに配置?
Aから送金されうるすべての口座を同じEGに?
トランザクションを分断すると危険
失敗例:「Aの口座からX円引いたけどBに届かない」
補償トランザクションすら失敗する可能性
2010/01/22
appengine ja night #4 - @ashigeru
46
BASE Transaction (2)
単純に考えてみる
まずAの口座から5000円引く
そのあと「一度だけ」Bの口座に5000円足す
atomic (tx1) {
Exactly Once
var a = get(tx1, KEY:Account(A))
a.amount -= 5000
put(tx1, a)
}
atomic (tx2) {
var b = get(tx2, KEY:Account(B))
b.amount += 5000
put(tx2, b)
}
2010/01/22
appengine ja night #4 - @ashigeru
47
BASE Transaction (3)
var key = KEY:Account(B)/Flag(unique)
atomic (tx1) {
var a = get(tx1, KEY:Account(A))
a.amount -= 5000
put(tx1, a)
enqueue(tx1, atomic(tx2) {
var flag = get(tx2, key)
if (flag != null) {
return
}
put(tx2, new Flag(key))
var b = get(tx2, KEY:Account(B))
b.amount += 5000
put(tx2, b)
})
}
2010/01/22
appengine ja night #4 - @ashigeru
48
BASE Transaction (4)
var key = KEY:Account(B)/Flag(unique)
atomic (tx1) {
var a = get(tx1, KEY:Account(A))
a.amount -= 5000
put(tx1, a)
enqueue(tx1, atomic(tx2) {
Read-modify-write
var flag = get(tx2, key)
if (flag != null) {
(A -= 5000)
return
}
put(tx2, new Flag(key))
var b = get(tx2, KEY:Account(B))
b.amount += 5000
put(tx2, b)
})
}
2010/01/22
appengine ja night #4 - @ashigeru
49
BASE Transaction (5)
var key = KEY:Account(B)/Flag(unique)
atomic (tx1) {
var a = get(tx1, KEY:Account(A))
a.amount -= 5000
put(tx1, a)
enqueue(tx1, atomic(tx2) {
Read-modify-write
var flag = get(tx2, key)
if (flag != null) {
(B += 5000)
return
}
put(tx2, new Flag(key))
var b = get(tx2, KEY:Account(B))
b.amount += 5000
put(tx2, b)
})
}
2010/01/22
appengine ja night #4 - @ashigeru
50
BASE Transaction (6)
var key = KEY:Account(B)/Flag(unique)
atomic (tx1) {
var a = get(tx1, KEY:Account(A))
a.amount -= 5000
put(tx1, a)
enqueue(tx1, atomic(tx2) {
var flag = get(tx2, key)
if (flag != null) {
return
}
put(tx2, new Flag(key))
var b = get(tx2, KEY:Account(B))
b.amount += 5000
put(tx2, b)
})
}
2010/01/22
appengine ja night #4 - @ashigeru
Exactly Once
(B += 5000)
51
BASE Transaction (7)
var key = KEY:Account(B)/Flag(unique)
atomic (tx1) {
var a = get(tx1, KEY:Account(A))
a.amount -= 5000
put(tx1, a)
enqueue(tx1, atomic(tx2) {
全体を合成
var flag = get(tx2, key)
if (flag != null) {
(A -= 5000, B += 5000)
return
}
put(tx2, new Flag(key))
var b = get(tx2, KEY:Account(B))
b.amount += 5000
put(tx2, b)
})
}
2010/01/22
appengine ja night #4 - @ashigeru
52
BASE Transaction (まとめ)
BASE Transaction
EGをまたいだゆるいトランザクション
いつか確実に完了する、という性質
やりかた
トランザクションを合成
一つ目のEGに対して操作を行う
Exactly Onceで二つ目のEGに対して操作を行う
注意点
操作が行われるまでタイムラグがある
Eventual Consistency: いずれ整合性が取れる
二つ目のEGに対する操作は制約をかけられない
送金先に受け取り拒否されるとすごく困る
2010/01/22
appengine ja night #4 - @ashigeru
53
ここまでのまとめ (3)
パターン: 冪等な処理
操作自体を最大一回だけ(ユニーク)にする
=
ユニーク制約 + トランザクションの合成
パターン: Exactly Once
最大一回だけ成功する処理を無限に繰り返す
=
冪等な処理 + Task Queue
パターン: BASE Transaction
自分を変更後、相手の変更を確実に一度だけ適用
=
read-modify-write + Exactly Once + 合成
2010/01/22
appengine ja night #4 - @ashigeru
54
おわりに
App Engineのトランザクションは「パズ
ル」になりがち
複雑な制約を考慮しつつ、時間内に解く
ルールも定石もあるので、積み重ねが大切
「仮説→検証」のサイクルが必要な段階
みんなで情報共有
パターンやアンチパターンを持ち寄ろう
2010/01/22
appengine ja night #4 - @ashigeru
55
参考文献
Programming Google App Engine
Oreilly
& Associates Inc, 2009/11
リレーショナルデータベース入門
サイエンス社,
トランザクション処理
日経BP社,
2003/03
2001/10
BASE: An Acid Alternative
http://queue.acm.org/detail.cfm?id=1394128
2010/01/22
appengine ja night #4 - @ashigeru
56
Question + Discussion
実はあと数枚残ってる
2010/01/22
appengine ja night #4 - @ashigeru
57
Transaction Puzzlers
Advanced Topics
appengine ja night #4
@ashigeru
2種類の並行性制御
悲観的並行性制御
リソースを独占できな
リソースを独占し、
他人を待たせる
特徴
楽観的並行性制御
かったら、やり直し
特徴
他人に迷惑をかける
他人に迷惑をかけない
成功確率が高い
失敗し続ける場合も
常にオーバーヘッド
成功すれば速い
誰に迷惑が掛かるか、という点で異なる
2010/01/22
appengine ja night #4 - @ashigeru
59
その他のパターン
Sharding
エンティティを複数のEGに分散
Long Running Transaction
リクエストをまたぐ長いトランザクション
Software Global Transaction
ソフトウェアで2PCプロトコルを実装
Software MVCC
ソフトウェアでMVCCを実装
2010/01/22
appengine ja night #4 - @ashigeru
60
トランザクション設計のポイント
データの局所性を生かす
更新頻度を意識する
本当の制約を見極める
2010/01/22
appengine ja night #4 - @ashigeru
61
ポイント: データの局所性を生かす
ユーザ情報はユーザごとに独立している
同一ユーザは同時にリクエストしないことが多い
多くのユーザを同時に捌いても並行稼動
「複数のユーザが共有するリソース」に注意
カウンタなどは典型例
チケット予約なども該当する
ユースケース分析は重要
単一ユースケースの並列性から考える
いざとなれば非正規化+Eventual
2010/01/22
Consistency
appengine ja night #4 - @ashigeru
62
ポイント: 更新頻度を意識する
更新頻度の観点でエンティティを分類
稀に更新/常に更新のプロパティを共存させない
更新しないプロパティはどこに混ぜてもいい
マスタ/トランザクションデータでは不十分
トランザクションデータも参照系と更新系に
参照系はトランザクション処理しなくてもよい場合
がある
もちろん、更新に局所性があれば問題ない
ローカルトランザクションを衝突させない設計
2010/01/22
appengine ja night #4 - @ashigeru
63
ポイント: 本当の制約を見極める
Strong Consistencyはオーバースペック
どの程度の不整合がどの順で許されるか考える
自動販売機の支払い/商品受け取り
(数秒)
コンビニの支払い/商品受け取り (数十秒)
ファーストフードの支払い/食事 (数分)
レストランの支払い/食事 (▲数十分)
同時にアプリケーション作成コストも考える
不整合の許容は制御することが増えすぎる
提供先のビジネス特性を考慮し、合意を得る必要性
あらゆる観点で「必要十分」に
2010/01/22
appengine ja night #4 - @ashigeru
64
© Copyright 2026 ExpyDoc