Upgrade to Pro — share decks privately, control downloads, hide ads and more …

攻略!Aurora DSQL の OCC(楽観的同時実行制御)

攻略!Aurora DSQL の OCC(楽観的同時実行制御)

JAWS FESTA 2025 in 金沢 2025/10/11【B-5】

Avatar for hmatsu47

hmatsu47 PRO

October 11, 2025
Tweet

More Decks by hmatsu47

Other Decks in Technology

Transcript

  1. 自己紹介 松久裕保(@hmatsu47) • https://qiita.com/hmatsu47 • 現在: ◦ 名古屋で Web インフラのお守り係をしています

    ◦ SRE チームに所属しつつ技術検証の支援をしています ◦ 普段カンファレンス・勉強会では DB の話しかしていません (ほぼ) 2
  2. 本日の内容 • 当セッションにおける用語について • Aurora DSQL とは? • トランザクションが競合するパターン・しないパターン •

    競合対策(競合頻度の低減・競合時リトライなど) • 書き込みスキュー異常(変更データ間の矛盾)回避 • まとめ 3
  3. 1. トランザクション処理 2. 変更(変更トランザクション) 3. ロック 4. PCC と OCC

    5. スナップショット分離(Snapshot Isolation) 当セッションにおける用語について 6
  4. [1] トランザクション処理 7 • 整合性・一貫性を保つために複数の処理をまとめて一つ の不可分な処理単位として扱う仕組み ◦ 例えば A さんの口座から

    B さんの口座へ 10 万円送金する場合 ▪ A さんの口座の残高確認(10 万円未満なら処理中止) ▪ A さんの口座から 10 万円減らす ▪ B さんの口座に 10 万円加算する を一連の不可分な処理として扱う ◦ 途中で障害などが起きたら一連の処理を「なかったこと」に
  5. 【補足】RDBMS のトランザクション処理 8 • 開始からコミットまでを「不可分な処理」として扱う ◦ 何らかの理由で処理をなかったことにする場合はロールバック • オートコミット設定によって動作が変わる ◦

    オートコミット ON の場合、1 つの SQL 文毎にコミット処理が 行われるが、BEGIN を発行するとトランザクション開始 ▪ BEGIN 以降はオートコミットされない ◦ オートコミット OFF の場合、最初の SQL 文が発行された時点か らトランザクション開始
  6. [2] 変更(変更トランザクション) 9 • 当セッションでは以下をまとめて「変更」と表現 ◦ 挿入(INSERT) ◦ 更新(UPDATE) ◦

    削除(DELETE) • 「変更」処理を含むトランザクション →「変更トランザクション」
  7. [3] ロック 10 • 他のトランザクションから値の参照・変更ができないよ うにする機構 ◦ 共有ロック(他からは参照のみ可・変更不可) ◦ 排他ロック(他からは参照も変更も不可)

    • 当セッションでは、いわゆる「楽観ロック」をロックと みなさない →AWS も「DSQL ではロックを使わない」とドキュメントで説明
  8. [4] PCC と OCC 11 • 同時実行制御方式の違い ◦ PCC(悲観的同時実行制御):ロックを使う ▪

    通常の RDBMS で採用 ◦ OCC(楽観的同時実行制御):ロックを使わない ▪ Aurora DSQL や Tsurugi(劔)で採用
  9. [4-1] PCC(悲観的同時実行制御) 12 • 通常の RDBMS で採用されている同時実行制御方式 ◦ ロックを使う ◦

    並行する複数のトランザクションが同じデータ行の変更を同時に 行わないよう、先行トランザクションがコミット/ロールバック するまで後続トランザクションは対象データ行の変更を待機
  10. • 後続トランザクションは先行トランザクションの COMMIT / ROLLBACK を待つ ⚫BEGIN ◎UPDATE A COMMIT⭕

    ⚫BEGIN ◦UPDATE A     →ロック待機 COMMIT⭕ ◎UPDATE A ⚫BEGIN 13 [4-1] PCC(悲観的同時実行制御) ◎UPDATE A ◦UPDATE A     →ロック待機
  11. [4-2] OCC(楽観的同時実行制御) 14 • Aurora DSQL で採用されている同時実行制御方式 ◦ ロックを使わない ◦

    並行する複数のトランザクションが同じデータ行の変更を試みた 場合、最初にコミットしたトランザクションの処理が成功 ▪ 後からコミットしたトランザクションの処理は中断(アボート)
  12. • トランザクションA(TxA)で COMMIT 成功⭕→ TxB で値は表示されない • TxB で COMMIT

    →(新たなトランザクション開始) → TxA で COMMIT した値が表示される [5] スナップショット分離(Snapshot Isolation) ⚫BEGIN ◎INSERT A (1, 100) COMMIT⭕ ⚫BEGIN ◎SELECT A →空 COMMIT⭕ ◎SELECT A →空 ◎SELECT A → (1, 100) ここはCOMMIT前とは 別のトランザクション 17 TxA TxB
  13. ・トランザクション A(テーブル準備) postgres=> CREATE SCHEMA hoge; postgres=> CREATE TABLE hoge.fuga(id

    INT PRIMARY KEY UNIQUE, val INT); ・トランザクション A(開始) postgres=> BEGIN; ・トランザクション B(開始) postgres=> BEGIN; ・トランザクション A(データ挿入&コミット) postgres=*> INSERT INTO hoge.fuga VALUES(1, 100); postgres=*> COMMIT; 【参考】Aurora DSQL での実行例 18
  14. ・トランザクション B(データ参照しても見えない) postgres=*> SELECT * FROM hoge.fuga; id | val

    ----+----- (0 rows) ・トランザクション B・B’(コミット →データ参照すると見える) postgres=*> COMMIT; postgres=> SELECT * FROM hoge.fuga; id | val ----+----- 1 | 100 (1 row) 19 【参考】Aurora DSQL での実行例
  15. DSQL:サーバーレス分散 SQL データベース 22 • PostgreSQL ワイヤープロトコル互換 ◦ psql コマンドが使える

    • シングルリージョン構成とマルチリージョン構成がある ◦ マルチリージョン構成は US 3 リージョン/欧州 3 リージョン/ 東京+大阪+ソウルの組み合わせでサポート ▪ エンドポイントは 2 リージョン、残り 1 つは Witness リージョンで構成 ◦ 次ページの図はシングルリージョン構成の例
  16. • 後続トランザクションほどロック待機時間が積み重なって長くなる ⚫BEGIN ◎UPDATE A COMMIT⭕ ⚫BEGIN ◦UPDATE A     →ロック待機

    COMMIT⭕ ◎UPDATE A ⚫BEGIN 26 再掲:PCC(ロック待機のイメージ) ◎UPDATE A ◦UPDATE A     →ロック待機
  17. おことわり 30 • 一部の例ではトランザクションの中で 1 つの SQL 文しか 発行しないケースを示しています(理解しやすくするため) •

    オートコミット設定 ON のケースを示しています →BEGIN を発行することで明示的にトランザクションを開始
  18. ・トランザクション A(開始〜データ更新) postgres=> BEGIN; postgres=*> UPDATE hoge.fuga SET val =

    110 WHERE id = 1; ・トランザクション B(開始〜データ更新) postgres=> BEGIN; postgres=*> UPDATE hoge.fuga SET val = 120 WHERE id = 1; ・トランザクション A(コミット) postgres=*> COMMIT; ・トランザクション B(コミット失敗) postgres=*> COMMIT; ERROR: change conflicts with another transaction, please retry: (OC000) 34 【参考】Aurora DSQL での実行例 [1]
  19. [2] 同じデータ行を変更しようとした場合 • TxA の COMMIT 成功⭕後に TxC が BEGIN

    し、 TxB の COMMIT 失敗❌後に TxC が COMMIT →成功⭕ ⚫BEGIN ◎UPDATE A COMMIT⭕ ◎UPDATE A ⚫BEGIN COMMIT⭕ ◎UPDATE A COMMIT❌ ⚫BEGIN 36 TxA TxB TxC
  20. ・トランザクション A(開始〜データ更新) postgres=> BEGIN; postgres=*> UPDATE hoge.fuga SET val =

    110 WHERE id = 1; ・トランザクション B(開始〜データ更新) postgres=> BEGIN; postgres=*> UPDATE hoge.fuga SET val = 120 WHERE id = 1; ・トランザクション A(コミット) postgres=*> COMMIT; ・トランザクション C(開始〜データ更新) postgres=> BEGIN; postgres=*> UPDATE hoge.fuga SET val = 130 WHERE id = 1; 37 【参考】Aurora DSQL での実行例 [2]
  21. ・トランザクション B(コミット失敗) postgres=*> COMMIT; ERROR: change conflicts with another transaction,

    please retry: (OC000) ・トランザクション C(コミット・データ参照 →トランザクション Cの値で上書きされている) postgres=*> COMMIT; postgres=> SELECT * FROM hoge.fuga; id | val ----+----- 1 | 130 (1 row) 38 【参考】Aurora DSQL での実行例 [2]
  22. • TxA の COMMIT(成功)前に TxC が BEGIN → TxC は

    COMMIT 時に失敗❌ → BEGIN ~ COMMIT の期間が重なっていれば競合対象 【補足】[3] 同じデータ行を変更しようとした場合 ⚫BEGIN ◎UPDATE A COMMIT⭕ ◎UPDATE A ⚫BEGIN ◎UPDATE A ⚫BEGIN COMMIT❌ 40 COMMIT❌ TxA TxB TxC
  23. ・トランザクション A(開始〜データ更新) postgres=> BEGIN; postgres=*> UPDATE hoge.fuga SET val =

    120 WHERE id = 1; ・トランザクション B(開始〜データ更新) postgres=> BEGIN; postgres=*> UPDATE hoge.fuga SET val = 130 WHERE id = 1; ・トランザクション C(開始) postgres=> BEGIN; ・トランザクション A(コミット) postgres=*> COMMIT; 【補足・参考】Aurora DSQL での実行例 [3] 41
  24. ・トランザクション C(データ更新) postgres=*> UPDATE hoge.fuga SET val = 140 WHERE

    id = 1; ・トランザクション B(コミット失敗) postgres=*> COMMIT; ERROR: change conflicts with another transaction, please retry: (OC000) ・トランザクション C(コミット失敗) postgres=*> COMMIT; ERROR: change conflicts with another transaction, please retry: (OC000) 42 【補足・参考】Aurora DSQL での実行例 [3]
  25. [4] 変更対象データの一部が重なる場合 • TxA と TxC は変更対象が重ならない→非競合 TxB が COMMIT

    失敗❌→ TxC の COMMIT は成功⭕ ⚫BEGIN ◎INSERT A COMMIT⭕ ◎INSERT B ⚫BEGIN ⚫BEGIN COMMIT⭕ ◎INSERT A ◎INSERT B 44 COMMIT❌ TxA TxB TxC
  26. ・トランザクション A(テーブル準備) postgres=> DELETE FROM hoge.fuga; postgres=> SELECT * FROM

    hoge.fuga; id | val ----+----- (0 rows) postgres=> CREATE TABLE hoge.piyo(id INT PRIMARY KEY UNIQUE, val INT); ・トランザクション A(開始〜データ挿入 A) postgres=> BEGIN; postgres=*> INSERT INTO hoge.fuga VALUES(1, 100); 45 【参考】Aurora DSQL での実行例 [4]
  27. ・トランザクション B(開始〜データ挿入 A・B) postgres=> BEGIN; postgres=*> INSERT INTO hoge.fuga VALUES(1,

    110); postgres=*> INSERT INTO hoge.piyo VALUES(1, 200); ・トランザクション C(開始〜データ挿入 B) postgres=> BEGIN; postgres=*> INSERT INTO hoge.piyo VALUES(1, 210); ・トランザクション A(コミット) postgres=*> COMMIT; 46 【参考】Aurora DSQL での実行例 [4]
  28. ・トランザクション B(コミット失敗) postgres=*> COMMIT; ERROR: change conflicts with another transaction,

    please retry: (OC000) ・トランザクション C(コミット) postgres=*> COMMIT; 47 【参考】Aurora DSQL での実行例 [4]
  29. ・トランザクション C(データ参照 →トランザクション A・Cで挿入した値が表示される) postgres=*> COMMIT; postgres=> SELECT * FROM

    hoge.fuga; id | val ----+----- 1 | 100 (1 row) postgres=> SELECT * FROM hoge.piyo; id | val ----+----- 1 | 210 (1 row) 48 【参考】Aurora DSQL での実行例 [4]
  30. • オートコミット設定 ON で BEGIN しないケース →INSERT・UPDATE・DELETE を発行すると自動コミット • 結論からいうと「競合する」

    →確実に変更を行うためには競合時のリトライは必須 ところで:オートコミットは競合する? 50
  31. ・シェルA(更新用.sqlファイルを用意) ~ $ vi update.sql UPDATE hoge.fuga SET val =

    val + 1 WHERE id = 1; (1000行繰り返す) ・シェルA(カウントを 0に) postgres=> UPDATE hoge.fuga SET val = 0 WHERE id = 1; postgres=> SELECT * FROM hoge.fuga WHERE id = 1; id | val ----+----- 1 | 0 (1 row) 【参考】Aurora DSQL での実行例 51
  32. ・シェルA(オートコミット ONで更新用.sqlファイルを読み込み) postgres=> \i ./update.sql psql:update.sql:127: ERROR: change conflicts with

    another transaction, please retry: (OC000) (中略) psql:update.sql:998: ERROR: change conflicts with another transaction, please retry: (OC000) ・シェルB(オートコミット ONで更新用.sqlファイルをシェル Aと並行で読み込み) postgres=> \i ./update.sql psql:update.sql:1: ERROR: change conflicts with another transaction, please retry: (OC000) (中略) psql:update.sql:827: ERROR: change conflicts with another transaction, please retry: (OC000) 【参考】Aurora DSQL での実行例 52
  33. ・シェルB(更新が成功した回数を確認) postgres=> SELECT * FROM hoge.fuga WHERE id = 1;

    id | val ----+------ 1 | 1044 (1 row) ※すべての UPDATEが成功していれば 2000になるはずだが、結果は 1044なので956回のUPDATEが競合で失敗 【参考】Aurora DSQL での実行例 53
  34. [1] 主キーの競合を避ける 56 • 挿入(INSERT)時の競合を防ぐ • シーケンス値のような単調増加の値を避け、UUID(v4) の ような重複・衝突が発生しづらい値を使う ◦

    DSQL には SERIAL が実装されていない ◦ 代用としての「テーブル最終行の主キーを +1 して使う」方法は 競合を誘発するので採用しない
  35. [2] データ行の変更をできるだけ避ける 57 • a. 変更頻度は低く ◦ 可能ならイミュータブルなテーブル設計により挿入(INSERT)後 の変更を行わない(理想論) ▪

    イミュータブルは無理でも、挿入後は更新せず削除のみにできないか? ◦ 高頻度で特定データ行の変更(更新)が発生する設計を避ける ▪ カウンターなど ▪ 必要なら別のデータストアの使用を検討
  36. [3] 変更トランザクションの実行時間を短くする 59 • トランザクション中の無駄な待ち時間はできるだけ削減 ◦ BEGIN 後速やかに変更処理 ◦ その後速やかに

    COMMIT / ROLLBACK • 前述のとおり DSQL には「1 トランザクション 3,000 行 まで」の変更制限あり ◦ トランザクションを適切に分割
  37. • 「A+B が 20 万未満なら +2 万して UPDATE」 (アプリケーションのロジックで判定) •

    SELECT は競合しない • UPDATE も競合しない(対象行が別) • たとえば A=10 万・B=8 万のときに、本来なら TxA の処理で A が 12 万になるだけ のはずが TxB が並行で進み競合せず→結果、B も 10 万に 書き込みスキュー異常の例 ⚫BEGIN ◎SELECT A ◎SELECT B   → 100000・   80000 COMMIT⭕ ⚫BEGIN COMMIT⭕ ⚫BEGIN ◎SELECT A ◎SELECT B   → 120000・   100000 ◎A+B<200000 →UPDATE A=A+20000 ◎A+B<200000 →UPDATE B=B+20000 64 ◎SELECT A ◎SELECT B   → 100000・   80000 TxA TxB
  38. • SELECT に ... FOR UPDATE を追加して A・B それぞれの行に変更フラグを立てる →

    TxB は競合扱い→ COMMIT 時に失敗❌(その後リトライしても B は 8 万のまま) 回避方法 ⚫BEGIN ◎SELECT A    ... FOR UPDATE ◎SELECT B    ... FOR UPDATE →100000・80000 COMMIT❌ ⚫BEGIN COMMIT⭕ ⚫BEGIN 66 ◎A+B<200000 →UPDATE A=A+20000 ◎A+B<200000 →UPDATE B=B+20000 ◎SELECT A ◎SELECT B   → 120000・   80000 ◎SELECT A    ... FOR UPDATE ◎SELECT B    ... FOR UPDATE →100000・80000 TxA TxB
  39. ・トランザクション A(テーブル準備〜初期データ挿入) postgres=> CREATE TABLE hoge.account(number INT PRIMARY KEY UNIQUE,

    name VARCHAR(100) NOT NULL, amount INT NOT NULL); postgres=> INSERT INTO hoge.account VALUES(10000001, '佐藤一郎', 100000); postgres=> INSERT INTO hoge.account VALUES(11000001, '佐藤二朗', 80000); postgres=> SELECT * FROM hoge.account; number | name | amount ----------+----------+-------- 10000001 | 佐藤一郎 | 100000 11000001 | 佐藤二朗 | 80000 (2 rows) 67 【参考】Aurora DSQL での実行例
  40. ・トランザクション A(開始〜SELECT...FOR UPDATE) postgres=> BEGIN; postgres=*> SELECT * FROM hoge.account

    WHERE number = 10000001 FOR UPDATE; number | name | amount ----------+----------+-------- 10000001 | 佐藤一郎 | 100000 (1 row) postgres=*> SELECT * FROM hoge.account WHERE number = 11000001 FOR UPDATE; number | name | amount ----------+----------+-------- 11000001 | 佐藤二朗 | 80000 (1 row) 68 【参考】Aurora DSQL での実行例
  41. ・トランザクション B(開始〜SELECT...FOR UPDATE) postgres=> BEGIN; postgres=*> SELECT * FROM hoge.account

    WHERE number = 10000001 FOR UPDATE; number | name | amount ----------+----------+-------- 10000001 | 佐藤一郎 | 100000 (1 row) postgres=*> SELECT * FROM hoge.account WHERE number = 11000001 FOR UPDATE; number | name | amount ----------+----------+-------- 11000001 | 佐藤二朗 | 80000 (1 row) 69 【参考】Aurora DSQL での実行例
  42. ・トランザクション A(合計20万未満なので 佐藤一郎の口座を +2万してコミット ) postgres=*> UPDATE hoge.account SET amount

    = amount + 20000 WHERE number = 10000001; postgres=*> COMMIT; postgres=> SELECT * FROM hoge.account; number | name | amount ----------+----------+-------- 10000001 | 佐藤一郎 | 120000 11000001 | 佐藤二朗 | 80000 (2 rows) ・トランザクション B(合計20万未満なので 佐藤二朗の口座を +2万してコミット →失敗) postgres=*> UPDATE hoge.account SET amount = amount + 20000 WHERE number = 11000001; postgres=*> COMMIT; ERROR: change conflicts with another transaction, please retry: (OC000) ※再実行したときには合計 20万なので条件を満たさず →加算せず終了 70 【参考】Aurora DSQL での実行例
  43. • OCC を意識したアプリケーション設計を行う ◦ 変更トランザクションの競合をできるだけ避ける ▪ テーブル設計、一度に変更する行数、実行時間など ◦ 競合時にはアプリケーションでリトライする ◦

    実行順が重要なケースではキューのサービスを併用する • 書き込みスキュー異常回避のためにあえて競合させる ◦ データ間の矛盾が生じうるケースでは SELECT ... FOR UPDATE で対象行に変更フラグを立ててあえて競合させトランザクション を失敗させる 72
  44. AWS ワークショップ 75 • Amazon Aurora DSQL Immersion Day ◦

    https://catalog.us-east-1.prod.workshops.aws/workshops/e02d9cba-c586 -45e9-af8b-2a80bb63579d/ja-JP
  45. 個人的な発信 76 • 「ゲームで体感!Aurora DSQL の OCC(楽観的同時実行 制御)」の結果ログから Aurora DSQL

    の動作を考察する ◦ https://qiita.com/hmatsu47/items/75eee0b21e3be5b80061 • Aurora DSQL のトランザクション(スナップショット分 離と OCC) ◦ https://www.docswell.com/s/hmatsu47/KQXN6R-aurora-dsql-transaction- 20250821