トランザクションのACID特性とトランザクション分離レベルが不十分な場合の挙動

はじめに

この記事では、

データベースのトランザクションのACID特性について基本的なところをまとめ、

トランザクション分離レベルが不十分な場合の挙動について絵を使って例をまとめました。




SQL Serverで現在のトランザクション分離レベルの設定を、

理解しておらず、

負荷試験時にデッドロックが発生して、

ハマった経験を元に注意点や

アプリケーションエンジニアが理解しておく点をまとめています。




トランザクションのACID特性とは

OracleやPostgreSQL、MySQLなどはDBMS(DataBase Management System)と呼ばれ、

これらDBMSにはデータを更新している最中に障害が発生しても、

データに矛盾が起こらないようにするトランザクション管理機能を持っています。


まずは、

トランザクションのイメージを掴むために以下の図を見てください。


トランザクションのイメージ


ピンクの矢印の部分がトランザクションになります。

つまり、山田さんの口座振込全体が1つのトランザクションです。




このトランザクションにはACID特性といわれる、

トランザクションに求められる4つの特性があります。

次はこの特性について見ていきましょう。


原子性(Atomicity)

トランザクションは、完全に実行されるか全くされないかのどちらかである。

この性質が原子性です。

例えば以下の場合


振込の途中で障害発生


トランザクションを中途半端な状態(山田さんの口座から3万円引かれた状態)

で終了させてはいけない。

という特性が原子性です。




中途半端な状態で終了させないとは、

振込に関する処理の途中で失敗したら振込直前の状態に戻し、

振込に関する全ての処理が成功したら振込完了状態にする。

ということです。


一貫性(Consistency)

「トランザクションは、データベース内部で整合性が保たれなければならない。」

これは以下の例をイメージすると分かりやすいと思います。


残金のマイナス値は業務として規定された整合性を満たせていない


このようなマイナスの値を許容しないような制約を予め定めておいて、

制約違反させないようにデータを管理させる性質が一貫性です。


独立性(Isolation)

「トランザクションは、同時に実行している他のトランザクションからの影響を受けず、

並行実行の場合も単独で実行している場合と同じ結果を返さねばならない。」

これは以下の例をイメージすると分かりやすいと思います。


実行途中のトランザクションの影響を受けてしまう


上の例では山田さんの振込失敗(残高10万円のまま)で、

斎藤さんからの3万円振込成功なので山田さん口座は13万円になるべきです。

しかし、

実行途中の状態を別トランザクションが参照できるため、

山田さん口座は10万円になってしまいました。




実行途中の状態のトランザクションを別トランザクションから参照させない性質が独立性です。

この特性を実現するためにDBMSでは排他制御御機能があります。


耐久性(Durability)

「トランザクションの結果は、障害が発生した場合でも、失われないようにしなければならない。」

これは保存しているデータはシステムに障害が発生したり、ディスクが壊れても、

障害発生直前の状態に復旧できる性質です。

この特性を実現するためにDBMSでは障害回復機能があります。


トランザクションはここまで見てきた4つの要件を満たす必要があります。




トランザクションの制御と分離レベル

ここからはトランザクション制御と、

トランザクション分離レベルについて見ていきます。




トランザクションの制御には、

(PostgreSQLの場合の例)


BIGINでトランザクションを開始し、

COMMITでトランザクションの結果を反映、

もしくは

ROLLBACKでトランザクションの結果を破棄

(トランザクション開始直前に戻す)

して終了します。




BIGIN

山田さんの口座から3万円減算

田中さんの口座へ3万円加算

田中さんの口座への加算が失敗したらROLLBACK(で全て無かったことに)

田中さんの講座での加算が成功したらCOMMIT(で結果を確定して反映)


先ほどの例だとこんなイメージです。




トランザクション制御はこのようにできるのですが、

トランザクションが同時並行で複数実行されます。




独立性をどこまで高めるか??

(中途半端な状態のトランザクションを他トランザクションから見せるのか見せないのかなど)




トランザクションの分離レベルをどの程度に設定するか??

をデータベースを利用する際には決める必要があります。




システムの要件(とテーブル設計)によっては、

トランザクション分離レベルを下げて、

同時に複数トランザクションを実行しても問題ない場合もありますが、

逆に同時に複数トランザクションを実行することで、

先ほどの独立性の例のように問題になる場合があります。




ここからは、

トランザクション分離レベルにはどういうものがあって、

