🐙

RDBにおける、共有ロック/占有ロック・2相ロック・トランザクション分離レベルの関係をシーケンスで説明する

2024/09/29に公開

概要

RDB(リレーショナルデータベース)において、ACID特性の一つである I (独立性)を保証する必要がある。
つまり、トランザクションを同時実行したときに、不整合が生じないように設計する必要がある。

トランザクションの同時実行制御に関する概念としては、以下が挙げられる。

  • 共有ロック/専有ロック
  • 2相ロック
  • トランザクション分離レベル

これらは代表的な用語だけあって、多くの記事で解説されているが、3つの関係性を説明する記事は少ない。

本記事の目的は、共有ロック/専有ロック・2相ロック・トランザクション分離レベルの関係性を解説・考察することである。

結論

いきなり結論だが、

  • トランザクション分離レベルは、SQLがサポートしている"トランザクションを分離するレベルの概念的な定義"である。つまり、それを実現する方法はDBMSの実装次第である。
  • 共有ロック/専有ロックと2相ロックは、具体的なロックの手法である。
  • トランザクション分離レベルは、共有ロック/専有ロック・2相ロックを用いて実現できる。
  • 2相ロックは、共有ロック/専有ロックを"どのように取得するか?"を規定しているプロトコルである。

ここからは、より具体的な関係性を見ていく。

用語集

本題に入る前に、復習も兼ねて簡単な説明を記載する。

詳細な説明については、多くの記事や文献があるため、そちらが参考になると思う。

用語 説明
共有ロック 読み取り専用のロック。他のトランザクションは、共有ロックが取得できるが、専有ロックは取得できない
専有ロック 読み書き専用のロック。他のトランザクションは、共有ロックと専有ロックのどちらも取得できない
2相ロック 2相でロックを取得・解除する方法。
(1相目)ロックを取得する。
(2相目)必要なロックを全て取得した後(1相目が終了した後)に、ロックを解除する。
トランザクション分離レベル 文字通りトランザクションを分離するレベル。次の(1)~(4)がある
(1)SERIALIZABLE 一番強いレベル。トランザクションを並列実行した場合でも直列実行した結果と同じになることを保証する。ファントムリードが発生しない。
(2)REPEATABLE READ 次に強いレベル。ノンリピータブルリードが発生しない。
(3)READ COMMITTED 3番目のレベル。コミットした情報しか読まない(ダーティリードが発生しない)
(4)READ UNCOMMITTED 何も制約を課さない状態。

本題

ここからは、4つのトランザクション分離レベルと各ロック方法の関係について説明する
説明しやすさのために、分離レベルの低い方から説明していく

  • (4)READ UNCOMMITTED
  • (3)READ COMMITTEDと共有ロック/専有ロック
  • (2)REPEATABLE READと2相ロック
  • (1)SERIALIZABLEと直列実行

(4)READ UNCOMMITTED

READ UNCOMMITTEDは何も制約をかけない。
つまり、単純に何もロックをかけなければよい(実際には、専有ロックは取得するらしい。参考

色々良くないことが発生するが、特にダーティリードが発生する。

ここに、簡単な例を示す:

  • Aさんの口座残高の初期値が500円
  • Aさんは100円を預け入れしたい
  • Bさんは100円を送金したい

dirty-read

結果、不整合が発生した。
(残高が増えたので、AさんにとってはHappyかもしれない)

(3)READ COMMITTEDと共有ロック/専有ロック

READ COMMITTEDを指定すると、ダーティリードが発生しなくなる。
これは、以下のようなロックをかけることで実現ができる。

  • 共有ロックをかける(読み取りが終わったら、すぐ解放してよい)
  • 2相ロックに従って、専有ロックをかける。

READ UNCOMMITEDで考えた例と同じ状況を考えてみる:

  • Aさんの口座残高の初期値が500円
  • Aさんは100円を預け入れしたい
  • Bさんは100円を送金したい

dirty-read-with-lock

今回は整合性が取れている。

(2)REPEATABLE READと2相ロック

READ COMMITTEDを指定すると、ノンリピータブルリードが発生しなくなる。
2相ロック(共有ロック/専有ロックの両方)をかけることで実現ができる。

別のケースで、ノンリピータブルリードを考えてみる。

まずは、READ COMMITEDでノンリピータブルリードが発生する場合:

  • Aさんの口座残高の初期値が500円
  • Aさんは300円を引き出したい
  • クレジットカード会社は300円を引き落としたい

non-repeatable-read

ちょっと得したのでHappyですね(?)

次に、同じケースで、REPEATBLE READ(2相ロック)でノンリピータブルリードが発生しない場合を考える:

  • Aさんの口座残高の初期値が500円
  • Aさんは300円を引き出したい
  • クレジットカード会社は300円を引き落としたい

repeatable-read

結果、デッドロックが発生した(あまり良い例ではなかったかもしれないが、ノンリピータブルリードは防げた)

この後は、いずれかのトランザクションがロールバックして、整合性が取れた状態になる。なお、ロールバックするトランザクションはデッドロック解除の方式に依る。

別の回避策

デッドロックが発生するのは、あまり良くない(というか例としてきれいでない)
別記事で読み取り時に専有ロックを取得する方法を考察する。

https://zenn.dev/neko_student/articles/7ee0c6f158e898

(1)SERIALIZABLEと直列実行

SERIALIZABLEは直列実行と同じ動きを保証する。
これは、表単位でロックを取得することで実現ができる。

(補足)
前節までは、行単位(銀行口座テーブルの口座A)のロックを考えていた。
SERIALIZABLEでは、口座Aに対する読み書きであっても、銀行口座テーブル全体にロックをかける。

特に、ファジーリードが発生しなくなる。
ファジーリードは行の挿入・削除により発生するが、表全体にロックをかけることで発生しなくなる。
(比較的理解しやすいと感じたので、シーケンスは省略する)

まとめ

ロック手法とトランザクション分離レベルの関連を説明した。
シーケンスで詳細を書いたので、個人的にも理解が深まった。

参考文献

GitHubで編集を提案

Discussion