背景
トランザクションの分離レベルで出てくる用語がぱっと頭に浮かぶよう、問題が発生するケースと対応方法をまとめます。
起きうる問題
基本的にどのDBも単一オブジェクトの原子性と分離性は保証します。
つまりデータ送信の途中でネットワーク接続が切れたら断片のみ保存するのではなく、全て破棄します。
また同時に更新処理があったとしてもデータを混ぜて保存するといったことはありません。
({id: 1, name: alice}
,{id: 1, name: bob}
があったとして{id: 1, name: aliob}
にはならない)
分離性で問題になるケースは主に複数のオブジェクトを並行で同時に操作する際に起きます。
ダーティリード
あるクライアントが他のクライアントのコミット前の書き込みを読み込むことです。
この例ではBobは新規メールがあるにも関わらず、未読件数は0のままです。
対応策
Read Committed以上の分離レベルであれば生じません
ダーティライト
あるクライアントが他のクライアントのコミット前の書き込みを上書きしてしまうことです。
この例では商品の購入者がBobなのに請求はAliceに行くという状態になってしまっています。
対応策
Read Committed以上の分離レベルであれば生じません
読み取りスキュー
クライアントが異なる時間から見たデータベースのデータを取得することで起きる矛盾です。
この例では銀行の送金で本来合計は変わらないはずなのに、Aliceから見た合計は900になっています。
特にDBのバックアップなどで起きやすい(バックアップ中も書き込み操作が入るので)です。
Nonrepeatable Read
先程の読み取りスキューは一般的な例ですが、それ以外にも同一行を読み取るケースでも起きます。
このように別のトランザクションによってAliceのトランザクション内での読み込むデータが変わってしまっています。
Phantom Read
また何らか集合を取得(検索)している際に、別のトランザクションが行を追加して行数が変わってしまうケースもあります。
対応策
スナップショット分離レベルであれば生じません。
更新のロスト
2つのクライアントが並行してアプリケーションが何らかの値をDBから読み取り、その値を変更して書き戻す場合(read-modify-writeサイクル)に生じます。
この例では1つ目の変更が2つ目の変更によって失われてしまいます。
対応策
更新のロストの対応策としては
- DBが提供するアトミックな書き込み
- 悲観ロック
- 楽観ロック
- CAS(Compare And Set/Swap)
があります。
分離レベルでいうと直列化可能(Serializable)であれば解決できます。
書き込みスキュー
トランザクションが何らかの値を読み取り、その値に基づいて判断をし、結果をDBに書き込む際に起きます。
判断の部分が他のクライアントの書き込みにより真でなくなってしまう場合です。
この例では当直が最低1人は必要、という条件で、2人以上いる場合は当直を外れる事が可能です。 しかし並行して処理が起きた際に当直が0人になってしまっています。
これはダーティライトでも更新のロストでもありません。 なぜなら2つのトランザクションが更新しているのは別々のオブジェクトだからです。
対応策
直列化可能分離レベルであれば解決できます。
分離レベル
Read Committed
Read Committedは以下を保証します
- ダーティリードが生じない
- ダーティライトが生じない
コミットされるまでAliceが見るデータは古い状態のままです。
AliceのトランザクションがコミットされるまでBobはトランザクションを待たなければいけません。
課題
一方で以下の問題を回避してくれません
- 読み込みスキュー
- 更新のロスト
- 書き込みスキュー
スナップショット分離
リピータブルリード(repeatable read)とも呼ばれます。
読み取りのみを行うトランザクションにとって有益な分離レベルです。
Read Committedでは解決されない読み込みスキューに対しての解決策になります。
原則として以下があります。
- 読み取りロックはない(読み取り同士のブロック)
- 書き込みロックはある(書き込み同士のブロック)
- 読み取りが書き込みをブロックすることはない
- 書き込みが読み取りをブロックすることはない
MVCC(Multi-Version Concurrency Control)
スナップショット分離の実装の1つにMVCCがあります。
各トランザクションが単調増加なトランザクションIDを持ち、DBへ書き込む際にデータにcreated by
、deleted by
が記載されます。
そして後から発行されたトランザクションIDによる書き込みを全て無視することで一貫したスナップショットを見ることができます。
直列化可能(Serializability)
2PL(2 phase lock)
2PLはトランザクションの実行中(フェーズ1)で各オブジェクトに対してのロックを取得し、トランザクションの終了時点(フェーズ2)ですべてのロックをリリースします。
ロックの種類として
- 共有モードのロック(読み込み時に使われる)
- 排他モードのロック(書き込み時に使われる)
の2種類があり、それらのブロックする関係は以下です。
ロックタイプ | 共有ロック | 排他ロック |
---|---|---|
共有ロック | X | |
排他ロック | X | X |
つまりスナップショット分離と異なり
- 書き込みは他の読み取りをブロックする
- 読み取りも他の書き込みをブロックする
ため、各トランザクションが直列に実行されることで分離性を保証します。
ただし大量のロックが発生するので以下のような問題もあります。
- パフォーマンスが良くない
- デッドロックが生じやすい
順次実行
Redisのように単一スレッドで順次トランザクションを処理していくやり方です。
そもそもトランザクションの並行性を排除することで回避しますが、スケーラビリティの面で後々困ることにもなります。