はじめに
こんにちはYamatoです。
この記事はYamato Advent Calendar 2023 2日目の記事です。
前回に引き続きデータベースの深掘りを行なっていきます。
今回はデータベースのトランザクションやスケーラビリティについてまとめようかなと思います。
トランザクション
トランザクションは開始して終了するまでの一連の流れをまとめたものです。
トランザクションはその一連の処理を「確定させる (Commit)」か「取り消す(RollBack)」かで終了します。
トランザクションで重要な概念がACID特性ではないでしょうか?
ACID特性
これは、トランザクション処理に求められる4つの特性をまとめたものです。具体的には以下の通りです。
Atomicity(原子性)
これはトランザクションの操作が「全て実行される」か「一つも実行されない」かのどちらの状態にしかならないという性質です。
「commit」や「rollback」は原子性を担保するための仕組みです。
具体的に、送金の処理をもとに考えて見ましょう。
登場人物にAさんとBさんがいるとします。
AさんがBさんに10000円送金する時を考えます。
この時
「Aさんの口座から-10000円」「Bさんの口座に+10000円」を両方行う
or
「Aさんの口座から-10000円」「Bさんの口座に+10000円」を両方行わない
のどちらかになります。
つまり、Atomicity(原子性)は、一連の処理をやるかやらないかのどっちかにしてねという性質です。
Consistency(一貫性)
これはトランザクションの前後で、整合性が保たれ、矛盾のないデータになっていることを保証する性質です。
DBには「NOT NULL制約」や「一意制約」などの制約が存在し、データの一貫性を保つことができます。
具体的に、送金の処理をもとに考えて見ましょう。
登場人物にAさんとBさんがいるとします。
AさんがBさんに10000円送金する時を考えます。
もし、Aさんの所持金が9000円だった時、10000円を送ることはできませんよね?
所持金が-1000円になるのはおかしいです。
また、数字以外の金額を送ることもできませんよね?
例えば、AさんがBさんに対して、AAA円送るなんてこともできません。
このように、Consistency(一貫性)はデータに矛盾が生じないようにしてねという性質です。
Isolation(独立性)
これはトランザクション実行中に、その処理が他の処理に影響を与えないという性質です。
排他制御などで実現されています。
具体的に、送金の処理をもとに考えて見ましょう。
ここでは、登場人物にAさんとBさんとCさんの3人いるとします。
AさんがBさんに10000円をCさんがBさんに5000円を送金する時を考えます。
ここでは、二つのトランザクションが発生しています。
①「Aさんの口座から-10000円、Bさんの口座に+10000円」
②「Cさんの口座から-5000円、Bさんの口座に+5000円」
の二つのトランザクションです。
最終的にどのようになるかというと、
Aさん:-10000円
Bさん:+15000円
Cさん:-5000円
となります。
さてこの時、トランザクション①とトランザクション②の実行順番を入れ替えてみるとどうなるでしょうか?
そうです。変わりません。
トランザクションは他に影響を与えることがなければ、他から影響を受けることもありません。完全に独立しています。そのためトランザクションの実行順番に関係なく同じ結果になります。
つまり、Isolation(独立性)は処理が他に影響を与えないようにしてねという性質です。
Durability(永続性)
これはトランザクションの完了したデータは保存され、永続化されるという性質です。
バックアップやログをとっておくことで実現します。
登場人物にAさんとBさんがいるとします。
AさんがBさんに10000円送金した後のことを考えます。
「Aさんは-10000円」「Bさんは+10000円」の状態になっているはずです。
システム障害などが起きようとこの状態は変わりません。
つまり、Durability(永続性)は処理が完了したらその状態を永遠に保持するという性質です。
ロック
トランザクションとACIDについて概要をつかめたところで、次はロックについて見ていきましょう!
先ほど例で扱ったものを図で書き起こしてみます。
Aさんの口座には10000円あり、Bさんに10000円送金する処理を考えます。
更新処理をトランザクションを用いて行うため、両方の更新が必ず成功しないといけません。
では、A,B,Cさんの送金処理を考えてみます。
口座Bに対して、同時に更新を試みたとき、口座Bには20000円増えるはずですが、片方しか増えないと言うことが起こり得ます。このように同時更新の際にデータの衝突を防ぐ仕組みとして、ロックが存在します。
ロックには2種類存在します。「排他ロック」と「共有ロック」です。
排他ロック
排他ロックは変更できるトランザクションを1つまでに制限します。
排他ロックは、「これからこのデータを変更するので、他のトランザクションは変更しないでね」ということを表します。
共有ロック
共有ロックはトランザクション内で読み取りを行なっていることを示すためのロックです。
共有ロックは、「今このデータを読み取っているので、他のトランザクションは変更しないでね」ということを表します。
排他ロックは、トランザクション実行の順番を担保するロックなので、複数のトランザクションで取得することはできません。
ただし、共有ロックは複数のトランザクションで取得可能です。
ロックの取得
SQLでロックを取得するには、以下のような方法が挙げられます。
- select句で
for update
やfor share
などの読み取りロックを使用する -
update
やdelete
を行う
これらのロックはトランザクション終了時に解放されます。
具体例の改善
先ほどの同時更新の例をロックで改善してみます。
更新を行うため、排他ロックをとることにします。
このようにロックを使用することで、トランザクションの実行順序を担保し、更新衝突が起きないようにすることができました。
デッドロック
排他ロックなどのロックシステムを使用して、正しく更新処理を行うことができましたが、ロックを使用する際に別の問題が発生する可能性があります。それがデッドロックです。
デッドロックは、複数のトランザクションが互いにトランザクションが保持するロックを待機して処理が進まなくなってしまうことを指します。
デッドロックは、共有ロックと排他ロックが同時に使われたり、複数の排他ロックが使われたりする場合に生じます。
デッドロックにも2種類あるようで、変換デッドロックとサイクルデッドロックです。
変換デッドロック
共有ロックと排他ロックで起こるデッドロックです。
Tx1とTx2がそれぞれ共有ロックを取り、その後に排他ロックを取ろうとした場合にデッドロックが生じます。
サイクルデッドロック
複数のトランザクションがそれぞれ別のデータの排他ロックをとり、その後に相手が持つデータの排他ロックを取ろうとすることで生じるデッドロックです。
デッドロックを防ぐには
デッドロックは共有ロックを取得したデータを更新したり、トランザクションの更新順序が一定でなかったりすると発生するので、更新する際は排他ロックのみをとるようにしたり、更新順序を一定にすれば回避できると言うことです。
デッドロックに関して以下の記事が大変わかりやくす勉強になりました。
参考文献
今回も大いに参考にさせていただきました。ありがとうございます。
まとめ
- トランザクションとACID特性についてまとめました
- ロックには排他ロックと共有ロックが存在する
- デッドロックについて
- 変換デッドロックとサイクルデッドロック
本来はならDBのスケーラビリティについて書こうと思ったのですが、力尽きてしまったのと、トランザクションと一緒に書くべきことではないという理由で次回以降に持ち越させてください🙇♂️
明日のYamato Advent Calendar 2023は「Career Reflection for Gophersに参加しました」です。イベントの参加記を書きます!