今回トランザクションの実装を行う機会があり、その際に学んだ内容をまとめたものになります。トランザクションとはという概論から、ACID特性やコミット、ロック、分離レベルについて諸学者の視点から情報をまとめています。
自分が学んでいた際に、トランザクション・ACID特性・コミットやロックなどの階層構造がまとまっていないと感じたため、本記事では単に概要をまとめるだけでなく求められる性質とその実現手段という形で整理していきたいと思います。
この記事ではトランザクションと実現方法について具体例を交えて紹介しています。
対象読者としては以下を想定しています。
・トランザクションについて理解したい方
・トランザクションを既に学んだがコミットやロック、分離レベルなどがごちゃごちゃしていて整理がつかない方
注意点として以下の点をお気をつけください。
・専門家ではなく初学者のため誤りを含む可能性があります
・解釈に近い部分もあるため誤りがありましたらご指摘を頂けますと幸いです
目次
1. トランザクションとは?
2. ACID特性とは?
3. それぞれの実現方法は?
4. まとめ
5. 感想
1. トランザクションとは?
トランザクションとは、①データベースにおいて複数の処理をひとかたまりにしたもの、また②ひとかたまりとして実行する手法と捉えると分かりが良いです。
よく挙げられる例にはなりますが、Aさんの銀行口座からBさんの銀行口座に10,000円を考えてみましょう。
処理としては
1. Aさんの口座から10,000円減らす
2. Bさんの口座に10,000円加える
という2つが今回発生するものになります。
この時、もしもトランザクションを考慮せず単一の処理をそれぞれ行なってしまうと、例えばAさん口座からの減額が成功した後、Bさん口座の増額に失敗してしまったケースでは、Bさんへの送金は失敗しているにも関わらずAさんの口座からはお金がなくなっていることになります。
このようなバグを発生させないためにトランザクションやその管理が重要となってきます。
2. ACID特性とは?
トランザクションとセットで紹介されるものにACID特性があります。
「Aが何を意味していて、Cが何を意味しているか覚えにくいし定義だけ聞いてもイメージが湧かない」と感じられるかと思いますが(筆者は感じました)、「良いトランザクション管理をするにはACID特性を満たしておくと良いよね」みたいな、1つの基準として捉えておくと良いかと思います。
「良いトランザクションを実現するために、気にするべきポイントがACID特性だよ!」という理解です。 「上手に料理を作るためには、気にするべきポイントとして火加減、塩加減、に気をつけましょう!」みたいなイメージです(むしろこの例で混乱させてしまったらすみません)。
そのため、ACID特性以外にもBASEという可用性を重視したものもあるみたいです1。「上手な料理を作るためには、火加減、塩加減じゃなくて材料の産地にこだわるべきだ!」みたいな感じですかね。
以下ではさらに、ACID特性それぞれの内容と具体例を紹介していきます。
Atomicity(原子性)
Atomicityは日本語では原子性と表現され、「一連の処理について全部実行するか、全部実行しないかを保証してくださいね」というものになっています。やるならやる、やらないならやらないって感じですね。
それではなぜ原子性が求められるのかについてですが、先ほど「1. トランザクションとは」であげた例から明らかかと思います。10,000円の送金を行うために、10,000円の減額と10,000円の増額という2つの処理は全部実行するか、失敗したなら全部実行しないように気をつけないといけないですね。中途半端に片方だけ減らす、増やすと大事故です。
Consistency(一貫性)
Consistencyは日本語では一貫性と表現され、「データベースが整合性を保っているか」を意味しています。正直よく分からない(自分はよく分からなかった)ので例で見ていきましょう。
先ほどの銀行口座の例で同時にCさんがAさんの口座から10,000円を引き落としたいとしましょう(CさんはAさんの配偶者ということで)。このとき、実は銀行残高が10,000円しかなく両方の処理が実行されてしまうと残高がマイナスになってしまいおかしくなってしまいます。
このようにデータベースとしての制約やデータの整合性がトランザクション中に崩れた状態で確定されないようConsistency(整合性)が重要になります。
Isolation(独立性)
Isolationは日本語では独立性と表現され、「複数のトランザクションがお互いに干渉せずに処理を実行する」ことが求められています。こちらも具体例を見ていきましょう。
今回も銀行口座の例でいきましょう(いったんCさんのことは忘れてください)。
実は、AさんがBさんに送金する裏側でDさんがAさんに5,000円送金しようとしていたとしましょう。
つまり2つのトランザクションが発生していることになり、それぞれが2つの処理を含んでいることになります。
トランザクション1. AさんからBさんへの送金(②Aさん口座減額、④Bさん口座増額)
トランザクション2. DさんからAさんへの送金(①Dさん口座減額、③Aさん口座増額)
ここで、①Dさんの口座減額、②Aさん口座減額、③Aさん口座増額、④Bさん口座増額の順で処理が行われ、Bさん口座増額が失敗してトランザクション1がロールバック(ロールバックについては後ほど紹介します。ここではトランザクション1が失敗して、Aさん口座減額とBさん口座増額がなかったことになったと思ってください)した場合を考えましょう。
このとき、③のAさん口座増額は②のAさん口座減額によって変化した残高(元々の残高をx円として、x – 10,000円。今回はxを10,000円としてこの時点での残高は0円としましょう)をもとに5,000円を増額します(つまり残高は5,000円となります)。その後、④に失敗してトランザクション1がロールバックするため、Aさん口座残高は10,000円に戻ってしまいます。
つまり、DさんからしてみたらAさんへの送金は完了しているのに、Aさん視点ではDさんからみ送金になってしまうということです。
Durability(耐久性)
最後に、Durabilityは耐久性と訳され、要は「トランザクションの結果が障害が発生しても保持されるようにしてください」というものになっています。
またまた銀行口座の例になりますが、AさんからBさんへの送金が完了した後にデータベースがクラッシュしてしまい、送金処理がなかったとこになってしまったとしましょう。Aさんは送金が完了したと思っているわけですが、データ上では未送金となってしまい問題です。これが原因で支払いが遅れて利子を取られてしまったら大事件ですね。
以上がACID特性のそれぞれの内容になっています。原子性、一貫性、独立性、耐久性と言われてもいまいち何故それが必要なのかが分かりにくいですが(筆者は分からなかった)、具体例が少しでも理解の助けになっていると幸いです(逆だったらごめんなさい)。
次節ではさらにどのようにしてACID特性を実現していくのかについて紹介していきたいと思います。
3. それぞれの実現方法は?
それぞれの実現方法と書いてはいますが、ロックという概念は一貫性と独立性の担保に貢献しているといった形で必ずしも1対1で対応しているわけではないので、各手法を紹介しながらそれぞれがどの特性(ACID)の担保に貢献しているのかを紹介できればと思います(正直、筆者は自分で勉強しているときにここがよく分からなかったです。ロックやISOLATION LEVELなどを紹介されてもそれがACID特性とどう対応しているか不明瞭で混乱してしまいました)。
コミットとロールバック
コミットはトランザクション中の複数の処理が全て完了したタイミングで更新を確定させること、ロールバックはトランザクション中の複数処理が途中で失敗した場合に全てをなかったことにしてトランザクション開始前に戻すことを指します。要は、トランザクションについて全部やるか・一つもやらないかを徹底するための機能となっていて、ACID特製のうちAtomicity(原子性)を実現しています。
AさんがBさんに10,000円送金する例で考えましょう。
Aさん口座からの減額、Bさん口座への増額が完了した後(全ての処理が完了した時点)でコミットが実行されます。一方で、Bさん口座への増額が失敗した場合にはロールバックが実行されAさん口座からの減額もなかったことになります。
このようにコミットとロールバックによって、全部やるか・一つもやらないかが実現されているわけです。
ロック
ロックは、あるトランザクションが利用しているデータを他のトランザクションが利用できないようにする仕組みです(そのためロックが重要になるのは複数のトランザクションが同時に実行される場合です)。「今、私(トランザクションA)がこのデータを使っているから、私以外は使っちゃダメです」という感じですね。ではロックが具体的にどのようなときに役に立つのか例を見ていきましょう。
AさんがBさんに送金、DさんがAさんに送金するとしましょう。この場合、トランザクションは2つあり処理は4つあります。
トランザクション1. AさんからBさんへの送金(①Aさん口座減額、④Bさん口座増額)
トランザクション2. DさんからAさんへの送金(②Dさん口座減額、③Aさん口座増額)
ここで、①〜④の順序で処理のタイミングが回ってきたとして、ロックは①の段階で発生したとします。すると③の処理が来たタイミングではAさん口座のデータがロックされている(参照や変更ができない)状態になっているため、③は順番待ちに入り先に④が実行されトランザクション1が完了してロックが解除された後に③が実行され、トランザクション2が完了します。
もしもロックがない場合には、トランザクション2でAさん口座を増額した後に④の処理が失敗しトランザクション1がロールバックした際にDさんからの送金が反映されなくなってしまいます。
ロックがあるおかげで、④の処理が失敗した場合にも、ロールバックした後の金額を基に③の処理が実行されるため、整合性(10,000円とならず10,000 + 5,000 = 15,000円になることを担保する)や独立性(トランザクション間の干渉)を防ぐことができます。
ロックによってトランザクション間で不正なタイミングでの読み取りが防がれ、ConsistencyとIsolationが守られるというわけです。
ISOLATION LEVEL
ISOLATION LEVELは、複数のトランザクションがどれだけお互いに干渉しないかを定めたものになっていて、コミットやロールバック、ロックとは違いグラデーションのあるものになっています(ロックはするかしないかの二元論ですが、ISOLATION LEVELはどの程度データの不整合を許すかと処理速度のトレードオフから選択するものになります)。
今回も2つの銀行送金が発生する例で考えましょう。先ほどとは処理の順番を変えているのでご注意ください。
トランザクション1. AさんからBさんへの送金(①Aさん口座減額、③Bさん口座増額)
トランザクション2. DさんからAさんへの送金(④Dさん口座減額、②Aさん口座増額)
初めにAさん口座を減額(10,000円→0円)した後、②でAさん口座を増額します(0円→5,000円)。その後、③が失敗してトランザクション1はロールバックした一方で、④は成功してトランザクション2がコミットされたとします。すると、最終的にAさんの口座残高は5,000円になってしまいます。これは当初の金額から5,000円減っていることになりおかしいです。
一方で、ISOLATION LEVELを適切に設定すると、①の処理が行われたタイミングでAさんのデータがロックされ②の処理は待ちに入ることになります。トランザクション1がロールバックされたタイミングでようやく②が実行される(10,000円→15,000円)ことになるため、金額が正確に反映されたことになります。
このようにISOLATION LEVELを設定する(トランザクション間がどれだけ干渉するのか)ことで、それに応じた粒度でロックがかけられ、正確な処理が行われるという形になります。
あくまで、ISOLATION LEVELはトランザクション間の干渉レベルを定義したものになっていて、結果的にロックが設定されるという形です。なのでISOLATION LEVEL ≠ ロックの粒度の設定、となります。
チェックポイントとバックアップ
最後にチェックポイントとバックアップになります。これはどちらもデータベースの内容を保存することに値しますが、想定する障害のスコープが異なリります。
チェックポイントは、メモリ上の変更をデータベースのディスクにちゃんと反映してあげることを指していて、バックアップはそのディスクの内容をデータベースの外部に保存してあげるというものになります。イメージとしては、チェックポイントがプレイ中のゲームのセーブで、バックアップはセーブデータを念の為外付けハードディスクにも保存するみたいな感じです。
そのため対応できる障害にも違いが出てきます。データベースがクラッシュした場合はチェックポイントとWAL(データベースの操作履歴みたいなものです)をもとにデータを元の状態に戻し、データベース自体が完全に壊れた場合はバックアップから復旧するという形です。
4. まとめ
この記事では、
①トランザクションとは何か
②トランザクションをより良くするために意識するポイントとしてのACID特性
③ACID特性を実現するための具体的な手法
という構成でそれぞれを紹介してきました。
改めて表形式でまとめると以下のようになります(厳密性に欠けるかもですが、ざっくりとまとめています)。
議題 | 特性 | 具体的な手法 |
トランザクション | Atomicity (原子性) | コミット ロールバック |
Consistency (一貫性) | ロック Isolation Level | |
Isolation (独立性) | ||
Durability (耐久性) | チェックポイント WAL バックアップ |
5. 感想
今回、トランザクションに関する情報をまとめてみた感想としては
・歴史があって抽象度高くまとめられている内容だけに具体例がないといまいちピンと気にくい
・抽象度が高い分、解説されている内容が性質の話をしているのか、それを実装する手段の話をしているか、議題の抽象度が分かりにくい
と感じました。一方で、自分の中で噛み砕けてきたときには、トランザクションやその周辺の概念・用語が綺麗にまとまっていることにも感動を覚えました。
今回の記事では、ロックやISOLATION LEVELの詳細、実際のデータベース(MySQLやPostgreSQL)での設定・操作方法については割愛しているため、今後別の記事として紹介できればと思います。
コメント