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

Aurora DSQL のトランザクション(スナップショット分離と OCC)

Aurora DSQL のトランザクション(スナップショット分離と OCC)

クラウド LT 大会 vol.14 フリーテーマ! 2025/8/21

Avatar for hmatsu47

hmatsu47 PRO

August 21, 2025
Tweet

More Decks by hmatsu47

Other Decks in Technology

Transcript

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

    ◦ SRE チームに所属しつつ技術検証の支援をしています ◦ 普段カンファレンス・勉強会では DB の話しかしていません (ほぼ) ▪ 今月これで 4 本目(1 本だけ珍しく AI コーディングの話) 2
  2. 本日の内容 • なぜこの話を? • Aurora DSQL 概要 • トランザクション処理おさらい •

    スナップショット分離と OCC • Aurora DSQL での動作 • まとめ 3
  3. Aurora DSQL:サーバーレスの新しいデータストア 5 • DynamoDB はよくできているが扱いが難しい面がある ◦ アプリケーションと密結合なテーブル設計になりがち • Aurora

    DSQL ならスケールする RDBMS として使える ◦ RDBMS のテーブル設計の知見が生かせる • 一方、Aurora DSQL は通常の RDBMS とは異なる部分も ◦ OCC(楽観的同時実行制御)の採用など ◦ ここでハマってしまうと「使えない」「難しい」となってしまう
  4. サーバーレス分散 SQL データベース 8 • PostgreSQL ワイヤープロトコル互換 ◦ psql コマンドが使える

    • シングルリージョン構成とマルチリージョン構成がある ◦ マルチリージョン構成は US 3 リージョン/欧州 3 リージョン/ 東京+大阪+ソウルの組み合わせでサポート ▪ エンドポイントは 2 リージョン、残り 1 つは Witness リージョンで構成 ◦ 次ページの図はシングルリージョン構成の例
  5. トランザクション処理とは 11 • 整合性・一貫性を保つために複数の処理をまとめて一つ の不可分な処理単位として扱う仕組みのこと ◦ 例えば A さんの口座から B

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

    オートコミット ON の場合、1 つの SQL 文毎にコミット処理が 行われるが、BEGIN を発行するとトランザクション開始 ▪ BEGIN 以降はオートコミットされない ◦ オートコミット OFF の場合、最初の SQL 文が発行された時点か らトランザクション開始
  7. スナップショット分離(Snapshot Isolation)とは 14 • DBMS におけるトランザクション分離レベルの 1 つ ◦ トランザクション開始時のコミット済みデータを読み取る

    ◦ 並行する他のトランザクションが更新したデータを読み取らない • 書き込みスキュー異常発生の可能性がある ◦ 並行する複数のトランザクションで相互に関連するデータを読み 取り、その値を元に別々のデータを更新するケースで、最終的な 結果の矛盾が生じることも ▪ 詳細は「Aurora DSQL での動作」にて
  8. OCC(楽観的同時実行制御)とは 15 • 同時実行制御方式の 1 つ ◦ ロックを使わない ▪ 通常の

    RDBMS ではロックを使う→ PCC(悲観的同時実行制御) ◦ 並行する複数のトランザクションが同じデータ行の更新を試みた 場合、最初にコミットしたトランザクションの処理が成功する ▪ 後からコミットしたトランザクションの処理は中断(アボート) ▪ 必要に応じてロールバック後にアプリケーションでリトライ
  9. おことわり 17 • 一部の例ではトランザクションの中で 1 つの SQL 文しか 発行しないケースを示しています ◦

    目的:理解しやすくするため • オートコミット設定 ON のケースを示しています ◦ BEGIN を発行することで明示的にトランザクションを開始
  10. • トランザクションA(TxA)で COMMIT 成功⭕→ TxB で値は表示されない • TxB で COMMIT

    →(新たなトランザクション開始) → TxA で COMMIT した値が表示される スナップショット分離(Snapshot Isolation) ⚫BEGIN ◎INSERT A (1, 100) COMMIT⭕ ⚫BEGIN ◎SELECT A →空 COMMIT⭕ ◎SELECT A →空 ◎SELECT A → (1, 100) ここはCOMMIT前とは 別のトランザクション 19 TxA TxB
  11. ・トランザクション 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; スナップショット分離(Snapshot Isolation) 20
  12. ・トランザクション 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) スナップショット分離(Snapshot Isolation) 21
  13. ・トランザクション 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) 同じデータ行を更新しようとした場合 [1] 25
  14. 同じデータ行を更新しようとした場合 [2] • TxA の COMMIT 成功⭕後に TxC が BEGIN

    し、 TxB の COMMIT 失敗❌・ROLLBACK 後に TxC が COMMIT →成功⭕ ⚫BEGIN ◎UPDATE A COMMIT⭕ ◎UPDATE A ⚫BEGIN COMMIT⭕ ◎UPDATE A COMMIT❌ →ROLLBACK ⚫BEGIN 27 TxA TxB TxC
  15. ・トランザクション 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; 同じデータ行を更新しようとした場合 [2] 28
  16. ・トランザクション B(コミット失敗・ロールバック) postgres=*> COMMIT; ERROR: change conflicts with another transaction,

    please retry: (OC000) postgres=> ROLLBACK; WARNING: there is no transaction in progress ・トランザクション C(コミット・データ参照 →トランザクション Cの値で上書きされている) postgres=*> COMMIT; postgres=> SELECT * FROM hoge.fuga; id | val ----+----- 1 | 130 (1 row) 同じデータ行を更新しようとした場合 [2] 29
  17. • TxA の COMMIT(成功)前に TxC が BEGIN → TxC は

    COMMIT 時に失敗❌ → BEGIN ~ COMMIT / ROLLBACK の期間が重なっていれば競合対象 同じデータ行を更新しようとした場合 [3] ⚫BEGIN ◎UPDATE A COMMIT⭕ ◎UPDATE A ⚫BEGIN ◎UPDATE A ⚫BEGIN COMMIT❌ 31 COMMIT❌ →ROLLBACK TxA TxB TxC
  18. ・トランザクション 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; 同じデータ行を更新しようとした場合 [3] 32
  19. ・トランザクション C(データ更新) postgres=*> UPDATE hoge.fuga SET val = 140 WHERE

    id = 1; ・トランザクション B(コミット失敗・ロールバック) postgres=*> COMMIT; ERROR: change conflicts with another transaction, please retry: (OC000) postgres=> ROLLBACK; WARNING: there is no transaction in progress ・トランザクション C(コミット失敗) postgres=*> COMMIT; ERROR: change conflicts with another transaction, please retry: (OC000) 同じデータ行を更新しようとした場合 [3] 33
  20. 1. BEGIN ~ COMMIT / ROLLBACK の期間が重なっていて も競合しない 2. 複数テーブル・複数行を更新するトランザクション間で

    は、更新対象行が重なるものだけが競合する 更新対象行がトランザクション毎に異なる場合 34
  21. 更新対象行がトランザクション毎に異なる場合 [2] • TxA と TxC は更新(挿入)対象が重ならない→非競合 TxB が COMMIT

    失敗❌・ROLLBACK → TxC の COMMIT は成功⭕ ⚫BEGIN ◎INSERT A COMMIT⭕ ◎INSERT B ⚫BEGIN ⚫BEGIN COMMIT⭕ ◎INSERT A ◎INSERT B 38 COMMIT❌ →ROLLBACK TxA TxB TxC
  22. ・トランザクション 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); 更新対象行がトランザクション毎に異なる場合 [2] 39
  23. ・トランザクション 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; 更新対象行がトランザクション毎に異なる場合 [2] 40
  24. ・トランザクション B(コミット失敗・ロールバック) postgres=*> COMMIT; ERROR: change conflicts with another transaction,

    please retry: (OC000) postgres=> ROLLBACK; WARNING: there is no transaction in progress ・トランザクション C(コミット) postgres=*> COMMIT; 更新対象行がトランザクション毎に異なる場合 [2] 41
  25. ・トランザクション 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) 更新対象行がトランザクション毎に異なる場合 [2] 42
  26. • 「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 44 ◎SELECT A ◎SELECT B   → 100000・   80000 TxA TxB
  27. • SELECT に ... FOR UPDATE を追加して A・B それぞれの行に更新フラグを立てる →

    TxB は競合扱い→ COMMIT 時に失敗❌(その後リトライしても B は 8 万のまま) 書き込みスキュー異常の回避 ⚫BEGIN ◎SELECT A    ... FOR UPDATE ◎SELECT B    ... FOR UPDATE →100000・80000 COMMIT❌ ⚫BEGIN COMMIT⭕ ⚫BEGIN 46 ◎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
  28. ・トランザクション 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) 書き込みスキュー異常の回避 47
  29. ・トランザクション 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) 書き込みスキュー異常の回避 48
  30. ・トランザクション 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) 書き込みスキュー異常の回避 49
  31. ・トランザクション 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万なので条件を満たさず →加算せず終了 書き込みスキュー異常の回避 50
  32. • OCC は PCC と挙動が異なる ◦ ロックしないのでコミット時に更新の競合を判定 ▪ 必要ならアプリケーションでリトライを実装 •

    BEGIN と COMMIT / ROLLBACK の時刻で競合判断 ◦ この時間範囲と更新対象行が重なっていれば競合とみなす ▪ (オートコミット ON 設定でのトランザクションは)BEGIN が始点になる ▪ 競合しても先行側が失敗・ロールバックしていればコミットは成功する • 書き込みスキューに注意 ◦ SELECT ... FOR UPDATE で対象行に更新フラグを立てて回避 52