Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

データベースにおける排他制御の復習 ● 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 で悲観的ロック対象行を明示的にコントロール することができる

Slide 6

Slide 6 text

データベースにおける排他制御の復習 ● 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 で悲観的ロック対象行を明示的にコントロール することができる 今回はこの文脈のお話

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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;

Slide 14

Slide 14 text

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;

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

(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 してる人がいるから 他の人来たら失敗して もらうゾウ

Slide 29

Slide 29 text

考慮すべきケース ● 「無かったら INSERT」をやりたいとき ○ 今回紹介したもの ● そもそも排他制御の対象がテーブルのレコードではないとき ○ バッチ処理の重複実行防止とか

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

ここからは記事のスクショを使います

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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人しかできないように合意形成

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

B: 古典的なファイルロック

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

C: Redis のアトミック操作

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

C: Redis のアトミック操作

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

D: データベース組み込みのアドバイザリーロック関数

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

Postgres 組み込みのアドバイザリーロック関数 【注意】トランザクションのネスト対応をフレームワークや ORM がやっていると厳しい ● 最上位のトランザクション以外は SAVEPOINT が実態なので,自動開放のタイミングが噛み合わない ● Postgres はトランザクション中にエラーが発生すると ROLLBACK しかできなくなる ↓ 手動開放するほうの関数を選びつつ,ロールバック先の SAVEPOINT をこまめに取るしかない ↓ ライブラリに任せるとラク

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

ベストプラクティス

Slide 63

Slide 63 text

おわり