それぞれの分離レベルではどういう事象が発生してしまう可能性があるのかを見ていきます。


以前、

SQL Serverでトランザクション分離レベルが意図せず変更してしまっており(分離レベルを上げていた)、

急にデッドロックが発生しまくる事象が起きたことがありますが、

それだけトランザクション分離レベルの違いは

アプリケーションの作りやテーブル設計にも影響があるので、

アプリケーションエンジニアも理解しておくべき事だと個人的には思います。


ちなみに、SQL Serverでのデッドロック原因は、

内部で処理を効率化するためにページ単位でロックをかけることがあるのですが、

そのロックを元々の分離レベルではSNAP SHOPで行っていたものを

分離レベルを上げたことで直接ロックかけるように変わってしまって

SELECT同士でもデッドロックが発生・・・みたいなオチでした


それでは本題に戻って、

トランザクション分離レベルとそのレベルで発生しうる事象を見ていきます。


トランザクション分離レベルリードアンコミッテド(READ UNCOMMITTED)とダーティリード

READ UNCOMMITEDは先ほどの独立性で紹介した例と同じです。

トランザクション途中でも

別トランザクションからその状態が見えてしまうような、

一番低い分離レベル。


READ UNCOMMITTEDとダーティリードの例


コミット前の状態を読み込めてしまう事象をダーティリードと呼び、

恐らく、一般的なシステムを運用する際、

この分離レベルの設定でデータベースを運用することはほとんどないかなと。。。

PostgreSQLではこの指定をしてもREAD COMMITTED扱いとなるくらいです。


トランザクション分離レベルリードコミッテド(READ COMMITTED)と反復不能読み取り

一般的なシステムではこれを使うことが多く、

DBMSの初期設定がこの設定であることも多いです。


各トランザクションでは、

別トランザクションがコミットしたデータのみ参照するようになります。

READ UNCOMMITTEDのようにトランザクションの

中途半端な状態を参照しないため同時実行の制御がしやすいですが、




トランザクションを開始して、

トランザクションを終了するまでに、

別トランザクションがコミットしたものは常に見えるため、

反復不能読み取りやファントムリードは発生します。(この辺りは後述)


READ COMMITTEDと反復不能読み取り


トランザクション分離レベルリピータブルリード(REPEATABLE READ)と反復不能読み取り

反復不能読み取りとは、

ざっくりいうと、

トランザクションの間に別トランザクションが更新した後の状態が

selectで見えてしまう事象です。




これが、

トランザクション分離レベルREAD COMMITTEDでは発生するので、

アプリケーションとしてそれが致命的となる場合には

トランザクション分離レベルをREPEATABLE READに引き上げます。

そうすることで、

反復不能読み取りが発生してしまう場合はCOMMIT時にエラーとして防ぐことができます。


READ COMMITTEDと反復不能読み取り


トランザクション分離レベルシリアライザブル(SERIALIZABLE)とファントムリード

ファントムリードとは、

ざっくりいうと、

トランザクションの間に別トランザクションが新規登録した後の状態が

selectで見えてしまう事象です。




先ほどの反復不能読み取りが更新データだったのに対して、

こちらは登録データです。




トランザクションを開始(トランザクション内で同じselect文を2回発行)して

最初のselectでは2件しか取れなかったのに、

トランザクションの途中で他トランザクションで登録されたため、

2回目のselectでは3件取れてしまう。

みたいな事象をイメージしてもらえるとわかりわすいかも。




これが、

トランザクション分離レベルREPEATABLE READでも発生するので、

アプリケーションとしてそれが致命的となる場合には

トランザクション分離レベルをSERIALIZABLEに引き上げます。

そうすることで、

ファントムリードが発生してしまう場合はCOMMIT時にエラーとして防ぐことができます。


SERIALIZABLEとファントムリード


まとめ

今回はデータベースのトランザクションの特徴と

トランザクションの分離レベルについてまとめました。




アプリケーションエンジニアもこの辺りは理解して、

データベース周りの処理を書く必要があると個人的には思います。




その辺りの意識が足りていない場合、

性能試験や本番運用で

同時に大量のトランザクション実行され、

よくわからないところでデッドロックが発生したり、

逆に整合性が壊れるような更新をかることに繋がってしまいます。




現場で役立つデータベース周りのナレッジをまとめた記事を作ったので、

是非そちらも参照ください!!

初めてデータベースを触る方に向けて〜新人プログラマー時代の自分に伝えたいこと〜

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください