コンピューターサイエンス:アルゴリズム / OS / ネットワーク / DB

MVCC(マルチバージョン同時実行制御)

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

MVCC(マルチバージョン同時実行制御)

このレッスンで分かること

  • MVCC が「読み手と書き手が衝突しない」を実現する仕組み
  • スナップショットアイソレーションという概念
  • VACUUM や Undo Log がなぜ必要か

MVCC とは

読み手をブロックしないバージョン管理の仕組み。本レッスンでは、MVCC の基本から実際の使いどころまでを整理し、現場で迷わず使える形に落とし込みます。

ロックの問題

伝統的な分離方式は ロック です。読み手は 共有ロック、書き手は 排他ロック を取り、衝突したら待たせます。

diagram (will load when visible)

問題は、読み手と書き手が常に競合 することです。書き込み中は読めない、読み中は書けない、というのは Web アプリにとって致命的です。同じ行を頻繁に更新する場合、読み手がずっと待たされます。

MVCC の発想

MVCC (Multi-Version Concurrency Control) は、データを 複数のバージョン で保持することでロックを回避します。

書き手が更新するとき、古いバージョンを残したまま新しいバージョンを作ります。読み手は自分のトランザクションが始まった時点での「スナップショット」を見るので、書き手と衝突しません。

diagram (will load when visible)

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)が記録されます。

idnamexminxmax
1Alice100200
1Alice200NULL

最初の行は 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 UPDATEUPDATE で待たされます。

各 DB の採用状況

DBMVCC実装
PostgreSQLあり行内 xmin/xmax
MySQL InnoDBありUndo Log
OracleありUndo segment
SQL Serverあり(READ_COMMITTED_SNAPSHOT 有効時)tempdb の version store
TiDBありTSO ベースの分散 MVCC

ほとんどの主要 RDB が MVCC を採用しています。

やってみよう

  • PostgreSQL で同じ行を別セッションで UPDATESELECT し、お互いに待たないことを確認する
  • pg_stat_user_tablesn_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 つができることを確認しましょう。

  1. MVCC(マルチバージョン同時実行制御) の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. MVCC(マルチバージョン同時実行制御) とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン