10. パターン: read-modify-write
エンティティのプロパティを変更する
例:
カウンタの増加
ショッピングカートに商品を追加
現在の値をもとに次の値が決まる
読む、変更、書き戻す、の3ステップが必要
途中で割り込まれると不整合が起こる
2010/01/22 appengine ja night #4 - @ashigeru 12
11. read-modify-write (1)
考え方
読んでから書き戻すまでエンティティを独占
100 + 1
100 101
2010/01/22 appengine ja night #4 - @ashigeru 13
12. 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
13. 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
14. 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
15. パターン: トランザクションの合成
同じEGに対する複数のトランザクション
処理を合成
例:
2つのカウンタを同時に変更 (恣意的)
非正規化した2つの情報を同時に更新
注意点
分断したトランザクションでは、途中で失敗
した際に修復が⼤変
2010/01/22 appengine ja night #4 - @ashigeru 17
16. トランザクションの合成 (1)
考え方
同じEGのトランザクションが2つあったら、
一度に処理してしまう
15 16
30 31
2010/01/22 appengine ja night #4 - @ashigeru 18
17. トランザクションの合成 (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
18. トランザクションの合成 (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
19. トランザクションの合成 (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
20. パターン: ユニーク制約
重複するエンティティの登録を防止する
例:
同じIDを持つユーザの登録を防ぐ
ダブルブッキングを防ぐ
注意点
データストアは制約機能を組み込んでいない
クエリはトランザクションに参加できない
2010/01/22 appengine ja night #4 - @ashigeru 22
21. ユニーク制約 (1)
考え方
エンティティの入れ物ごと独占
入れ物が空なら追加するエンティティは一意
@hoge @hoge @hoge
2010/01/22 appengine ja night #4 - @ashigeru 23
22. ユニーク制約 (2)
var key = KEY:User(hoge@example.com)
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
23. ユニーク制約 (3)
var key = KEY:User(hoge@example.com)
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
24. ユニーク制約 (4)
var key = KEY:User(hoge@example.com)
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
25. ユニーク制約 (5)
var key = KEY:User(hoge@example.com)
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
26. ユニーク制約 (6)
var key = KEY:User(hoge@example.com)
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
27. ここまでのまとめ (2)
read-modify-write
最初に読んでから書き戻すまで独占
トランザクションの合成
同一EGに対する操作をまとめる
ユニーク制約
入れ物を独占してからエンティティを作成
すでにあったらユニークじゃないので失敗
2010/01/22 appengine ja night #4 - @ashigeru 29
28. パターン: 冪(べき)等な処理
1回分しか効果を出さない処理
2回以上成功しても、1回分しか反映しない
例:
フォームの多重送信を防止
お一人様一点限り。
注意点
英語でidempotentだけど覚えにくい
2010/01/22 appengine ja night #4 - @ashigeru 30
29. 冪等な処理 (1)
考え方
「処理がユニークに成功する」ということ
まだ成功していなかったら成功させる
一度成功していたら何もしない
成功 成功 成功
結果
2010/01/22 appengine ja night #4 - @ashigeru 31
30. 冪等な処理 (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
31. 冪等な処理 (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
32. 冪等な処理 (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
33. 冪等な処理 (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
34. 冪等な処理 (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
35. 冪等な処理 (まとめ)
冪等な処理
「1回分しか効果を出さない」パターン
やりかた
「成功」済みかどうかについてユニーク制約
トランザクションを合成
そのキーでユニーク制約を確認
OKなら続きの処理を⾏う
注意点
ごみ(Flag)が残るが、これを消すのは一⼿間
2010/01/22 appengine ja night #4 - @ashigeru 37
36. パターン: Exactly Once
確実にぴったり1回成功する処理
冪等な処理では0回の場合もある (最⼤1回)
例:
カウンタの値を正確に更新する(恣意的)
注意点
「確実に失敗する」処理には適⽤できない
2010/01/22 appengine ja night #4 - @ashigeru 38
37. Exactly Once (1)
考え方
1度しか反映されない操作を執拗に繰り返す
いつかは成功するはず
間違えて2回以上成功しても効果は1回分
2010/01/22 appengine ja night #4 - @ashigeru 39
38. 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
39. 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
40. 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
41. 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
42. Exactly Once (まとめ)
Exactly Once
「確実にぴったり1回成功する」パターン
ただし、いつ成功するかは不明
やりかた
冪等な処理を無限に繰り返す
一度成功したらあとは無駄なので打ち切る
App EngineのTask Queueを使える
成功するまで無限に繰り返す、という性質
30秒ルールがあるからwhile(true)は無理
2010/01/22 appengine ja night #4 - @ashigeru 44
43. パターン: BASE Transaction
複数のEGにまたがるゆるいトランザク
ション
ACIDほど強い制約がない
例:
口座間の送⾦処理
注意点
途中の状態が外側に⾒える
ACIDよりアプリケーションが複雑
2010/01/22 appengine ja night #4 - @ashigeru 45
44. 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
45. BASE Transaction (2)
単純に考えてみる
まずAの口座から5000円引く
そのあと「一度だけ」Bの口座に5000円足す
atomic (tx1) {
var a = get(tx1, KEY:Account(A)) Exactly Once
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
46. 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
47. 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) {
var flag = get(tx2, key) Read-modify-write
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
48. 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) {
var flag = get(tx2, key) Read-modify-write
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
49. 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) Exactly Once
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 51
50. 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
51. BASE Transaction (まとめ)
BASE Transaction
EGをまたいだゆるいトランザクション
いつか確実に完了する、という性質
やりかた
トランザクションを合成
一つ目のEGに対して操作を⾏う
Exactly Onceで二つ目のEGに対して操作を⾏う
注意点
操作が⾏われるまでタイムラグがある
Eventual Consistency: いずれ整合性が取れる
二つ目のEGに対する操作は制約をかけられない
送⾦先に受け取り拒否されるとすごく困る
2010/01/22 appengine ja night #4 - @ashigeru 53
52. ここまでのまとめ (3)
パターン: 冪等な処理
操作自体を最⼤一回だけ(ユニーク)にする
= ユニーク制約 + トランザクションの合成
パターン: Exactly Once
最⼤一回だけ成功する処理を無限に繰り返す
= 冪等な処理 + Task Queue
パターン: BASE Transaction
自分を変更後、相⼿の変更を確実に一度だけ適⽤
= read-modify-write + 合成 + Exactly Once
2010/01/22 appengine ja night #4 - @ashigeru 54
53. おわりに
App Engineのトランザクションは「パズ
ル」になりがち
複雑な制約を考慮しつつ、時間内に解く
ルールも定⽯もあるので、積み重ねが⼤切
「仮説→検証」のサイクルが必要な段階
みんなで情報共有
パターンやアンチパターンを持ち寄ろう
2010/01/22 appengine ja night #4 - @ashigeru 55
54. 参考文献
Programming Google App Engine
Oreilly & Associates Inc, 2009/11
リレーショナルデータベース入門
サイエンス社, 2003/03
トランザクション処理
日経BP社, 2001/10
BASE: An Acid Alternative
http://queue.acm.org/detail.cfm?id=1394128
2010/01/22 appengine ja night #4 - @ashigeru 56