$30 off During Our Annual Pro Sale. View details »

排他制御のためだけに渋々 Redis 使ってませんか?

mpyw
November 11, 2022

排他制御のためだけに渋々 Redis 使ってませんか?

MySQL/Postgres におけるトランザクション分離レベル - Speaker Deck
https://speakerdeck.com/mpyw/postgres-niokerutoranzakusiyonfen-li-reberu

上記からの続編です。

排他制御のためだけに Redis 渋々使ってませんか?データベース単独でアドバイザリーロックできるよ!
https://zenn.dev/mpyw/articles/rdb-advisory-locks

上記の記事を解説する補佐的なスライドとして,株式会社ゆめみの社内勉強会にて発表しました。

mpyw

November 11, 2022
Tweet

More Decks by mpyw

Other Decks in Programming

Transcript

  1. 排他制御のためだけに 渋々 Redis 使ってませんか? 2022/11/11 Presented by @mpyw

  2. 目次 1. データベースにおける排他制御の復習 2. ロックするものが無いときにどうするか? 3. アドバイザリーロック手法の比較 4. ベストプラクティス

  3. 目次 1. データベースにおける排他制御の復習 2. ロックするものが無いときにどうするか? 3. アドバイザリーロック手法の比較 4. ベストプラクティス

  4. 以前の発表内容 MySQL/Postgres におけるトランザクション分離レベル - Speaker Deck

  5. データベースにおける排他制御の復習 • Strict 2-Phase Locking (S2PL) から始まった ◦ ロックを徐々に獲得していき,トランザクションのコミットで一気に開放する •

    トランザクション分離レベルの変遷と Multi-Version Concurrency Control (MVCC) の登場 ◦ READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE ◦ これらだけで説明しきれないアノマリーと新しい分離レベルのもろもろが登場 ◦ MVCC では読み取り専用のスナップショットとデータ本体を分離し,性能と部分的一貫性を両立 • MySQL/Postgres での REPEATABLE READ 以上の実装 ◦ MySQL は一貫して「予めロックしておく」悲観的制御だけでなんとかする ◦ Postgres は「競合したら失敗させる」楽観的制御が入ってくる 一方で,両者とも (REPEATABLE READ 以上にせずとも) SELECT … FOR UPDATE の Locking Read で悲観的ロック対象行を明示的にコントロール することができる
  6. データベースにおける排他制御の復習 • Strict 2-Phase Locking (S2PL) から始まった ◦ ロックを徐々に獲得していき,トランザクションのコミットで一気に開放する •

    トランザクション分離レベルの変遷と Multi-Version Concurrency Control (MVCC) の登場 ◦ READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE ◦ これらだけで説明しきれないアノマリーと新しい分離レベルのもろもろが登場 ◦ MVCC では読み取り専用のスナップショットとデータ本体を分離し,性能と部分的一貫性を両立 • MySQL/Postgres での REPEATABLE READ 以上の実装 ◦ MySQL は一貫して「予めロックしておく」悲観的制御だけでなんとかする ◦ Postgres は「競合したら失敗させる」楽観的制御が入ってくる 一方で,両者とも (REPEATABLE READ 以上にせずとも) SELECT … FOR UPDATE の Locking Read で悲観的ロック対象行を明示的にコントロール することができる 今回はこの文脈のお話
  7. 目次 1. データベースにおける排他制御の復習 2. ロックするものが無いときにどうするか? 3. アドバイザリーロック手法の比較 4. ベストプラクティス

  8. READ COMMITTED + Locking Read で成功 id value 1 AAA

    2 BBB 3 CCC id=2 のレコードの 大文字小文字を反転したい id=2 のレコードの 大文字小文字を反転したい
  9. READ COMMITTED + Locking Read で成功 id value 1 AAA

    2 BBB 3 CCC BEGIN; SELECT * FROM data WHERE id = 2 FOR UPDATE;
  10. READ COMMITTED + Locking Read で成功 id value 1 AAA

    2 BBB 3 CCC BEGIN; SELECT * FROM data WHERE id = 2 FOR UPDATE;
  11. READ COMMITTED + Locking Read で成功 id value 1 AAA

    2 BBB 3 CCC BEGIN; SELECT * FROM data WHERE id = 2 FOR UPDATE;
  12. READ COMMITTED + Locking Read で成功 id value 1 AAA

    2 BBB 3 CCC BEGIN; SELECT * FROM data WHERE id = 2 FOR UPDATE;
  13. READ COMMITTED + Locking Read で成功 id value 1 AAA

    2 bbb 3 CCC UPDATE data SET value = ‘bbb’ WHERE id = 2; COMMIT; BEGIN; SELECT * FROM data WHERE id = 2 FOR UPDATE;
  14. READ COMMITTED + Locking Read で成功 id value 1 AAA

    2 bbb 3 CCC UPDATE data SET value = ‘bbb’ WHERE id = 2; COMMIT; BEGIN; SELECT * FROM data WHERE id = 2 FOR UPDATE;
  15. READ COMMITTED + Locking Read で成功 id value 1 AAA

    2 bbb 3 CCC BEGIN; SELECT * FROM data WHERE id = 2 FOR UPDATE;
  16. READ COMMITTED + Locking Read で成功 id value 1 AAA

    2 BBB 3 CCC UPDATE data SET value = ‘BBB’ WHERE id = 2; COMMIT;
  17. READ COMMITTED + Locking Read で成功 id value 1 AAA

    2 BBB 3 CCC UPDATE data SET value = YYY WHERE id = 2; COMMIT;
  18. READ COMMITTED + Locking Read で成功 id value 1 AAA

    2 BBB 3 CCC UPDATE data SET value = YYY WHERE id = 2; COMMIT; 2人が更新したらもとに戻る= Lost Update は回避できた
  19. READ COMMITTED + Locking Read で失敗 id value 1 AAA

    2 BBB id=3 のレコードが無ければ value=XXX で作りたい id=3 のレコードが無ければ value=YYY で作りたい
  20. READ COMMITTED + Locking Read で失敗 id value 1 AAA

    2 BBB BEGIN; SELECT * FROM data WHERE id = 3 FOR UPDATE;
  21. READ COMMITTED + Locking Read で失敗 id value 1 AAA

    2 BBB BEGIN; SELECT * FROM data WHERE id = 3 FOR UPDATE; ロックがかからない!
  22. READ COMMITTED + Locking Read で失敗 id value 1 AAA

    2 BBB BEGIN; SELECT * FROM data WHERE id = 3 FOR UPDATE;
  23. READ COMMITTED + Locking Read で失敗 id value 1 AAA

    2 BBB BEGIN; SELECT * FROM data WHERE id = 3 FOR UPDATE; ロックがかからない!
  24. READ COMMITTED + Locking Read で失敗 id value 1 AAA

    2 BBB 3 XXX INSERT INTO data VALUES (3, ‘XXX’); COMMIT;
  25. READ COMMITTED + Locking Read で失敗 id value 1 AAA

    2 BBB 3 XXX INSERT INTO data VALUES (3, ‘XXX’); COMMIT;
  26. READ COMMITTED + Locking Read で失敗 id value 1 AAA

    2 BBB 3 XXX 3 YYY INSERT INTO data VALUES (3, ‘YYY’); COMMIT; • 外部キー制約があればエラー • 外部キー制約がなければ不整合レコードが作られてしまう
  27. (MySQL) REATABLE READ + Locking Read で成功 id value 1

    AAA 2 BBB BEGIN; SELECT * FROM data WHERE id = 3 FOR UPDATE; ギャップロック! 影響範囲広すぎ…
  28. (Postgres) SERIALIZABLE + Locking Read で成功 id value 1 AAA

    2 BBB BEGIN; SELECT * FROM data WHERE id = 3 FOR UPDATE; SIRead ロックによりエラーとして検知できる (外部キー制約に依存しない) しかし SERIALIZABLE はエラー復帰制御が難しい… id=3 で FOR UPDATE してる人がいるから 他の人来たら失敗して もらうゾウ
  29. 考慮すべきケース • 「無かったら INSERT」をやりたいとき ◦ 今回紹介したもの • そもそも排他制御の対象がテーブルのレコードではないとき ◦ バッチ処理の重複実行防止とか

  30. 目次 1. データベースにおける排他制御の復習 2. ロックするものが無いときにどうするか? 3. アドバイザリーロック手法の比較 4. ベストプラクティス

  31. ロックするものが無いなら何をロックするか? • 予め用意しておいた,存在が保証されているものをロックする ◦ (A) 専用テーブルのレコードロック • 存在していなくてもロックできる別の手段を使う ◦ (B)

    古典的なファイルロック ◦ (C) Redis のアトミック操作 ◦ (D) データベース組み込みのアドバイザリーロック関数
  32. ロックするものが無いなら何をロックするか? • 予め用意しておいた,存在が保証されているものをロックする ◦ (A) 専用テーブルのレコードロック • 存在していなくてもロックできる別の手段を使う ◦ (B)

    古典的なファイルロック ◦ (C) Redis のアトミック操作 ◦ (D) データベース組み込みのアドバイザリーロック関数
  33. ここからは記事のスクショを使います

  34. A: 専用テーブルのレコードロック id value 1 AAA 2 BBB id=3 のレコードが無ければ

    value=XXX で作りたい 最初に mutex テーブルの action=data_insert を 確認しよう! id=3 のレコードが無ければ value=XXX で作りたい 最初に mutex テーブルの action=data_insert を 確認しよう! action data_insert … data mutex
  35. A: 専用テーブルのレコードロック id value 1 AAA 2 BBB id=3 のレコードが無ければ

    value=XXX で作りたい 最初に mutex テーブルの action=data_insert を 確認しよう! id=3 のレコードが無ければ value=XXX で作りたい 最初に mutex テーブルの action=data_insert を 確認しよう! action data_insert … data mutex 「data テーブルに INSERT する」を 同時に1人しかできないように合意形成
  36. A: 専用テーブルのレコードロック

  37. A: 専用テーブルのレコードロック 面倒

  38. A: 専用テーブルのレコードロック レコードロック単体では セッションを超えられないが 併用して工夫すると可能に (次ページで説明)

  39. A: 専用テーブルのレコードロック

  40. A: 専用テーブルのレコードロック レコードロック成功時 • 誰のロックか • いつ切れるか というアプリケーションレベル でのロックも組み合わせる

  41. ロックするものが無いなら何をロックするか? • 予め用意しておいた,存在が保証されているものをロックする ◦ (A) 専用テーブルのレコードロック • 存在していなくてもロックできる別の手段を使う ◦ (B)

    古典的なファイルロック ◦ (C) Redis のアトミック操作 ◦ (D) データベース組み込みのアドバイザリーロック関数
  42. B: 古典的なファイルロック

  43. B: 古典的なファイルロック ファイルを消すことは出来ないが, fopen() 時に存在してなくてもよい 追記モード等でオープンすれば 「無いときだけ新規作成」は アトミックに書ける

  44. B: 古典的なファイルロック とはいえ書き捨て PHP スクリプトの同時実行制御とかでは非常に便利 プロセスの多重起動をアドバイザリロックで防止する for PHP - Qiita

  45. B: 古典的なファイルロック とはいえ書き捨て PHP スクリプトの同時実行制御とかでは非常に便利 プロセスの多重起動をアドバイザリロックで防止する for PHP - Qiita

    自分自身 __FILE__ を ロック対象にする
  46. ロックするものが無いなら何をロックするか? • 予め用意しておいた,存在が保証されているものをロックする ◦ (A) 専用テーブルのレコードロック • 存在していなくてもロックできる別の手段を使う ◦ (B)

    古典的なファイルロック ◦ (C) Redis のアトミック操作 ◦ (D) データベース組み込みのアドバイザリーロック関数
  47. C: Redis のアトミック操作

  48. C: Redis のアトミック操作 タイムアウト設定が必須 意外と脆い アトミック命令

  49. C: Redis のアトミック操作

  50. C: Redis のアトミック操作 将来に期待 現段階でも Amazon MemoryDB を 使えば信頼性は得られる

  51. ロックするものが無いなら何をロックするか? • 予め用意しておいた,存在が保証されているものをロックする ◦ (A) 専用テーブルのレコードロック • 存在していなくてもロックできる別の手段を使う ◦ (B)

    古典的なファイルロック ◦ (C) Redis のアトミック操作 ◦ (D) データベース組み込みのアドバイザリーロック関数
  52. D: データベース組み込みのアドバイザリーロック関数

  53. D: データベース組み込みのアドバイザリーロック関数 セッション超えが不要なケースでは 極めて優秀 タイムアウト設定も不要

  54. Postgres 組み込みのアドバイザリーロック関数

  55. Postgres 組み込みのアドバイザリーロック関数 トランザクションに 任せて自動開放 手動開放が必要 基本はこれでOK

  56. Postgres 組み込みのアドバイザリーロック関数 【注意】トランザクションのネスト対応をフレームワークや ORM がやっていると厳しい • 最上位のトランザクション以外は SAVEPOINT が実態なので,自動開放のタイミングが噛み合わない •

    Postgres はトランザクション中にエラーが発生すると ROLLBACK しかできなくなる ↓ 手動開放するほうの関数を選びつつ,ロールバック先の SAVEPOINT をこまめに取るしかない ↓ ライブラリに任せるとラク
  57. Postgres 組み込みのアドバイザリーロック関数

  58. MySQL 組み込みのアドバイザリーロック関数

  59. MySQL 組み込みのアドバイザリーロック関数 ロック獲得までのタイムアウト (ロック持続時間のタイムアウトではない)

  60. 応用: セッション超えしたいときの対処法 組み合わせれば 一番よい選択肢になり得る (コードは記事参照)

  61. 目次 1. データベースにおける排他制御の復習 2. ロックするものが無いときにどうするか? 3. アドバイザリーロック手法の比較 4. ベストプラクティス

  62. ベストプラクティス

  63. おわり