DB Transaction
Overview
トランザクションについてのセクション
更新は単独のクエリで行われることは少なく、複数のクエリで連続的に行われることがほぼ。
更新の元データとしてSELECTを利用する場合は、それを含めて複数のクエリを一貫性のある形でひとまとまりにして扱う必要がある。
トランザクションとは、このような複数のクエリをひとまとまりにしたもの。
実験した内容
2つのウィンドウを立ち上げる。そして片方で insert
し COMMIT
もう片方もトランザクションがはじめており、その場合は select
してもコミットの内容が見えない。トランザクションが終了してから見える。
DDLによる暗黙のコミット
MySQL
や Oracle
では CREATE TABLE
のようなDDL発行時に、暗黙のコミットが発行される。
そのため、1つのコネクションで行われ た CREATE TABLE
が成功すると、それ以降は他のコネクションでも参照できるようになる。
自動コミットの設定
トランザクションの開始(「BEGIN TRANSACTION」や「START TRANSACTION」、「SET TRANSACTION」など)が明示的に指定されない場合に、トランザクションの区切りはどうなるかというと、次の2つのパターンがあります。
1.「1つのSQL文で1つのトランザクション」という区切りになる 2. ユーザがCOMMITまたはROLLBACKを実行するまでが、1つのトランザクションとなる
一般的なDBMSでは、どちらのモードも選択可能になっています。またデフォルト設定が自動コミットモードになっているDBMSにはMySQL、PostgreSQL、SQL Serverなどがあります。 今回の実習では①かつ②のAであったため、他のコネクションでもINSERT後、すぐにテーブルとデータが確認できたというわけです。
トランザクションの特性(ACID特性)
4つの特性により定義されACID特性と呼ばれる
- Atomicity(原子性)
- Consistency(一貫性)
- Isolation(分離性もしくは隔離性、独立性)
- ロック制限
- 表全体
- ブロック単位
- 行単位
- シリアライザブル(DBMSの仕様)
- ダーティリード(ANSIが定義)
- リピータブルリード(ANSIが定義)
- ファントムリード(ANSIが定義)
- ロック制限
- Durability(持続性)
Atomicity(原子性)
「原子性」とは、データの変更(INSERT/UPDATE/DELETE)を伴う一連のデータ操作が「全部成功」するか「全部失敗」するかを保証する仕組み
トランザクションではすべての処理の後に COMMIT
を発行して処理を確定する。
その場合にそれぞれのデータ操作は永続的となり、結果が失われないようになる。
また、このようにシステムが正常動作していた上でのエラーではなくクライアントからサーバへの通信が途絶えてしまったり、サーバがダウンしてしまった場合でも、COMMITされたもの以外はROLLBACKする仕組みになっている
Consistency(一貫性)
データベースには、データベースオブジェクト(テーブルをはじめとするデータベース内に定義できるオブジェクト)に対して、各種整合性制約を付加でき る。
これは、一連のデータ操作の前後でそれらの状態を保つことを保証する、すなわち「一貫性」を保つための仕組み
たとえばシステムに利用ユーザを登録する場合、そのユーザを一意に識別するために、連番をユーザに振って(ユーザ番号)、それに対してユニーク制約(一意制約)を設定した場合、重複するようなユーザ番号を格納することはできない。
これは複数のユーザが同時にユーザ番号を取得しようとした場合も同じ。
Isolation(分離性もしくは隔離性、独立性)
トランザクション分離レベルと言われているもの
「分離性(隔離性、独立性)」とは、一連のデータ操作が、複数のユーザから同時に行われる際に、「それぞれの処理が矛盾なく行えることを保証する」こと。
ロックの単位には「表全体」「ブロック単位」「行単位」などがありますが、MySQLでトランザクション処理を行う場合は、主に行単位のロック機能を利用する。
具体的には、処理で SELECT〜FOR UPDATE
を実行することにより、SELECTした行にロックがかかる。
すると、後続の処理は、そのロックが解放される(COMMITまたはROLL BACK、今回COMMIT)まで待たされ、正しく処理を継続できるという仕組み
この仕組みに関してはDBのテーブル型が関わ ってくる
InnoDB
型のテーブルは「MVCC」という仕組みで動作しているため、今回の例でユーザBが単純に値を参照する、といった場合には、SELECTにFOR UPDATE
は不要であり、その場合読み取りはブロックされない。
そのため、更新するユーザが少数で、参照するユーザが多い場合には、ユーザの同時実行・並列実行性が高くなる
シリアライザブル(Serializable:直列化可能)
これを分離性のレベルとしてDBMS側で実装・提供しているものが「シリアライザブル(Serializable:直列化可能)」という仕様。
しかしながら、「シリアライザブル」の分離度では、常に同時に動作しているトランザクションは1つのイメージとなってしまい、パフォーマンス的に実用に耐えられない。
そのため、シリアライザブルから分離の厳格性を緩めて、シリアライザブル以外に、自分以外のトランザクションの影響を受けることを許容する3つのレベルが「ANSI」という規格団体によって定義されている。
それらの呼称をトランザクション分離レベルと いう
トランザクション分離レベルに至るまでの歴史
その当時の多くのデータベースでは、ロックをある単位(テーブル、ブロック、行)で取得する方式でトランザクションの分離性を担保していました。この場合、読込のタイミングによっては読込がブロックされる場合があり、例えば「不正確でもいいので、その瞬間の値のスナップショットがほしい」という場合でも、待たされてしまうことがありました。この場合、非コミット読取を利用すると、読込はブロックされず、その瞬間の大まかな値が知りたい場合には大変便利です。しかしMVCCを利用する場合、読取がブロックされることはないため、先ほどの例の場合でも非コミット読取が必要とされません。そのため、Oracle、PostgreSQLやFirebirdでは、非コミット読取はサポートされていないわけです。 MySQLも、それらと同様にMVCCをサポートしているため、通常非コミット読取を利用する必要はないわけです。
トランザクション分離レベル(Transaction Isolation Level)
※どの分離レベルをサポートするかはDBMSによって変わる場合もある。以下は MySQL
の場合
データベース管理システム(DBMS)の機能であり、DBレイヤーで制御されます。
ORM(Object-Relational Mapping)ツールやフレームワーク(たとえば、ActiveRecordなど)は、このDBレイヤーの機能を利用して分離レベルを指定し、管理しています。
トランザクション分離レベルはデータベースのトランザクションが他の同時実行トランザクションの影響をどの程度受けるかを定義する設定
データベース管理システム(DBMS)では、トランザクションの一貫性を保ちながら同時実行性を管理するために、異なる分離レベルが提供されている。
SQL標準では、4つの主要なトランザクション分離レベルが定義されています。それぞれの分離レベルは、データの一貫性とパフォーマンスのバランスを取るために異なるトレードオフを提供します。
トランザクション分離レベルは、データベースのトランザクションが他の同時実行トランザクションの影響をどの程度受けるかを制御します。分離レベルを適切に選択することで、データの一貫性とパフォーマンスのバランスを最適化することが可能。
- READ COMMITTED: ダーティリードを 防ぎますが、ファジーリードは防ぎません。
- REPEATABLE READ: ファジーリードを防ぎますが、ファントムリードを防ぎません。
- SERIALIZABLE: すべてのリードの問題(ダーティリード、ファジーリード、ファントムリード)を防ぎますが、もっとも制約が厳しく、パフォーマンスに影響を与える可能性があります。
- READ UNCOMMITTED(非コミット読み取り)
- 説明: 他のトランザクションがコミットしていない変更(ダーティリード)を読み取ることができます。あまり使われることがない
- 利点: 高い同時実行性とパフォーマンス。
- 欠点: データの一貫性が保証されないため、もっとも低い分離レベル。
- READ COMMITTED(コミット済み読み取り)
- 説明: 他のトランザクションがコミットした変更のみを読み取ることができます。ダーティリードは発生しません。
- 利点: データの一貫性がある程度保証される。
- 欠点: ファジーリード(同じクエリ内で異なる結果が返されること)が発生する可能性がある。
- REPEATABLE READ(再読み込み可能読み取り)
- 説明: トランザクション内で読み取ったデータが、トランザクション終了まで変更されないことを保証します。ダーティリードやファジーリードは発生しませんが、ファントムリード(範囲クエリが新しい行を返すこと)は発生する可能性があります。
- 利点: 高いデータ一貫性。
- 欠点: パフォーマン スが低下する可能性。
- SERIALIZABLE(直列化可能)
- 説明: もっとも高い分離レベルで、すべてのトランザクションがシリアルに実行されたかのように扱われます。ファントムリードも防止されます。
- 利点: 完全なデータ一貫性。
- 欠点: 同時実行性がもっとも低く、パフォーマンスに大きな影響を与える可能性。
各分離レベルは、以下の現象を制御するために使用される。
- ダーティリード(Dirty Read): 他のトランザクションがコミットしていない変更を読み取る。
- ファジーリード(Non-repeatable Read): 同じクエリを複数回実行した際に、異なる結果が返される。
- ファントムリード(Phantom Read): トランザクション内で範囲クエリを実行した際に、新しい行が挿入されることで異なる結果が返される。
使用例
たとえば、Railsでトランザクション分離レベルを指定する場合は、以下のように設定する。
この設定により、トランザクションは SERIALIZABLE
分離レベルで実行され、完全なデータ一貫性が保証されます。
ActiveRecord::Base.transaction(isolation: :serializable) do
# トランザクション内の処理
end
PostgreSQLでは、以下のようにトランザクション分離レベルを設定します:
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- トランザクション内の処理
COMMIT;
MySQLでは、以下のようにトランザクション分離レベルを設定します:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
-- トランザクション内の処理
COMMIT;
ダーティリード、リピータブルリード、ファントムリードをちゃんと理解してからトランザクション分離レベルを理解しよう
ダーティリード(Dirty Read)
あるトランザクションがコミットされる前に、別のトランザクションからデータを読み出せてしまう現象。
たとえばユーザAが値を変更し、まだコミットしていない場合でもユーザBが変更後の値を読み出してしまうことを指す。
ユーザAが部屋の残数「10」であるレコードを「9」に変更した場合、コミット前でもユーザBがSELECTした結果が「9」になる確定前の「汚れた(Dirty)」データを読み出し(Read)てしまうことから付いた名前
別のトランザクションでコミットされていないデータが読み取れる現象
- トランザクションAでレコードを①から②にUPDATE(未コミット)
- トランザクションBでレコードをSELECTする
- トランザクションAをロールバックする
- トランザクションBで取得したデータは②となっている。
一番低いレベルでないと発生しない。これを許容するシステムをまだ見たことがない・・
ファジーリード(非再現リード,ノンリピータブルリード):Fuzzy Read
あるトランザクションが以前読み込んだデータを再度読み込んだとき、2回目以降の結果が1回目と異なる現象。
たとえば最初にユーザAが部屋の残数「10」を読み出し、その後ユーザBが値を「9」に変更しコミットも行ったとする。
続いて、ユーザAがSELECTを再度実行すると、最初にSELECTできた「10」ではなく、変更後の「9」が読み出されてしまう。