データベースのトランザクション、特にトランザクション分離レベルの理解を定着させる上でまずはAnomalyを起点に話を整理するとわかりやすい。 なぜならトランザクション分離レベルというのは様々あるAnomalyをどこまで許容あるいは防ぐかという度合いだからだ。 しかし自分が見聞きしたデータベース、MySQL関連のトランザクション分離レベルの基本の解説では、明確にAnomalyという用語が使われていないことが多いように感じた。
よってこの記事では、データベースのトランザクションの理解のためにAnomalyの概要をまず整理する。
Anomaly概要
データベースにおいては並列処理のスループットを上げることは重要である。 並列で処理出来ないと組織でそのデータを同時に参照あるいは更新することが出来ず、1つずつ直列でしか操作が出来ない。
しかしデータを並列で処理する際にはデータ不整合に関する様々な問題が発生し得る。 データベースにおいてデータを並列に処理する上で起こってほしくないそのような問題はAnomalyと呼ばれ、具体的に様々な種類のAnomalyが定義されている。
Anomalyの種類
- dirty read
- あるトランザクションが他トランザクションのcommitしていない変更内容を読めてしまうこと
- 特にこれが問題になるのは、読み取った他トランザクションの内容が最終的にcommitされずrollbackされた場合など。それは存在しない不整合なデータを読み取った事になってしまう
- nonrepeatable read (fuzzy read。読み取りskew)
- あるトランザクションの途中で、他トランザクションの新たなcommitによるレコードの値の変更(更新)を読めてしまうこと
- phantom read
- あるトランザクションの途中で、他トランザクションの新たなcommitによる追加・削除(更新以外)を読めてしまうこと。また、selectクエリなどで条件に合致するレコードが増減してしまうこと
- つまり、とあるレコード自体の値は一貫性がある(nonrepeatable readは発生しない)が、selectされるレコード自体が増減するということ
- nonrepeatable readがより特化したものともいえそう
- lost update
- 同じレコードに対して複数のトランザクションが更新処理をした場合に、あるトランザクションの更新が別トランザクションの更新に影響されること
- このAnomalyはトランザクション分離レベルにより振る舞いが異なる
- トランザクション分離レベルがrepeatable readであれば、後から実行されたupdateが後勝ちになり、先に実行されたupdateがなかったものになる
- read committedであれば、先のupdateの結果に後のupdateが影響される
これらのAnomalyを解決するためにトランザクション分離レベルというものが定義されている。 Anomalyとトランザクション分離レベルはANSI SQLで定義されているもの(ANSI SQLに関する参考記事)でMySQLやInnoDBに固有のものではない。
これらのトランザクション分離レベルをどのように実現するかの具体的な方法論や仕組みは、各データベースやストレージエンジンで異なる。 MySQLであればストレージエンジンごとに解決方法が異なる。
その他のAnomaly (読み込みのAnomalyと書き込みのAnomaly)
dirty readやnonrepeatable read, phantom readはデータを読み取る際に発生する読み取りのAnomalyであり、それに対しlost updateはデータの書き込みに際して発生するAnomalyとも言える。 書き込みのAnomalyにはdirty writeというAnomalyがあり、これはdirty readしたコミットされていない(ロールバックされうる)データに基づいた書き込みのこと。 書き込みのAnomalyには他にwrite skewというものもある。(write skewについては「データ指向アプリケーションデザイン」が詳しい)
トランザクション分離レベル
- read uncommitted: コミット前のデータも読む
- dirty read: 発生してしまう
- nonrepeatable read: 発生してしまう
- phantom read: 発生してしまう
- lost update: 発生してしまう
- read committed: コミット済みのデータのみ読む
- dirty read: 発生しない
- nonrepeatable read: 発生してしまう
- phantom read: 発生してしまう
- lost update: 発生してしまう
- repeatable read: コミット中に値が変わらない (MySQL InnodBのデフォルト)
- dirty read: 発生しない
- nonrepeatable read: 発生しない
- phantom read: 発生してしまう (*MySQL InnoDBではネクストキーロックの仕組みによって発生しない)
- lost update: 発生してしまう
- serializable: 完全に直列で処理する。
これらのトランザクション分離レベルはデータベースの設定で切り替えることができ、上記一覧で最初に記述したものほど並列度が高く、最後のものが処理としてより直列になる。 つまりトランザクション分離レベルはデータベースにおけるACID特性のうちのIsolation(I)をどこまで厳密にやるかを規定したものと言える。
最初に話したように直列で処理することは処理のスループット低下に繋がるので、データベースを利用する際にどこまで並列度(スループット)を求めるかあるいはデータの整合性を求めるかはトレードオフの関係であり、それはデータベースを利用するアプリケーションに求めるものや特性によって異なる。
ところでACIDのIsolationの説明として、「実行中のトランザクションは(他のトランザクションから)参照出来ない」とありこれは比較的イメージがしやすいと思う。この他に「実行中のトランザクションは(他トランザクションが)変更出来ない」ともある。これはどういう意味だろうか?
これはあるトランザクション(トランザクションaとする)は他トランザクションから影響を受けないということを意味し、実行中のトランザクションaから見ると他のトランザクションは実行されておらず存在していないものとして、トランザクションa実行中は他トランザクション要因のデータの変更が全く無いように振る舞うということを意味する。(その実行中のトランザクションa自身による変更は見える)
つまりトランザクションが開始されるとその開始時点のデータで固定される(データの変更が停止される)ように振る舞うことを意味する。(厳密にはトランザクション開始時点か、そのトランザクション中の最初のselect時点か、どちらのタイミングかという話もあるがそれは別途解説する)
Anomalyへの対応
Anomalyへの対応つまりそれぞれのトランザクション分離レベルをどのように実現しているかの具体的な方法論は以下のようなものがある。その詳細、特にMVCCについては別記事にまとめる予定。
- 楽観的同時実行制御: トランザクションの競合はめったに起こらないという前提でロック取得による制御は行わない。その代わりに、トランザクション中に読み取った値の変更があった場合や他トランザクションの変更をさらに上書きしてしまうような場合には競合が発生したとしてトランザクションを失敗扱いにしてabortさせる。
- スナップショット分離: unrepeatable readを解決してrepeatable readを実現するための手法。とあるトランザクションはそのデータをデータベースの一時点の一貫性がある(変化しない)スナップショットから読み取る。つまりトランザクション毎に同じスナップショットを見るということ。この方法により、あるトランザクションからのデータの読み取りに際してはロックを必要とさせず一貫した値を参照出来る。ロックを必要としないということは並列度やスループットを上げることに繋がる。
- MVCC: ロックを取らずに(ロック待ちを発生させずに)一貫性を保ちつつ並列度を上げるための仕組み。このMVCCは上記スナップショット分離を用いて実現されている。
- 悲観的(保守的)同時実行制御: トランザクションの順番(タイムスタンプ)を基準に同時実行制御を行いロックを使わない方法と、ロックを使う方法の二種類がある。