トランザクション
トランザクションの背景
トランザクションの内容に入る前に、トランザクションが必要な背景をお話しします。
通常、Webアプリケーションからデータベースに対して大量の問い合わせ(SQL文)がきます。
すると、いくつか考えるべきことが出てきます。1つ目は、同じレコードが同時に変更されたらどうするか。つまり、競合状態をどう回避したら良いか? 次に、データベースの更新途中でシステム障害が起きたら、データベースをどんな状態に復旧したら良いか? 正しい状態とはそもそも何か?
前者の問題を解決する機能を同時実行制御、後者を解決する機能を、クラッシュリカバリと呼びます。この2つは、トランザクションが実装すべき重要な機能です。同時実行制御は、同時アクセスにより起こりうるデータの不整合を防ぐことが目的で、クラッシュリカバリの目的は、有事の際に、データベースを正しい状態で復旧することです。
トランザクションとは
トランザクションとは、アプリケーションがデータベースに対する複数の読み書きを論理的な単位としてまとめる方法です。下図では、預金の確認から1万円の引き出し、1万円の振り込みまでをトランザクションとしてまとめています。
DBMSはトランザクションを変更の単位として扱っています。なので、トランザクションの処理は、全体として成功するか中断(失敗)するかのどちらかになります。このシンプルさのおかげで、アプリケーションを簡単に実装することが可能になります。アプリケーションはトランザクションが失敗したら、エラーログを吐いて終わることもできるし、安全にリトライすることも可能になります。アプリケーションは並行性の問題を気にする必要がありません。
トランザクション in MySQL
MySQLでのトランザクションの書き方をみてみましょう。新しいトランザクションは、BEGINで開始できます。そして、いくつかのSQL文に続いて、COMMITまたはROLLBACKで終了させることができます。COMMITした場合は、DBMSがBEGIN以降に行われた全ての変更を永続化するか、変更に問題があれば中断します。ROLLBACKをした場合は、全ての変更が取り消されてなかったことになります。
ACID特性
次に、トランザクションが持つ特性、あるいはトランザクションが提供する安全性の保証と呼ばれるものを紹介します。
- 原子性(Atomicity):トランザクションに含まれる操作全てが成功か中断になる
- 一貫性(Consistency):トランザクションを実行した前後ではデータの一貫性が損なわれない
- 分離性(Isolation):同時実行している複数のトランザクションは互いに独立している
- 永続性(Durability):一旦コミットが完了したトランザクションによる変更は永続化される
原子性と永続性
原子性は、トランザクションの処理中に障害が起きた場合に、そのトランザクションが確実に中断され、それまでの操作が破棄されることを保証します。永続性は、トランザクションが一度成功したら、システム障害が起きようとそのトランザクションによって行われた変更は失われないことを保証します。
先ほどのトランザクションを例に、原子性と永続性がない場合に、何が問題になるかをみていきます。まず、原子性がないと、2と3の間で障害が起きた場合に、1万円が消えてしまいます。また、永続性がない場合はトランザクション終了後に、気づいたら1万円振り込んでいなかったことになったりします。
原子性と永続性の代表的な実現方法はロギングです。すべての操作ログを格納しておけば、トランザクション中断後、逆順に操作をやり直す(Undo)ことで、それまでの操作を無かったことにできます。コミット前にシステム障害が起きた場合は、データベース再起動時に、コミット済みだがディスクに未反映の操作を再実行 (Redo)すれば、正しい状態に復旧することができます。
分離性
分離性は、並行して実行されたトランザクション同士が互いに影響を与えないことを保証してくれます。分離性の目的は、並行性の問題をアプリケーションから隠すことです。分離性がないと、例えば、ダーティーリードと呼ばれる並行性の問題が起きる可能性があります。ダーティーリードとは、あるトランザクションAが、他のトランザクションBの未コミットな書き込みを読み取れてしまう現象のことを言います。BがロールバックされるとAは存在しないデータを読み込んだことになってしまいます。
分離性を実現するために、単純に逐次処理していくという方法も考えられると思いますが、現実には性能を考慮する必要があるので、実際はもう少し違うアプローチがとられています。実際の実現方法は、どういう並行性の問題を許すか、あるいは、トランザクション間でどの程度の影響を許すか(トランザクションの独立性)によって異なります。
この影響の程度にはいくつかのレベルが考えられており、分離レベルと呼ばれています。表はANDI SQL-92で定められている分離レベルと並行性問題の関係性をまとめたものです。
横軸に並行性の問題、縦軸に分離レベルをとり、表全体としてそれぞれの分離レベルがどの並行性の問題を許可/抑止するかを表しています。READ UNCOMMITEDが最も分離レベルが低く、SERIALIZABLEが一番高いです。
ダーティーリードは、ほとんどの分離レベルによって抑制されているのがわかると思います。せっかくなので、他の並行性問題がどういったものかも見ていきましょう。
ファジーリードとは、あるトランザクションAが読み出した行が他のトランザクションにより更新/削除され、トランザクションAで再度同じ行を読み込んだときに、その行が更新/削除される問題をさします。
例では、2度預金を出力する預金確認のトランザクションの間で口座振込のトランザクションが実行される様子を示しています。データベースがファジーリードを許容していると、1度目と2度目の預金出力の間で、Aさんの残高が減るので、同じトランザクション内の処理にも関わらず、2度目の出力値が変わってしまいます。
最後に、ファントムリードは、他のトランザクションの書き込みによって、あるトランザクション内で、同じ検索条件で読んでいるのに、あったはずの行が消えたり、なかった行が現れたりする問題です。ある検索条件を満たす行の集合を読み込んだ後に、他のトランザクションがその検索条件に合うような行を追加/削除するときに起こります。
例を見ると、トランザクション1は、10歳から30歳のユーザーを検索するクエリを2度行っていて、トランザクション2は、その間に新しいユーザーを追加しているのがわかると思います。ファントムリードを許容していると、ユーザーの検索条件は同じでも、2度目のクエリ結果が1人多くなることもあるということです。
ここで、MySQLの分離レベルを紹介します。MySQL 5.6(InnoDB)でサポートされている分離レベルはREAD UNCOMMITTED、READ COMMITTED、REPEATABLE READ(デフォルト)、SERIALIZABLEになります。実装にはMVCCやロックが使われています。
分離レベルが高くなればなるほど多くの並行性の問題を抑止することができます。しかし、その分、処理速度は落ちます。なので、データベースの多くは、そのトランザクションの独立性と処理速度のトレードオフのバランスをどうするかを、トランザクションの分離レベルで設定できる仕組みを提供しています。
一貫性
一貫性の保証とは、トランザクションを実行した前後ではデータの一貫性が損なわれないことを意味します。データベースの状態遷移から考えると、トランザクション開始前に、データベースが一貫性のある状態であれば、終了後は別の一貫性のある状態へと遷移することを指します(図参照)。データに変更はあるものの、一貫性が保たれているということです。一貫性を損なうような操作は処理されず、中断されます(一貫性は保たれたまま)。
一貫性のモチベーションという観点からもみていきましょう。多くの場合、データについて常に真でなければならない何らかの言明(不変性)が存在します。例えば、口座振込において、引出額と振込額は同じでなければならない(この例はいわゆるビジネスルールと呼ばれるものです)。つまり、一貫性はトランザクション前後でその不変性を常に満たすことを保証するものです。
しかしながら、DBMSはどんな不変性やビジネスルールがあるのかわからないので、保証のしようがありません。こういった理由から、一貫性はACIDの中で唯一のアプリケーションの特性と言えます。一貫性を保つようにトランザクションを適切に定義することはアプリケーションの責任です(ただし、例えば、外部キー制約などの一種の不変性はデータベースがチェック可能です)。
まとめ
本記事では、SQLの醍醐味でもあるJOIN結合と、トランザクションについて学びました。次回は、データベース設計について触れていきます。