💫

DB同時実行制御のヒミツ:トランザクションはなぜ整合性を保てるのか

に公開

概要

「ECサイトで最後の1個の商品に、同時に2人のユーザーから購入リクエストが来たらどうなるの?😱」
「銀行の残高照会中に、別で振込処理が走ったら、表示される金額はどうなっちゃうの?😨」

Web開発をしていると、こんなふうに複数のリクエストが同時にデータベース(DB)にアクセスする場面に必ず出くわします。もし何の対策もしていないと、データがおかしくなってしまい(整合性が壊れる)、大変なことになってしまいます。

この記事では、そんな恐ろしい事態を防ぎ、データの整合性を守りながら、たくさんのリクエストを華麗に捌いてくれるDBのスーパーヒーロー、「トランザクション」のヒミツに迫ります。

そもそもトランザクションって何?

トランザクションとは、一言でいうと「すべて成功するか、すべて失敗するかのどちらかにしたい、一連の処理のかたまり」のことです。銀行の振込処理を例に考えてみましょう。

AさんからBさんに1万円を送金する

  1. Aさんの口座残高から1万円を引く
  2. Bさんの口座残高に1万円を足す

この2つの処理は、絶対にセットで実行されなければいけません。もし1の処理だけ成功して、2の処理がシステム障害で失敗したら…Aさんの1万円はどこかへ消えてしまいます💸

トランザクションは、こうした一連の処理を一つのパッケージとして扱い、「全部成功(コミット)」するか、「全部失敗(ロールバック)」するかのどちらかの状態を保証してくれます。これにより、データが中途半端な状態になるのを防ぎます。

このトランザクションの信頼性を支えているのが、有名な「ACID特性」です。

トランザクションを支えるACID特性

ACIDは、4つの性質の頭文字を取ったものです。

  • Atomicity(原子性)
    • トランザクション内の処理は「すべて実行される」か「一つも実行されない」かのどちらか。All or Nothingの原則です。
  • Consistency(一貫性)
    • トランザクションの前後で、データの整合性が保たれていること。例えば、「残高がマイナスにならない」といったルール(制約)を常に守ります。
  • Isolation(独立性/隔離性)
    • 複数のトランザクションを同時に実行しても、それぞれが他のトランザクションの影響を受けず、あたかも一つずつ順番に実行されているかのように見えること。今回の記事の主役です!
  • Durability(永続性)
    • 正常に完了(コミット)したトランザクションの結果は、システム障害が起きても失われないこと。

複数のリクエストを捌くヒミツ:独立性(Isolation)の裏側

さて、ここからが本題です。たくさんのリクエスト(=たくさんのトランザクション)が同時に来ても、なぜDBは混乱せずに整合性を保てるのでしょうか?その鍵を握るのが、ACID特性の I (Isolation)です。

もし、この独立性がなかったら、以下のような問題(競合問題)が発生します。

  • ダーティリード (Dirty Read)
    • あるトランザクションが更新途中の、まだコミットされていないデータを、別のトランザクションが読み込んでしまう現象。お化けデータ👻を読んでしまうようなものです。
  • ノンリピータブルリード (Non-Repeatable Read)
    • あるトランザクションが同じデータを2回読んだときに、その間に別のトランザクションがデータを更新・コミットしたせいで、1回目と2回目で違う結果が返ってくる現象。読むたびに結果が変わってしまいます。
  • ファントムリード (Phantom Read)
    • あるトランザクションが一定範囲のデータを検索した後に、別のトランザクションがその範囲内に新しいデータを追加・コミットしたせいで、再度同じ範囲を検索すると、以前はなかったはずのデータ(ファントム👻)が出現する現象。

これらの問題を解決するために、DBには「トランザクション分離レベル」という設定があります。これは、独立性(Isolation)のレベルを調整するためのもので、レベルが高いほどデータの整合性は強固になりますが、その分、性能(同時に処理できる数)は少し落ちる(トレードオフの関係)傾向にあります。

分離レベル ダーティリード ノンリピータブルリード ファントムリード
READ UNCOMMITTED 発生する 発生する 発生する
READ COMMITTED 防げる 発生する 発生する
REPEATABLE READ 防げる 防げる 発生する
SERIALIZABLE 防げる 防げる 防げる

多くのDB(MySQLのInnoDBやPostgreSQLなど)では、REPEATABLE READREAD COMMITTEDがデフォルトの分離レベルとして採用されています。

どうやって独立性を実現しているの?

では、DBは具体的にどうやってこの「独立性」を実現しているのでしょうか?主な仕組みは2つあります。

1. ロック(Locking) 🔐

最も直感的な方法が「ロック」です。あるトランザクションがデータにアクセスするとき、そのデータに鍵をかけて、他のトランザクションが中途半端にアクセスできないようにします。

  • 共有ロック (Shared Lock)
    • データを読み取るときに使います。他のトランザクションも共有ロックをかけて読み取ることはできますが、後述の排他ロックをかけることはできません。
  • 排他ロック (Exclusive Lock)
    • データを**書き込む(更新・削除する)**ときに使います。排他ロックがかかっているデータには、他のトランザクションは共有ロックも排他ロックもかけられず、ただ待つことしかできません。

これにより、誰かが書き込んでいる最中に別の誰かが読み込んだり、同時に複数の人が書き込んだりするのを防ぎます。

2. MVCC (Multi-Version Concurrency Control) 📜

ロックはシンプルで強力ですが、書き込みと読み込みが頻繁に発生すると、ロックの待ち時間が増えて性能が落ちてしまうことがあります。

そこで登場するのが「MVCC(多版型同時実行制御)」という賢い仕組みです。

MVCCは、データを更新するたびに古いデータをすぐに上書きせず、新しいバージョンのデータを作成します。そして、各トランザクションが始まった時点の、適切(整合性が取れる)なバージョンのデータを読み取るように制御します。

MVCCのメリット

  • 読み取りが書き込みをブロックしない:書き込み処理が行われていても、読み取り処理は古いバージョンのデータを参照すれば良いので、ロックのように待つ必要がありません。これにより、高い同時実行性能を実現できます。
  • 書き込みも読み取りをブロックしない

PostgreSQLやMySQL (InnoDB)、Oracleなど、主要なRDBMSで採用されている人気の方式です。私たちが普段、DBの性能の高さを実感できるのは、このMVCCのおかげと言っても過言ではありません。

まとめ

最後に、今回のヒミツをまとめてみましょう!

  • DBは「トランザクション」という仕組みで、処理を安全なまとまりとして扱う。
  • トランザクションの信頼性はACID特性(特に独立性)によって支えられている。
  • 独立性を実現するために、DBは裏側でロックMVCCといった高度な仕組みを動かしている。
  • トランザクション分離レベルを適切に設定することで、システムの要件に合わせて整合性とパフォーマンスのバランスを取ることが大事。

これで、たくさんのリクエストが来てもDBがデータをしっかり守ってくれる理由が分かりましたね!普段何気なく使っているDBの健気な働きに、少し感謝したくなるかもしれません🙏

Happy Coding! 🎉

Discussion