まとめ
-
DBにおいてデータの整合性はきわめて重要
- Principle of Explosion定期
-
リレーショナルモデルだけではなくトランザクション理論も不可欠
- 分離レベル
- ロック
- MVCC
- 制約
トランザクション
-
データを正しく保つために考案された手法
- DBからは独立した理論体系
-
正規化理論と目的は同じだが着眼点は異なる
- 双方が必要
トランザクションの機能
-
下記のようなケースで生じうる不整合からデータを守る
- DBサーバに対して多数のクライアントから同時にアクセスが発生
- DBサーバあるいはアプリケーションが更新処理の途中でクラッシュ
- よく聞く例: 預金操作
-
機能
- 同時実行制御
- クラッシュリカバリ
トランザクションの鍵、スケジュール
-
同時実行に不整合を起こさないためには?
-
同時実行しない
- リソースを使い切れないので非現実的
- 同時実行しつつなんとかする
-
-
トランザクションとは、複数のデータアイテムの操作をまとめたもの
-
操作によっては他の操作に影響を及ぼす
- 特に書き込み処理
-
どのようにスケジュールを組めばデータの正しさを保証できるのかが課題
- どのような処理が互いに干渉するのか、しないのかを踏まえる
-
「データの正しさ」の定義
-
「直列化されたスケジュール」
- 個々のトランザクションを1つずつ順番に実行するスケジュール
-
データが正しいとは?
- 直列化されたスケジュールと同じ結果になること
- 【補】= SERIALIZABLE
- 「データが正しい」スケジュールは多数存在しうる
- その中から良い物を選択するのがスケジューラの任務
スケジューラの性能
-
性能項目
- いかに多くのトランザクションを並列化できるか
-
いかに低コストで最適なスケジュールを探し出せるか
- 組み合わせ最適化問題なので計算コスト甚大
- そこそこのコストでそこそこ良いものを得るのが現実解
トランザクションの特徴
ACIDとは
- これを満たせばRDBでなくても「トランザクションを実装している」といえる
原子性(Atomicity)
-
トランザクションの結果がCommitかAbortのいずれかになる性質
- Commit: すべての操作が成功
-
Abort: すべての操作が失敗
- SQLでは「ROLLBACK」
-
必ず成功することが保証されているわけではないことに注意
-
エラー処理(リトライ)を実装していないアプリケーションはトランザクションの使い方を本質的に間違えている
- ままある(残念)
-
一貫性(Consistensy)
- トランザクションを実行すると、ある一貫性のある状態から別の一貫性のある状態へと遷移
-
「一貫性のある状態」とは?
- RDBは知らない
- データに意味を与えるのはアプリケーション
分離性(Isolation)
-
同時に実行している複数のトランザクションが互いに影響を与えない
- 影響を与えないとは?
- 直列に実行した場合と結果が同じ
永続性(Durability)
-
いったんコミットが完了したトランザクションが消失しない
-
論理
- 確定したトランザクションが取り消されることがない
-
物理
- クラッシュリカバリ
-
さまざまな異常
- 具体的にどのような異常(Anomaly)があるの?
ダーティリード
TR1 | TR2 | データX |
---|---|---|
1000 | ||
BEGIN | ||
X読み取り: 1000 | ||
X更新: 1300 | 1300 | |
BEGIN | ||
X読み取り: 1300 | ||
ROLLBACK | 1000 | |
Xから200引く: 1100 | ||
X更新 | 1100 | |
COMMIT |
- あるトランザクションで未コミットのデータが他のトランザクションから読み取れてしまう
- 200引いたのに増えている!
インコンシステントリード
TR1 | TR2 | データX | データY |
---|---|---|---|
1000 | 1000 | ||
BEGIN | |||
X読み取り: 1000 | |||
Xから100引く: 900 | |||
X更新 | 900 | ||
BEGIN | |||
X読み取り: 900 | |||
Y読み取り: 1000 | |||
X+Y: 1900 | |||
COMMIT | |||
Y読み取り: 1000 | |||
Yに100足す: 1100 | |||
Y更新 | 1100 | ||
COMMIT |
- 100円送金する例
-
新旧データ(新:X,旧:Y)を読み取ってしまう不整合
- X+Y=2000でないといけない
ロストアップデート
TR1 | TR2 | データX |
---|---|---|
1000 | ||
BEGIN | ||
X読み取り: 1000 | ||
BEGIN | ||
X読み取り: 1000 | ||
Xに300加算: 1300 | ||
X更新 | ||
COMMIT | 1300 | |
Xから200引く: 800 | ||
X更新 | 800 | |
COMMIT |
- 更新が後勝ちしちゃうやつ
ノンリピータブルリード(ファジーリード)
TR1 | TR2 | データX |
---|---|---|
1000 | ||
BEGIN | ||
X読み取り: 1000 | ||
BEGIN | ||
X読み取り: 1000 | ||
Xから200引く: 800 | ||
X更新 | ||
COMMIT | ||
X読み取り: 800 | ||
COMMIT |
- 同じデータアイテムを複数回読み取った時に異なる結果が得られてしまう
ファントムリード
TR1 | TR2 | データX |
---|---|---|
1000 | ||
BEGIN | ||
件数読み取り: 1 | ||
BEGIN | ||
X追加: 2000 | ||
COMMIT | 1000,2000 | |
件数読み取り: 2 | ||
COMMIT |
- SQLなどのように範囲検索がある場合に起こる
- 他のトランザクションで追加されたデータを読み取れてしまう
スケジュールとロック
- 前述のスケジュールは実行してはいけないもの
-
ロッキングスケジューラ
- ポピュラー
-
ロックによる排他制御で前述のスケジュールを防ぐ
- 操作の対象となる行に対して、操作前にロックをかける
- 競合するデータアイテムへのアクセスを必要とするトランザクションはブロックされる
TR1 | TR2 | データX | データY |
---|---|---|---|
1000 | 1000 | ||
BEGIN | |||
Xをロック | |||
X読み取り: 1000 | |||
Xから100引く: 900 | |||
X更新 | 900 | ||
Yをロック | |||
BEGIN | |||
Yをロック | |||
ブロック | |||
Y読み取り: 1000 | |||
Yに100足す: 1100 | |||
Y更新 | 1100 | ||
COMMIT | |||
Yをアンロック | |||
Xをアンロック | |||
Y読み取り: 1100 | |||
Yから100引く: 1000 | |||
Y更新: 1000 | 1000 | ||
Xをロック | |||
X読み取り: 900 | |||
Xに100足す: 1000 | |||
X更新 | 1000 | ||
COMMIT | |||
Xをアンロック | |||
Yをアンロック |
デッドロック
-
両トランザクションが互いにブロックし合う
- 【補】ロックを「追加確保」すると生ずる
-
解消するためにはロールバックする必要がある
-
解消方法は実装依存
- 両方ロールバック
-
片方ロールバック
- 犠牲者をどう選ぶ
-
トランザクションの分離レベル
分離レベル | Dirty Read | Inconsistent Read | Fuzzy Read | Lost Update | Phantom Read |
---|---|---|---|---|---|
READ-UNCOMMITED | o | o | o | o | o |
READ-COMMITED | x | o | o | o | o |
REPEATABLE-READ | x | x | x | o | o |
SERIALIZABLE | x | x | x | x | x |
-
x: 起きない
- 【補】o: 起きる とは限らない(製品依存だったはず)
- 【補】ノンリピータブルリード(ファジーリード)は別資料より
- SERIALIZABLEが最も分離性が高い
- が、性能は低い
-
性能のために分離レベルを下げることがある
- 異常を回避するために余計に手間がかかる
-
ロックの挙動や構文は製品依存
- 移植性低い
MVCC: MultiVersion Concurrency Control
- 非ロックスケジューリング
TR1 | TR2 | データX(最新) | データX(古) | データY |
---|---|---|---|---|
1000 | 1000 | |||
BEGIN | ||||
X読み取り: 1000 | ||||
Xから100引く: 900 | ||||
X更新 | 900 | 1000 | ||
BEGIN | ||||
X読み取り: 1000 | ||||
Y読み取り: 1000 | ||||
X+Y: 2000 | ||||
COMMIT | ||||
Y読み取り: 1000 | ||||
Yに100足す: 1100 | ||||
Y更新 | 1100 | |||
COMMIT |
- 古いバージョンの値を参照できるため、インコンシステントリードが生じない
-
ロールバックセグメント
- 古いバージョンの値の格納領域
- 通常通りSELECTを記述すれば自動的に参照される
-
SERIALIZABLE以外の分離レベルを採用する動機
- 特に、参照だけのトランザクション
クラッシュリカバリ
DBサーバのコンポーネント
-
トランザクション理論上のコンポーネント
-
ステーブルDB
- 不揮発ストレージ上のDB
-
DBキャッシュ
- 揮発メモリ上の、DBのサブセット
- ステーブルDBからデータフェッチ
- DBキャッシュ上で変更操作
- ステーブルDBへフラッシュして反映
-
ステーブルログ
- 不揮発ストレージ上ログ
-
DBキャッシュ上で行われた操作の履歴
- COMMIT
- ROLLBACK
-
ログバッファ
- ステーブルログに書き込みを行う前に利用されるバッファ
-
-
クラッシュリカバリ
- ステーブルログに記録されているログエントリをREDO
- クラッシュする瞬間に完了していなかったトランザクションによる更新をUNDO
トランザクションとデータモデルの融合
リレーショナルモデルとACIDの「C」
- 一貫性(Consistency)のためにはデータモデルへの理解が不可欠
リレーショナルモデルと異常
-
トランザクションにおける異常
-
同時に複数の処理を実行したときに生じる異常
- 個々の行の値がいきなり変わる
- 行そのものが増減する
-
- リレーションの演算の正しさを保証するためには上記異常が起こらないことが不可欠
正規化と直交性
-
リレーショナルモデルにおける異常
-
データそのものに矛盾が生じる
- 重複データの一部のみ更新
-
- スケジュールに問題がなくても、DBに重複があり更新時異常が起きたら意味がない
制約
データモデルだけでは不十分
- 述語論理、あるいは集合演算の外のビジネスロジックはアプリケーションに記述する
-
アプリケーションの「バグ」が生じることを前提でRDBを守らなければならない
- 二重三重の防御策が必要
制約を活用してデータを守る
-
NOT NULL
- 正規化するときは必ずつけよう(1NFの条件)
-
一意性制約
- 候補キーすべてに主キーorユニークインデックスをつけよう
-
CREATE TYPE
- SQLがサポートしているデータ型よりもドメインを狭めたい時に
- 方言が激しい
-
CHECK制約
- カラムが取りうるデータの範囲を細やかに・現時点でのテーブルの状態に合わせて制限するのに役立つ
-
製品によりサポート状況異なる
- SQL標準が実装されていなかったり
-
外部キー制約
- 子テーブルに存在するキーと同じ値のキーが親テーブルに存在する
-
痛い目を観たくなければ使っておけ
- ほんの少しの性能の低下や運用の手間に比べるとはるかに重要
- 【補】論理削除の場合はつけられない
-
トリガー
-
外部キーで表現できない制約を表現する
- 子テーブルに存在するキーと同じ値のキーが親テーブルに存在しない(NOT)
-
子テーブルに存在するキーと同じ値のキーが複数の親テーブルのいずれかに存在する(OR)
- 【補】「現在単価」と「単価履歴」をFKで参照する場合
-
子テーブルに存在するキーと同じ値のキーが複数の親テーブルのいずれかだけに存在する(XOR)
- 【補】「ユーザマスタ」と「削除済みユーザ」をFKで参照する場合
- 子テーブルに存在するキーと同じ値のキーが複数の親テーブルのうちN個以上/未満存在する
-
-
リレーションの演算でない操作を使用する
-
例
- 集計
- ストアドファンクション
- 集合論で表現できないロジックを記述するので当然
-
- 行トリガー/文トリガーがある
- 1行ごとの制約の記述に向いているのは行トリガー
- 複数行にわたる値の検査はアプリケーション側でトランザクション内で行うとよい