MVCC と UNDO ログ
InnoDB では REPEATABLE-READ を実現するために MVCC (Multi Version Concurrency Control) という仕組みを備えており、読み取り専用のトランザクションにおいて若干の時差を許容することにより一貫性のある読み取りを実現している。
あるトランザクションが参照できるデータを常に同じものにするという性質を、もし最新のデータだけで実現しようとすると、参照したすべての行をロックするような実装になる。もし、あるトランザクションが更新した行を、COMMIT する前にほかのトランザクションが参照してしまう (ダーティリード) と、ロールバックによって行の値が元に戻り、新しい値が消失する可能性もある。そのため、現在実行中のトランザクションが更新した行については COMMIT が完了するまで、別のトランザクションは読み取るのを待つ必要がある。また、トランザクションが常に一貫した値を取得するには、読み取った行がほかのトランザクションから更新されないようにする必要がある。
そこで、過去のバージョンのデータを見せることにより、トランザクションに一貫したデータを参照させる MVCC と呼ばれる手法が実装された。ある行が更新された際、すでに実行していたトランザクションがその行データを参照しようとすると、最新ではなく、更新前のデータを参照させるようなロジックとなる。このように過去のデータを参照させればトランザクション開始時のデータを一貫して提供できるようになる上に、行に対してロックをかける必要はない。
なお、過去バージョンのデータについては、行データが更新される際に古いデータが UNDO ログとよばれる領域に退避 (完全コピーを作成) され、この退避された行データを UNDO レコードと呼ぶ。それぞれの行にはロールバックポインタというデータ領域があり、退避された UNDO レコードを指すようになっている。さらに同じ行が更新されると、再び行データが UNDO ログへ退避されるが、その退避された行データはさらに古いバージョンの UNDO レコードへのロールバックポインタを持つため、ロールバックポインタを準に辿ることで過去のバージョンを遡ることができる。
ちなみに、トランザクションがロールバックされると、UNDO ログにある古いデータが復活するようになっている。また、UNDO ログは複数のセグメントで構成されるため、その領域のことをロールバックセグメントと呼ぶ。
InnoDB では REPEATABLE-READ と READ-COMMITTED で MVCC が用いられる。一方 READ-UNCOMMITTED と SERIALIZABLE は UNDO ログを参照しない。READ-UNCOMMITTED はダーティーであっても最新のデータを読み取り、SERIALIZABLE は全ての行アクセスでロックをかけるため、最新のデータしか参照しないようになっている。
パージ処理
ある UNDO レコードを参照する可能性のあるトランザクションとは、新しい行データを書いたトランザクションが COMMIT するよりも前に実行を開始したもの、つまり古いトランザクションだけとなる。そのような古いトランザクションが終了すれば、過去のバージョンを参照する必要がなくなるため、UNDO レコードも削除可能になる。
UNDO レコードは、パージスレッドによってバックグラウンドで順次削除されるようになっている。またパージスレッドは「削除済み」とマークされた行を実際に削除する役目も担っている。DELETE により行が削除された場合、新しい行データというものは存在しないため、UNDO レコードは作成されない。しかし、削除される前のデータを参照するトランザクションは存在するかもしれないので、行は即座にデータページから削除されるのではなく、削除済みであるというマークが付けられる。その行を参照するトランザクションが存在するかぎり、その行データを実際に削除することはできない。
このように UNDO レコードと削除マークがついたレコードがパージスレッドによって削除される。
なお、InnoDB はトランザクションを COMMIT するたびに、History List と呼ばれるグローバルなリストに、トランザクションごとの UNDO レコードをひとつのエントリとして、COMMIT した順に保持している。SHOW ENGINE INNODB STATUS の History List Length はこのリストの長さ、つまりパージされていない UNDO ログエントリの数が表示される。パージスレッドはこの History List から古い順に UNDO ログエントリを辿って不要な UNDO レコードをみつけ、削除する。
mysql> SHOW ENGINE INNODB STATUS \G;
*************************** 1. row ***************************
Type: InnoDB
Name:
Status:
=====================================
2018-08-15 00:21:32 0x7feafc258700 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 5 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 76 srv_active, 0 srv_shutdown, 658677 srv_idle
srv_master_thread log flush and writes: 658731
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 643
OS WAIT ARRAY INFO: signal count 590
RW-shared spins 0, rounds 374, OS waits 181
RW-excl spins 0, rounds 302, OS waits 10
RW-sx spins 101, rounds 3030, OS waits 101
Spin rounds per wait: 374.00 RW-shared, 302.00 RW-excl, 30.00 RW-sx
------------
TRANSACTIONS
------------
Trx id counter 7729
Purge done for trx's n:o < 7727 undo n:o < 0 state: running but idle
History list length 33
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 422122514794320, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
...