コンピューターサイエンス:アルゴリズム / OS / ネットワーク / DB
MVCC(マルチバージョン同時実行制御)
MVCC(マルチバージョン同時実行制御)
このレッスンで分かること
- MVCC が「読み手と書き手が衝突しない」を実現する仕組み
- スナップショットアイソレーションという概念
- VACUUM や Undo Log がなぜ必要か
MVCC とは
読み手をブロックしないバージョン管理の仕組み。本レッスンでは、MVCC の基本から実際の使いどころまでを整理し、現場で迷わず使える形に落とし込みます。
ロックの問題
伝統的な分離方式は ロック です。読み手は 共有ロック、書き手は 排他ロック を取り、衝突したら待たせます。
問題は、読み手と書き手が常に競合 することです。書き込み中は読めない、読み中は書けない、というのは Web アプリにとって致命的です。同じ行を頻繁に更新する場合、読み手がずっと待たされます。
MVCC の発想
MVCC (Multi-Version Concurrency Control) は、データを 複数のバージョン で保持することでロックを回避します。
書き手が更新するとき、古いバージョンを残したまま新しいバージョンを作ります。読み手は自分のトランザクションが始まった時点での「スナップショット」を見るので、書き手と衝突しません。
T1 は自分の開始時点 (version 1) のデータを見続け、T2 の更新は見えません。T3 は新しく開始したので version 2 を見ます。読み手は待たず、書き手も待たないのが MVCC の魅力です。
「読み手は書き手をブロックせず、書き手は読み手をブロックしない」が MVCC のスローガン。
スナップショットアイソレーション
MVCC を使った分離レベルが Snapshot Isolation です。これは厳密には ANSI 標準にはないですが、実質的に Repeatable Read 相当か、それより少し弱い保証を提供します。
PostgreSQL の Repeatable Read は実は Snapshot Isolation です。MySQL InnoDB の Repeatable Read も MVCC ベースで、ANSI 標準より強い保証(ギャップロック でファントムも防止)を提供します。
バージョンの管理
PostgreSQL の場合
各行に xmin(作成 TX ID)と xmax(削除 TX ID)が記録されます。
| id | name | xmin | xmax |
|---|---|---|---|
| 1 | Alice | 100 | 200 |
| 1 | Alice | 200 | NULL |
最初の行は TX 100 で作られて TX 200 で削除(実は更新)された旧バージョン、2 行目が現行バージョンです。読み手のスナップショットが TX 150 なら、xmin < 150 AND (xmax IS NULL OR xmax > 150) で旧バージョンを見ます。
MySQL InnoDB の場合
更新前の値を Undo Log に書きます。読み手は現在の行から Undo Log をたどって自分のスナップショット時点の値を復元します。
VACUUM と Undo Log の問題
MVCC は「古いバージョンが溜まる」副作用があります。
PostgreSQL は VACUUM で不要バージョンを回収します。これを怠ると テーブル膨張 が起き、性能が劣化します。autovacuum が自動実行しますが、長時間のトランザクションが走っていると VACUUM できず、肥大化が止まりません。
ロングトランザクション + MVCC = テーブル膨張地獄。これは PostgreSQL 運用の鬼門。
MySQL InnoDB の Undo Log も似た問題があり、長時間トランザクションが Undo Log を肥大化させます。
MVCC でも防げない異常
MVCC ベースの Snapshot Isolation でも、書き込みスキュー (Write Skew) という異常があります。
医師オンコール表の例で、2 人の医師が同時に「自分以外にもう 1 人いるから休む」と判断して両方休んでしまう状況です。同じ行を読んでいるわけではないので MVCC のロックでは検出できません。
これを防ぐには Serializable Snapshot Isolation (SSI) または 明示的ロック (SELECT ... FOR UPDATE) を使います。PostgreSQL の SERIALIZABLE は SSI を実装しています。
明示的なロック
MVCC でも書き込み競合は避けられません。同じ行を同時に UPDATE すると後者は待ちます。それより前段階で予約したい場合は SELECT ... FOR UPDATE を使います。
SQL クエリ
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- アプリで残高チェック
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
COMMIT;これは行ロックを取り、他の TX は同じ行の SELECT FOR UPDATE や UPDATE で待たされます。
各 DB の採用状況
| DB | MVCC | 実装 |
|---|---|---|
| PostgreSQL | あり | 行内 xmin/xmax |
| MySQL InnoDB | あり | Undo Log |
| Oracle | あり | Undo segment |
| SQL Server | あり(READ_COMMITTED_SNAPSHOT 有効時) | tempdb の version store |
| TiDB | あり | TSO ベースの分散 MVCC |
ほとんどの主要 RDB が MVCC を採用しています。
やってみよう
- PostgreSQL で同じ行を別セッションで
UPDATEとSELECTし、お互いに待たないことを確認する pg_stat_user_tablesでn_dead_tupを確認し、VACUUM の効果を見るSELECT ... FOR UPDATEを使った同時実行制御を 2 セッションで試す
よくある質問
Q. MVCC のメリットは何ですか?
A. 読み取りが書き込みをブロックせず、書き込み中の値とは別バージョンを読めるため、並行性能が大幅に上がります。PostgreSQL や InnoDB が採用しており、長時間の SELECT が書き込みを止めない点が運用上大きなメリットです。
Q. MVCC のデメリットは?
A. 古いバージョンを保持するためディスク使用量が増え、定期的な vacuum / undo log のクリーンアップが必要になります。長時間のトランザクションは古いバージョンを掴み続けるため、vacuum が進まず DB が肥大化するリスクがあります。
Q. MVCC とロックはどう違いますか?
A. ロックは「相手を待たせる」方式、MVCC は「バージョンを増やして同時実行する」方式です。読み取りは MVCC で待たずに済むため、OLTP では MVCC + 行ロックの組み合わせが現代的な定石になっています。
次のレッスン
次は デッドロックと回避 で、複数トランザクションが互いにロック解放を待ち合って進まなくなる現象とその検出・回避の仕組みを学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- MVCC(マルチバージョン同時実行制御) の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. MVCC(マルチバージョン同時実行制御) とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン