トランザクションと同時実行制御
トランザクションはDBを触る人なら聞いたことがあるだろう。
しかし、それらは大抵DBMSが内部でゴニョゴニョして複雑なことを単純に表現してくれている。
これはDBに限らず大抵の基盤的システムに言えることでもある。
普段は見ることがないけど、それを気にしてみるとその基盤に乗っているもののことを理解しやすくなることがある。
ここではSQLを理解する上でトランザクションという普段はみないものを見てみる。
セッション
ユーザセッションともいうが、これはDBを使うときにパスワードなどを使って接続すると確立されるもので
いったんセッションが確立されるとユーザはその上で権限の範囲でならあらゆるアクセスが可能になる。
そしてこのセッション中では0個以上のトランザクションを実行することができる。
トランザクションとACID特性
トランザクションが必要とする特性がある。それは頭文字を取ってACID特性よぶ
- 原子性 (Atomicity)
- 一貫性 (Consistency)
- 独立性 (Isolation)
- 耐久性 (Durability)
原子性
原子性は、トランザクション全体が永続的な状態であるかそうでないかという2つの状態のどちらかをとることを指す。つまり何かというと、トランザクションが完全に実行されるかされないかの2つの状態しかないということ。
SQLではCOMMIT文が実行された時に永続化され、ROLLBACK文はトランザクションを破棄しそれが始まる前の状態に戻す。これらは明示的に実行される場合もあるしDB側で処理している場合もある。
よく起こるパターンとして、SQLが何らかのエラーを吐いた時にROLLBACKが行われる。また原子性に従うので、仮に100万行をINSERTする場合に1行でも整合性制約に反していれば全てのINSERTが拒否されて自動的にロールバックされる。
このようにひとつの長いトランザクションを利用する場合はいくつか考慮することがある。
というのも、原子性に従う場合に長いトランザクションは少しのエラーでロールバックされてしまう可能性が捨てきれない。逆にトランザクションの粒度を細かくするとトランザクションとトランザクションの間に別の予期しない処理が走ってしまい思わぬ結果になってしまう場合が考えられる。
これを防ぐために、SAVEPOINTというものがある。これはトランザクションの最中に設定することで限定的なロールバックを行うものである。これによりトランザクションの粒度を細かくせずに長いトランザクションを利用することができる。
一貫性
これはトランザクションが開始されたときに、DBは全てのデータ整合性制約や参照整合性制約などを満たしている状態にあるということを指す。
ただし、トランザクション実行中は一貫性が保たれているということは保証されていない。
独立性
これは非常に簡単で、トランザクションは他のトランザクションから独立し、いかなる影響も受けないという性質のことである。これはどういうことかというと、トランザクションは並列に実行しようと直列に実行されたときと同じ結果になるという意味で別名として直列性と言われる。
このあたりはトランザクションの分離レベルで触れる。
耐久性
これはDBのプログラム自体が破損したとしても、DBそのものは保全されるという性質。この性質が満たされている場合、DBは破損したとしても一貫した状態に復元することができる。ロギングとバックアップの機能がこれらを担保している。
同時実行制御
同時実行制御というのは複数のユーザが同時にDBを利用する環境でいかにトランザクションを独立させて処理するかという問題を解決するための手段である。
5つの現象
SQLが全て検索などのREADである場合は無条件にACID特性が満たされているが2人以上が同時にデータを更新作成する場合にはACID特性を満たすように動作させる必要がある。
しかし、実際には5つのパターンにおいてトランザクション同士が干渉してしまう場合がある。
- P0(ダーティーライト):トランザクションT1がデータを変更するときに、別トランザクションT2がT1完了以前に同じデータを変更してしまう場合に不整合が起きる。この場合、どちらかのトランザクションがロールバックすると正しいデータが何か不明確になってしまう。これは一貫性を損なう。
- P1(ダーティーリード):トランザクションT1がデータを変更し確定していないときにトランザクションT2がそのデータを読んでしまうこと。
- P2(ファジーリード):トランザクションT1がある行を参照する場合にそのトランザクションが終了する前にトランザクションT2が変更を加える。この状態でT1がロールバックするとT2の変更がかかったあとで変更後の値をとるか行そのものが存在しない。
- P3(ファントムリード):トラザクションT1が検索結果としてN行の行を読みだす。トランザクションT2がその条件に合う行を挿入すると、同じ条件でも前に実行された結果とは異なる結果が取得されること。
- P4(ロストアップデート):トランザクションT1がデータを読みだし、トランザクションT2がそのデータを更新する。その後、T1がデータを更新するとT2の更新は失われる。
この様に、5つのパターンでトランザクションは干渉する。しかし、これらが全て悪いわけではなくDBを利用するサービスによっては起こることも容認する場合がある。それに全てのパターンを防がないとするとDBは高速に動作する。
そのため実際には分離レベルを考慮してDBを設計する必要がある。
分離レベル
分離レベルとはトランザクションがどの程度干渉を受けるかを設定するのに設定することが多い。
分離レベルは以下の種類がある。
トランザクション分離レベル | P1(ダーティリード) | P2(ファジーリード) | P3(ファントムリード) |
SERIALIZABLE | o | o | o |
REPEATABLE READ | o | o | x |
READ COMMITED | o | x | x |
READ UNCOMMITED | x | x | x |
SERIALIZABLEは一般的にデフォルトで設定されている。このレベルでは複数のトランザクションが並列に実行されても直列で実行された場合と結果が変わらない。
REPEATABLE READでは同じセッションの間では同じ読み取り結果が保証される。
READ COMMITEDでは同じセッション間でも他のトランザクションが変更しコミットしたデータが読み出される。
READ UNCOMMITEDでは同じセッション間において他のトランザクションがコミットしなくても、変更したデータが見れる。
ただし、分離レベルによらずSQLの文の実行以外では全ての現象は発生しないように動作する。
また何故この3つのパターンのみなのかというと、オリジナルのANSIモデルではこの3つのパターンしか定義されていないからである。
悲観的な同時実行制御
悲観的な同時実行制御というものは、トランザクション同士は衝突するという前提の元に設計される同時実行制御のことを指す。
このような場合は必ずロックを使って排他的アクセスを許可する。ロックメカニズムには色々とありDB2 for z/OSではラッチという仕組みを使って、DB全体をロックしてしまうのではなくテーブルレベルでロックする。こうすることでテーブル全体を対象にするトランザクションではひとつのフラグしか利用しない。
さらに、テーブルを行レベルでロックすれば他のユーザはそのテーブルの他の行を利用することができる。ただしこれは効率を優先するがパフォーマンスは低下してしまう問題を抱えている。
テーブルロックと行ロックの間にあるのがページロックで、これは行のサブセットに対してロックをかける。これは物理ストレージにおいてテーブルがページという単位で実装されていることが多いからこういう名前になっている。
スナップショット分離と楽観的同時実行制御
楽観的同時実行制御はトランザクション同士がそこまで頻繁に衝突しないということを前提に設計されている同時実行制御を指す。基本的には問題が起きてから対処しようという方針になる。これを実現するのにスナップショット分離というものがある。
スナップショット分離では。それぞれのトランザクションはデータをトランザクションが開始された時点のコミット済みのデータのスナップショットから読み込む。この仕組みによりトランザクションは読み込みにおいてブロックされることは無い。ただしこの仕組みによりトランザクションによって作られた複数のバージョンを持つ可能性がある。
実際にはどういう仕組みになっているかというと、トランザクションT1はコミットする準備ができるとコミットタイムスタンプを取得する。これは全ての既存の開始タイムスタンプとコミットタイムスタンプより後の時刻になる。
トランザクションT1のコミットが成功する場合はT1の実行期間にT1が書き込もうとしたのと同じデータを書き込むためのコミットタイムスタンプを取得したT2がない場合になる。つまり、コミット早いもの勝ち戦略によってP4を防止できる。
論理的な同時実行制御
論理的な同時実行制御というのは、SQL文を互いに比較・分析してどのSQL同士であれば同時に実行することが許されるかどうかという観点において設計された同時実行制御のことを指す。
SELECT文であれば、単純な処理になりやすいがそれ以外にデータを操作するSQLの場合は非常に複雑な処理を必要とする。一見すると完璧に見えるかもしれないが、待ち時間の長いSQLをどのようにして処理していくかが次のライブロック問題を引き起こす原因となる。
これを回避するのにもっとも簡単な処理は優先度をつけることだがそれを行うと、重要度の異なる処理を割りこませるという処理が必要になる。これをしてしまうと、優先度が低いが待ち時間の長いSQLが本当に重要なSQLの前に割り込まれてしまうなどの問題を抱えることがある。
デッドロックとライブロック
同時実行制御において真っ先に思いつく困ったこととして、デッドロックがある。これはSQL同士が互いに必要とするリソースを確保してしまい相手にとって必要なリソースを開放できないという問題である。こうなってしまうと、片方のセッションを管理者が切断してロールバックするしか無い。
またライブロックという問題も起こることがある。これはひとつのセッションでリソースを専有する場合に他のセッションがそのリソースの開放を待ち、開放を待っている側がなかなか実行されないという問題である。これも起こってしまうとセッションを切ってロールバックするのが一般的な解決手法としてあげられる。
しかし、いつまでも管理者を待つこともできないのでセッションごとに優先度を決めて実行する場合も存在する。
おわりに
今回はトランザクションと同時実行制御について書いたが、これは普段DBAが責任をもつ領域でアプリケーションなど比較的高いレイヤーではあまり考えない分野になっている。
しかし、これを知っておくのはトラブルの際に役立つし何より学んでいて楽しい部分ではある。