twitter rss
行レベルロックによるトランザクションの重複回避
Jun 19, 2017
One minute read

トランザクション処理の実装に汎用的に使用できそうなので、テーブルの行レベルロックによってトランザクション処理の重複実行を回避する方法を備忘録としてまとめる。 重複実行のシーンとしては、フロントがバグっていて間違えてリクエストを2回送ってしまっていたり、ユーザーが実行ボタンを連打したりが挙げられる。

行レベルロックについて

  • 行レベルロックとは、特定の行に対して獲得されるロックであり、トランザクションがコミットまたはロールバックされるまで保持される。
  • 実際に行を変更せずに行に対して行レベルロックを獲得するには、該当する行をSELECT FOR UPDATEで選択する。
  • いったん行レベルロックが獲得されると、他のトランザクションはそのロックが解除されるまで(コミットまたはロールバックされるまで)待機する。

行レベルロックを実行してみる(PostgresSQL)

// トランザクション開始
begin work;

// 行レベルロック獲得
select * from users where id = 1 FOR UPDATE;

// ロックが効いているかはこのSQLで確認できる
SELECT l.pid, db.datname, c.relname, l.locktype, l.mode
FROM pg_locks l
        LEFT JOIN pg_class c ON l.relation=c.relfilenode
        LEFT JOIN pg_database db ON l.database = db.oid
ORDER BY l.pid;

// コミット
commit work;

Golangで行レベルロックを体験する

Sample Program(ORMにはgorpを使用)

以下のHandlerはユーザーに対してトランザクションを実行するまでの一連の処理を想定、 このHandlerを連続で実行すると、前のトランザクションが閉じるまで、2回目以降のトランザクションはここで待機の部分で待たされる。 1回目のトランザクション時に実行フラグを更新しておけば、2回目以降のトランザクション実行は防げる。

func PostTransactionHandler(w http.ResponseWriter, r *http.Request) {
  // Postgresの接続情報を取得
  dbmap := helpers.GetPostgres()

  // トランザクション開始
  tx, err := dbmap.Begin()
  if err != nil {
	   return
   }

  defer func() {
    // 終了時にトランザクションを閉じる
    if err != nil {
      tx.Rollback()
      return
    }
    tx.Commit()
  }

  // ... ユーザーIDを取得する処理は省略


  // ----- 別のトランザクションは行レベルロックが解除されるまでここで待機 -----
  user, err := GetUserRowLock(tx, userID) // ここで行レベルロックがかかる
  if err != nil {
    return
  }
  // 動作確認用
  time.Sleep(5 * time.Second)

  // ... トランザクション実行フラグ(未実行 or 実行済)をチェック(省略)

  // ... ユーザーに対して何かしらのトランザクションを実行(省略)

  // ... トランザクション実行フラグを更新(省略) 未実行 -> 実行済

}

// Model
type User struct {
  ID int `json:"id" db:"id"`
  Name string `json:"name" db:"name"`
}

func GetUserRowLock(tx *gorp.Transaction, userID int) (*User, error) {
  u := new(User)
  err := tx.SelectOne(&c, "SELECT * FROM users WHERE id = $1 FOR UPDATE", userID)
  if err != nil {
    return nil, err
  }
  return u, nil
}

Back to posts