理論から学ぶデータベース実践入門 ch14 トランザクションの本質

RDBSQL勉強メモ

出典: 


まとめ

  • 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
    • ログバッファ

      • ステーブルログに書き込みを行う前に利用されるバッファ
  • クラッシュリカバリ

    1. ステーブルログに記録されているログエントリをREDO
    2. クラッシュする瞬間に完了していなかったトランザクションによる更新をUNDO

トランザクションとデータモデルの融合

リレーショナルモデルとACIDの「C」

  • 一貫性(Consistency)のためにはデータモデルへの理解が不可欠

リレーショナルモデルと異常

  • トランザクションにおける異常

    • 同時に複数の処理を実行したときに生じる異常

      • 個々の行の値がいきなり変わる
      • 行そのものが増減する
  • リレーションの演算の正しさを保証するためには上記異常が起こらないことが不可欠

正規化と直交性

  • リレーショナルモデルにおける異常

    • データそのものに矛盾が生じる

      • 重複データの一部のみ更新
  • スケジュールに問題がなくても、DBに重複があり更新時異常が起きたら意味がない

制約

データモデルだけでは不十分

  • 述語論理、あるいは集合演算の外のビジネスロジックはアプリケーションに記述する
  • アプリケーションの「バグ」が生じることを前提でRDBを守らなければならない

    • 二重三重の防御策が必要

制約を活用してデータを守る

  • NOT NULL

    • 正規化するときは必ずつけよう(1NFの条件)
  • 一意性制約

    • 候補キーすべてに主キーorユニークインデックスをつけよう
  • CREATE TYPE

    • SQLがサポートしているデータ型よりもドメインを狭めたい時に
    • 方言が激しい
  • CHECK制約

    • カラムが取りうるデータの範囲を細やかに・現時点でのテーブルの状態に合わせて制限するのに役立つ
    • 製品によりサポート状況異なる

      • SQL標準が実装されていなかったり
  • 外部キー制約

    • 子テーブルに存在するキーと同じ値のキーが親テーブルに存在する
    • 痛い目を観たくなければ使っておけ

      • ほんの少しの性能の低下や運用の手間に比べるとはるかに重要
    • 【補】論理削除の場合はつけられない
  • トリガー

    • 外部キーで表現できない制約を表現する

      • 子テーブルに存在するキーと同じ値のキーが親テーブルに存在しない(NOT)
      • 子テーブルに存在するキーと同じ値のキーが複数の親テーブルのいずれかに存在する(OR)

        • 【補】「現在単価」と「単価履歴」をFKで参照する場合
      • 子テーブルに存在するキーと同じ値のキーが複数の親テーブルのいずれかだけに存在する(XOR)

        • 【補】「ユーザマスタ」と「削除済みユーザ」をFKで参照する場合
      • 子テーブルに存在するキーと同じ値のキーが複数の親テーブルのうちN個以上/未満存在する
  • リレーションの演算でない操作を使用する

      • 集計
      • ストアドファンクション
    • 集合論で表現できないロジックを記述するので当然
  • 行トリガー/文トリガーがある
  • 1行ごとの制約の記述に向いているのは行トリガー
  • 複数行にわたる値の検査はアプリケーション側でトランザクション内で行うとよい