appengine ja night #4 transaction puzzlers
Post on 12-Jul-2015
3.021 Views
Preview:
TRANSCRIPT
Transaction Puzzlers
appengine ja night #4
あらかわ (@ashigeru)
appengine ja night #4 - @ashigeru 42010/01/22
今日の内容� トランザクション処理の考え方� トランザクション処理のパターン
appengine ja night #4 - @ashigeru 52010/01/22
トランザクション処理の考え方� リソースを一時的に独占できる技術
�同時に変更して不整合が起こる、などを回避
� 今回は悲観的/楽観的をあまり気にしない�App Engineは楽観的並⾏性制御�いずれも一時的にリソースを独占できる�設計/実装時には考慮する必要がある
appengine ja night #4 - @ashigeru 62010/01/22
App Engineのトランザクション� トランザクションはEntity Group (EG)単位
�同一EG内のエンティティに対する操作はACID
�複数EGにまたがる操作は対応していない
appengine ja night #4 - @ashigeru 72010/01/22
Entity Groupの構成� 同じルートキーを持つエンティティ群
�データストア上で近くに配置される� 例
�Foo(A)
�Foo(A)/Hoge(B)
�Foo(B)
�Bar(A)/Foo(A)
�Bar(A)/Foo(B)/Hoge(D)
EG: Foo(A)
EG: Foo(B)
EG: Bar(A)
appengine ja night #4 - @ashigeru 82010/01/22
Entity Groupの特徴� ポイント
�トランザクションの範囲はエンティティ作成時に決まり、変更できない
�EGを⼤きくするとトランザクションで独占するエンティティが多くなる
� EGの設計が非常に重要に�間違えると並列性が極端に低下する�うまくやればスケールアウトする
appengine ja night #4 - @ashigeru 92010/01/22
ここまでのまとめ (1)
� App EngineのトランザクションはEG単位�EG内ではACIDトランザクション�EGをまたぐトランザクションは未サポート
� EGの設計によっては並列性が落ちる�EGを⼤きくすると独占範囲が広がる�EGを分断すると整合性を保つのが困難
appengine ja night #4 - @ashigeru 102010/01/22
トランザクション処理のパターン� App Engineのトランザクションはやや特殊
�パターンで対応したほうがよさそう� 本日紹介するもの
� Read-modify-write
�トランザクションの合成�ユニーク制約�冪(べき)等な処理� Exactly Once
� BASE Transaction
appengine ja night #4 - @ashigeru 112010/01/22
注意点� プログラムの説明に擬似コードを多⽤
�言語は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)
appengine ja night #4 - @ashigeru 122010/01/22
パターン: read-modify-write
� エンティティのプロパティを変更する� 例:
�カウンタの増加�ショッピングカートに商品を追加
� 現在の値をもとに次の値が決まる�読む、変更、書き戻す、の3ステップが必要�途中で割り込まれると不整合が起こる
appengine ja night #4 - @ashigeru 132010/01/22
read-modify-write (1)
� 考え方�読んでから書き戻すまでエンティティを独占
100 101
100 + 1
appengine ja night #4 - @ashigeru 142010/01/22
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()
}
appengine ja night #4 - @ashigeru 152010/01/22
read-modify-write (3)
var tx = beginTransaction()try {var counter = get(tx, KEY:Counter(C))counter.value++put(tx, counter)tx.commit()
}finally {if (tx.isActive()) tx.rollback()
}
読んでから書き戻すまでをACIDに⾏う
appengine ja night #4 - @ashigeru 162010/01/22
DSL: atomic (tx) { … }
� 以後は下記のように省略�トランザクションの開始と終了を簡略化
atomic(tx) {var counter = get(tx, KEY:Counter(C))counter.value++put(tx, counter)
}
appengine ja night #4 - @ashigeru 172010/01/22
パターン: トランザクションの合成� 同じEGに対する複数のトランザクション処理を合成
� 例:
�2つのカウンタを同時に変更 (恣意的)
�非正規化した2つの情報を同時に更新� 注意点
�分断したトランザクションでは、途中で失敗した際に修復が⼤変
appengine ja night #4 - @ashigeru 182010/01/22
トランザクションの合成 (1)
� 考え方�同じEGのトランザクションが2つあったら、一度に処理してしまう
15 16
30 31
appengine ja night #4 - @ashigeru 192010/01/22
トランザクションの合成 (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)
}
appengine ja night #4 - @ashigeru 202010/01/22
トランザクションの合成 (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のエンティティに対する操作
appengine ja night #4 - @ashigeru 212010/01/22
トランザクションの合成 (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に
appengine ja night #4 - @ashigeru 222010/01/22
パターン: ユニーク制約� 重複するエンティティの登録を防止する� 例:
�同じIDを持つユーザの登録を防ぐ�ダブルブッキングを防ぐ
� 注意点�データストアは制約機能を組み込んでいない�クエリはトランザクションに参加できない
appengine ja night #4 - @ashigeru 232010/01/22
ユニーク制約 (1)
� 考え方�エンティティの入れ物ごと独占�入れ物が空なら追加するエンティティは一意
@hoge @hoge @hoge
appengine ja night #4 - @ashigeru 242010/01/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)
}
appengine ja night #4 - @ashigeru 252010/01/22
ユニーク制約 (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)
}
ユニーク制約をキーで表す(メールアドレス)
appengine ja night #4 - @ashigeru 262010/01/22
ユニーク制約 (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)
}そのエンティティがすでにあれば制約違反
appengine ja night #4 - @ashigeru 272010/01/22
ユニーク制約 (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)
}
存在しなければユニークなので追加
appengine ja night #4 - @ashigeru 282010/01/22
ユニーク制約 (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までを独占
appengine ja night #4 - @ashigeru 292010/01/22
ここまでのまとめ (2)
� read-modify-write
�最初に読んでから書き戻すまで独占� トランザクションの合成
�同一EGに対する操作をまとめる� ユニーク制約
�入れ物を独占してからエンティティを作成�すでにあったらユニークじゃないので失敗
appengine ja night #4 - @ashigeru 302010/01/22
パターン: 冪(べき)等な処理� 1回分しか効果を出さない処理
�2回以上成功しても、1回分しか反映しない� 例:
�フォームの多重送信を防止�お一人様一点限り。
� 注意点�英語でidempotentだけど覚えにくい
appengine ja night #4 - @ashigeru 312010/01/22
冪等な処理 (1)
� 考え方�「処理がユニークに成功する」ということ�まだ成功していなかったら成功させる�一度成功していたら何もしない
成功 成功 成功
結果
appengine ja night #4 - @ashigeru 322010/01/22
冪等な処理 (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)
}
appengine ja night #4 - @ashigeru 332010/01/22
冪等な処理 (3)
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)
}
「ユニークなキー」を表す→ db.allocate_ids()
→ DatastoreService.allocateIds()
appengine ja night #4 - @ashigeru 342010/01/22
冪等な処理 (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回目は失敗
appengine ja night #4 - @ashigeru 352010/01/22
冪等な処理 (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)
}
それ以降の処理は一度だけしか⾏われない
appengine ja night #4 - @ashigeru 362010/01/22
冪等な処理 (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に
appengine ja night #4 - @ashigeru 372010/01/22
冪等な処理 (まとめ)
� 冪等な処理�「1回分しか効果を出さない」パターン
� やりかた�「成功」済みかどうかについてユニーク制約�トランザクションを合成
� そのキーでユニーク制約を確認� OKなら続きの処理を⾏う
� 注意点�ごみ(Flag)が残るが、これを消すのは一⼿間
appengine ja night #4 - @ashigeru 382010/01/22
パターン: Exactly Once
� 確実にぴったり1回成功する処理�冪等な処理では0回の場合もある (最⼤1回)
� 例:
�カウンタの値を正確に更新する(恣意的)
� 注意点�「確実に失敗する」処理には適⽤できない
appengine ja night #4 - @ashigeru 392010/01/22
Exactly Once (1)
� 考え方�1度しか反映されない操作を執拗に繰り返す�いつかは成功するはず�間違えて2回以上成功しても効果は1回分
appengine ja night #4 - @ashigeru 402010/01/22
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) {}
}
appengine ja night #4 - @ashigeru 412010/01/22
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) {}
}
冪等な処理のパターン
appengine ja night #4 - @ashigeru 422010/01/22
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)
}} catch (ignore) {}
}
冪等な処理を無限に繰り返す
30秒ルールがあるので確実とはいえない
appengine ja night #4 - @ashigeru 432010/01/22
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で成功するまで繰り返し
appengine ja night #4 - @ashigeru 442010/01/22
Exactly Once (まとめ)
� Exactly Once
�「確実にぴったり1回成功する」パターン�ただし、いつ成功するかは不明
� やりかた�冪等な処理を無限に繰り返す�一度成功したらあとは無駄なので打ち切る
� App EngineのTask Queueを使える�成功するまで無限に繰り返す、という性質� 30秒ルールがあるからwhile(true)は無理
appengine ja night #4 - @ashigeru 452010/01/22
パターン: BASE Transaction
� 複数のEGにまたがるゆるいトランザクション�ACIDほど強い制約がない
� 例:
�口座間の送⾦処理� 注意点
�途中の状態が外側に⾒える�ACIDよりアプリケーションが複雑
appengine ja night #4 - @ashigeru 462010/01/22
BASE Transaction (1)
� 送⾦処理で本当にやりたいことは2つ� Aの口座からX円引く� Bの口座にX円足す
� 「トランザクションの合成」は困難� Aの口座とBの口座を同じEGに配置?� Aから送⾦されうるすべての口座を同じEGに?
� トランザクションを分断すると危険�失敗例:「Aの口座からX円引いたけどBに届かない」�補償トランザクションすら失敗する可能性
appengine ja night #4 - @ashigeru 472010/01/22
BASE Transaction (2)
� 単純に考えてみる�まずAの口座から5000円引く�そのあと「一度だけ」Bの口座に5000円足す
atomic (tx1) {var a = get(tx1, KEY:Account(A))a.amount -= 5000put(tx1, a)
} atomic (tx2) {var b = get(tx2, KEY:Account(B))b.amount += 5000put(tx2, b)
}
Exactly Once
appengine ja night #4 - @ashigeru 482010/01/22
BASE Transaction (3)
var key = KEY:Account(B)/Flag(unique)atomic (tx1) {var a = get(tx1, KEY:Account(A))a.amount -= 5000put(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 += 5000put(tx2, b)
})}
appengine ja night #4 - @ashigeru 492010/01/22
BASE Transaction (4)
var key = KEY:Account(B)/Flag(unique)atomic (tx1) {var a = get(tx1, KEY:Account(A))a.amount -= 5000put(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 += 5000put(tx2, b)
})}
Read-modify-write
(A -= 5000)
appengine ja night #4 - @ashigeru 502010/01/22
BASE Transaction (5)
var key = KEY:Account(B)/Flag(unique)atomic (tx1) {var a = get(tx1, KEY:Account(A))a.amount -= 5000put(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 += 5000put(tx2, b)
})}
Read-modify-write
(B += 5000)
appengine ja night #4 - @ashigeru 512010/01/22
BASE Transaction (6)
var key = KEY:Account(B)/Flag(unique)atomic (tx1) {var a = get(tx1, KEY:Account(A))a.amount -= 5000put(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 += 5000put(tx2, b)
})}
Exactly Once
(B += 5000)
appengine ja night #4 - @ashigeru 522010/01/22
BASE Transaction (7)
var key = KEY:Account(B)/Flag(unique)atomic (tx1) {var a = get(tx1, KEY:Account(A))a.amount -= 5000put(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 += 5000put(tx2, b)
})}
全体を合成(A -= 5000, B += 5000)
appengine ja night #4 - @ashigeru 532010/01/22
BASE Transaction (まとめ)
� BASE Transaction� EGをまたいだゆるいトランザクション�いつか確実に完了する、という性質
� やりかた�トランザクションを合成
� 一つ目のEGに対して操作を⾏う� Exactly Onceで二つ目のEGに対して操作を⾏う
� 注意点�操作が⾏われるまでタイムラグがある
� Eventual Consistency: いずれ整合性が取れる�二つ目のEGに対する操作は制約をかけられない
� 送⾦先に受け取り拒否されるとすごく困る
appengine ja night #4 - @ashigeru 542010/01/22
ここまでのまとめ (3)
� パターン: 冪等な処理�操作自体を最⼤一回だけ(ユニーク)にする� = ユニーク制約 + トランザクションの合成
� パターン: Exactly Once
�最⼤一回だけ成功する処理を無限に繰り返す� = 冪等な処理 + Task Queue
� パターン: BASE Transaction
�自分を変更後、相⼿の変更を確実に一度だけ適⽤� = read-modify-write + 合成 + Exactly Once
appengine ja night #4 - @ashigeru 552010/01/22
おわりに� App Engineのトランザクションは「パズル」になりがち�複雑な制約を考慮しつつ、時間内に解く�ルールも定⽯もあるので、積み重ねが⼤切
� 「仮説→検証」のサイクルが必要な段階�みんなで情報共有�パターンやアンチパターンを持ち寄ろう
appengine ja night #4 - @ashigeru 562010/01/22
参考文献� 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
top related