- データベース
- 技術 💻
トランザクション分離レベル8選|ロストアップデート対策
分離レベルって、ほとんどの人がデフォルトのまま使っていますよね。
でも、同じSELECTが違う結果を返したり、更新が黙って消えたり——「たまに起きる変な挙動」の多くは、ここが出どころです。今回は、知っていると障害調査が変わるものを8つ集めました。1つでも知らなかったら儲けものです。
① 同じトランザクション内で、同じSELECTの結果が変わります
まず、これを見てください。
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- → 1000
-- (この間に、他の誰かが更新してCOMMIT)
SELECT balance FROM accounts WHERE id = 1; -- → 800 !?
COMMIT;
トランザクションの中なのに、2回目のSELECTが別の値を返す。バグではありません。READ COMMITTEDという分離レベルの、仕様どおりの動きです。
「コミット済みの最新を読む」=他人のコミットが途中で見えるということなんですよね。反復不能読み取り(non-repeatable read)と呼ばれる現象です。
② 行が「増える」パターンもあります
①は既存の行の値が変わる話でした。もうひとつ、集計の間に行が増えるパターンがあります。
「WHERE 条件で件数を数える → もう一度数えると増えている」。他の誰かが条件に合う行をINSERTしてコミットしたからです。ファントムリード(幻読)と呼ばれます。
月次の締め処理で、集計の途中にデータが増えると、合計と明細が合わなくなる。「昨日のバッチだけ数字がズレた」の犯人候補なんです。
③ デフォルトの分離レベルは、DBで違います
ここ、即答できますか。
| DB | デフォルトの分離レベル |
|---|---|
| PostgreSQL | READ COMMITTED |
| Oracle | READ COMMITTED |
| MySQL(InnoDB) | REPEATABLE READ |
つまり①の「同じSELECTが変わる」現象は、PostgreSQLでは起きて、MySQLのデフォルトでは起きません。同じSQLを移植しただけで、挙動が変わるわけです。
「前のDBでは動いていたのに」の原因が、コードではなく分離レベルだったりするんですよね。
④ 同じ名前の分離レベルでも、中身は同じではありません
さらに深い話です。
MySQLのREPEATABLE READとPostgreSQLのREPEATABLE READは、名前が同じでも実装思想が違います。スナップショットの取り方、ロックの掛け方、ファントムをどこまで防ぐか——細部の挙動が一致しないんです。
分離レベルの名前は、いわば「最低保証のラベル」。実際の挙動は、使っているDBのドキュメントでしか確認できません。
「規格で決まっているから同じでしょ」が通用しない、珍しい領域なんです。
⑤ ロストアップデートは、分離レベル任せにできません
いちばん実害が大きいのが、これです。
2人が同時に「残高を読んで、計算して、書き戻す」をやると、後から書いた方が前の更新を上書きして消します。ロストアップデートです。
対策は、アプリ側で明示的に守ること。
- 悲観ロック —
SELECT ... FOR UPDATEで行を先に押さえる - 楽観ロック — バージョン列を持ち、更新時に「読んだときのバージョンのままか」を条件に入れる
「読んで書き戻す」処理を見たら、どちらで守るかを必ず決める。レビュー観点として最強の1つです。
⑥ SERIALIZABLEは、安全と引き換えに「失敗」します
「全部SERIALIZABLEにすれば安全では?」——正しいです。ただし代償があります。
SERIALIZABLEは、同時に走ったトランザクションを「1本ずつ順番に実行したのと同じ結果」に保証するレベルです。その保証のため、両立できない組み合わせを検知すると、片方をエラーで失敗させます。
つまりアプリ側にリトライ処理が必須になります。リトライを書かずにSERIALIZABLEだけ上げると、「たまに落ちるバグ」が増えたように見えるんですよね。
安全は、設計とセットで買うものなんです。
⑦ 読み取り専用でも、トランザクションには意味があります
「SELECTしかしないなら、トランザクションは要らない」——実は、あります。
複数のSELECTで帳票を作るとき、1本ごとに世界が進むと、明細と合計がズレ得ます。全クエリに同じ時点のスナップショットを見せたいんですよね。
ここで①が効いてきます。READ COMMITTEDのままBEGINで囲むだけでは足りません。1文ごとに最新を読むので、囲んでもズレるんです。REPEATABLE READ以上で囲む(Oracleなら SET TRANSACTION READ ONLY)ことで、はじめて全クエリが同じ時点を見ます。
「複数のSELECTの整合性」は、読み取り専用トランザクションの仕事。地味ですが、締め処理の品質を支える知識です。
⑧ 長いトランザクションは、周りを巻き込みます
最後は、運用の話です。
トランザクションを長く開きっぱなしにすると、ロックを持ち続けて他を待たせるだけでなく、DBが「古いスナップショットのために保持している過去データ」を掃除できなくなります。
結果、自分は何もしていないのに、DB全体がじわじわ重くなる。
「トランザクション内で外部APIを呼ぶ」「画面の入力待ちの間、開きっぱなし」——この2つは、レビューで止めたい定番パターンです。
番外編:手元で確かめられる小ネタ
今の分離レベルを見る
-- MySQL
SELECT @@transaction_isolation;
-- PostgreSQL
SHOW transaction_isolation;
まず現在地の確認からです。
ターミナルを2枚開くだけで実験できます
①の現象は、ターミナルを2つ開いて片方でBEGIN、もう片方でUPDATE→COMMITすれば、手元で再現できます。教科書より速いですよ。
ANSIの4レベルは1992年生まれ
READ UNCOMMITTEDからSERIALIZABLEまでの4段階は、SQL-92で定義されたものです。30年以上、現役の分類なんですね。
RJCでも、トランザクションと向き合っています
RJCが携わる保険、金融、決済のシステムは、残高と履歴の整合が仕様の中心にある世界です。⑤のロストアップデート対策は、まさに設計レビューの定番論点なんですよね。
こういう仕組みの話を、Techチームで共有しながら開発しています。
よくいただくご質問
Q. まずどれから押さえるべきですか?
A. ⑤のロストアップデートかなと思います。実害が一番大きくて、しかも分離レベルを上げるだけでは防げないんですよね。
Q. 結局、分離レベルはどう選べばいいですか?
A. デフォルトを把握したうえで、守りたい処理だけ明示的にロックやリトライで固めるのが現実的です。全体を上げるより、急所を守る発想ですね。
Q. 現場でどう確認すればいいですか?
A. 番外編のコマンドで現在の設定を見て、①の実験を2枚のターミナルでやってみてください。挙動を目で見た人がチームに1人いるだけで、調査が速くなります。
Q. 未経験からでも、分かるようになりますか?
A. なります。「2人が同時に触ったら何が起きるか」を図に描く練習をすれば、必ず追える世界です。
「同時に来たら」を想像できる人が、強いです
今日の8つに共通していたのは、これでした。
1人で動かしている限り、トランザクションの問題は見えません。2人目が同時に来た瞬間に、すべてが始まります。
「この処理、同時に2本走ったらどうなる?」——この一言をレビューで言える人が、本番の障害を未然に減らすんですよね。
みなさんが踏んだトランザクションの事故、よかったら教えてください。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。