Transaction Puzzlers

appengine ja night #4
あらかわ (@ashigeru)
今日の内容
  トランザクション処理の考え方
  トランザクション処理のパターン




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)                                EG: Foo(A)
     Foo(A)/Hoge(B)
     Foo(B)                                EG: Foo(B)
     Bar(A)/Foo(A)
                                           EG: Bar(A)
     Bar(A)/Foo(B)/Hoge(D)

2010/01/22      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                            101



2010/01/22         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                  16

             30         31


2010/01/22        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   @hoge              @hoge



2010/01/22       appengine ja night #4 - @ashigeru   23
ユニーク制約 (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
ユニーク制約 (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
ユニーク制約 (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
ユニーク制約 (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
ユニーク制約 (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
ここまでのまとめ (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) {
  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
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) {
    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
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
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
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
  リレーショナルデータベース入門
     サイエンス社, 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

appengine ja night #4 Transaction Puzzlers

  • 1.
    Transaction Puzzlers appengine janight #4 あらかわ (@ashigeru)
  • 2.
    今日の内容 トランザクション処理の考え方 トランザクション処理のパターン 2010/01/22 appengine ja night #4 - @ashigeru 4
  • 3.
    トランザクション処理の考え方 リソースを一時的に独占できる技術 同時に変更して不整合が起こる、などを回避 今回は悲観的/楽観的をあまり気にしない App Engineは楽観的並⾏性制御 いずれも一時的にリソースを独占できる 設計/実装時には考慮する必要がある 2010/01/22 appengine ja night #4 - @ashigeru 5
  • 4.
    App Engineのトランザクション トランザクションはEntity Group (EG)単位 同一EG内のエンティティに対する操作はACID 複数EGにまたがる操作は対応していない 2010/01/22 appengine ja night #4 - @ashigeru 6
  • 5.
    Entity Groupの構成 同じルートキーを持つエンティティ群 データストア上で近くに配置される 例 Foo(A) EG: Foo(A) Foo(A)/Hoge(B) Foo(B) EG: Foo(B) Bar(A)/Foo(A) EG: Bar(A) Bar(A)/Foo(B)/Hoge(D) 2010/01/22 appengine ja night #4 - @ashigeru 7
  • 6.
    Entity Groupの特徴 ポイント トランザクションの範囲はエンティティ作成 時に決まり、変更できない EGを⼤きくするとトランザクションで独占す るエンティティが多くなる EGの設計が非常に重要に 間違えると並列性が極端に低下する うまくやればスケールアウトする 2010/01/22 appengine ja night #4 - @ashigeru 8
  • 7.
    ここまでのまとめ (1) App EngineのトランザクションはEG単位 EG内ではACIDトランザクション EGをまたぐトランザクションは未サポート EGの設計によっては並列性が落ちる EGを⼤きくすると独占範囲が広がる EGを分断すると整合性を保つのが困難 2010/01/22 appengine ja night #4 - @ashigeru 9
  • 8.
    トランザクション処理のパターン AppEngineのトランザクションはやや特殊 パターンで対応したほうがよさそう 本日紹介するもの Read-modify-write トランザクションの合成 ユニーク制約 冪(べき)等な処理 Exactly Once BASE Transaction 2010/01/22 appengine ja night #4 - @ashigeru 10
  • 9.
    注意点 プログラムの説明に擬似コードを多⽤ 言語は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
  • 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) varkey = 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) varkey = 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) varkey = 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) varkey = 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) varkey = 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) varkey = 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) varkey = 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) varkey = 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) varkey = 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.
    おわりに AppEngineのトランザクションは「パズ ル」になりがち 複雑な制約を考慮しつつ、時間内に解く ルールも定⽯もあるので、積み重ねが⼤切 「仮説→検証」のサイクルが必要な段階 みんなで情報共有 パターンやアンチパターンを持ち寄ろう 2010/01/22 appengine ja night #4 - @ashigeru 55
  • 54.
    参考文献 ProgrammingGoogle 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