このページをはてなブックマークに追加このページを含むはてなブックマーク このページをlivedoor クリップに追加このページを含むlivedoor クリップ

目次

データベースの破壊と保護

 データ破壊の原因は次の2つが考えられる。

  • トランザクションの条件に違反した場合
  • 不慮障害が発生した場合

 データベースのデータはこうした不慮の事故にあっても復元できなければならない。そのために、DBMSはいつでも復旧できる仕組みを有している。その主要な機能は次の3つがある。

  • バックアップ機能
  • 更新ログ機能
  • リカバリ機能

 定期的に別の媒体(他のディスクや他のコンピュータ)にDBのコピーをとっておく機能をバックアップという。このようにしておけば、障害が起こった場合でもバックアップを取った時点の状態に復元することができる。しかし、これだけでは最新の状態に復元できない。そこで、DBにデータの挿入・更新・削除などを行った場合に、その内容を履歴として自動的に記録していく機能を提供している。これを更新ログという。バックアップ時点の状態から、この更新ログを使って更新履歴の順に再現していけば最新の状態に復元できるわけである。このように最新の状態まで復元する機能をリカバリという。

トランザクション

 論理的に矛盾を起こさない最も小さな処理の単位をトランザクション(transaction)という。データベースには矛盾があってはいけないので、データの状態を「ある正しい状態」から「別の正しい状態」に変換する最も小さい処理のことをトランザクションというわけである。

 トランザクションには次のような4つの特性を持つ。

  • 原子性(Atomicity)
    • 論理的に矛盾を起こさない最も小さな処理の単位。
    • 例えば、A口座からB口座に100万円を振り込んだ場合、「A口座−10万円」と「B口座+10万円」のセットで1処理単位となる。
    • オール・オア・ナッシング(All or Nothing)を実現する機能として、コミットとロールバックが備わっている。コミットがオール(All)、ロールバックがナッシング(Nothing)を実現する機能という対応になっている。
    • トランザクションは、分解できない処理の固まりという意味で原子なのである。
  • 一貫性(Consistency)
    • データベースには矛盾があってはいけないということを表した特性。
    • 例えば、「A口座−10万円」と「B口座+10万円」の処理の一方が完了しないまま終了した場合は矛盾するから、そういうことがないように処理の単位を決めなければならないということである。
    • しかし、2つ以上のデータを同時に修正することは不可能なので、ひとつ目のデータを修正し、2つ目のデータを修正し終えるまでの間は矛盾した状態になる。この瞬間だけ、それらのデータを見せなくするようにする。2つ以上のデータをセットで修正する場合は、関係するすべてのデータを誰にも見せないようにし、すべてが完了してから見せるような制御を必要とするわけである。それが排他制御(ロック制御)である。これは一時的にデータを独占して処理が完了したら解放するという仕組みである。
  • 独立性(Isolation)
    • トランザクション同士はお互いを気にせずにいつでも実行できるという性質のこと。
    • トランザクションの順番を変更した結果が変わっていたのでは問題がある。
  • 耐久性(Durability)
    • トランザクションが更新したデータは次に更新されない限りその結果を永久に維持するという特性である。
    • これを実現するには障害などの対策も必要。
    • データベースに対するアクセスは、トランザクションによってのみ行われるものと考えればよい。

 この4つの各特性の頭文字をとって、ACID特性と呼ばれる。

 データベースシステムでは、この特性を保証するために色々な機能を備えていなければならない。その例として次が挙げられる。

原子性コミット、ロールバック
一貫性排他制御、制約ルール
独立性排他制御
耐久性障害対策

障害対策

 トランザクションの4つの特性のうち、原子性・一貫性・独立性の3つについては正しいトランザクションの設計をしていれば論理的に問題発生を避けることができる。しかし、残りのひとつである耐久性に関しては、障害という避けられない現象にも対応できなければならない。つまり、障害対策を考慮しなければならないということである。

障害の種類

 障害には4つのレベルがある。

  • レベル1:トランザクション障害
    • あるトランザクションが正しく終了しない。
    • トランザクションはオール・オア・ナッシング(All or Nothing)のどちらかで終了するというのが条件である。しかし、プログラムのバグや誤作動によってコミットもロールバックも発行せずに終了した場合に、データベースが矛盾した状態のままになる。
    • 原子性を守らずにトランザクションが終了してしまうので、ロールバックを代行してやる必要がある。
      • 原因の例:プログラムの異常終了、デッドロックの発生
  • レベル2:システム障害
    • システムの異常終了により実行中のすべてのトランザクションが正しく終了しない。
    • ここでいうシステムとはトランザクションを操作させる環境のこと。具体的には格納装置や媒体を除くハードウェアやOSなどを含めたコンピュータシステム全般を指す。
    • システム障害が起こると、そのときそのシステム上で実行しているトランザクションがすべてトランザクション障害を起こすことになる。 しかし、システム障害の場合はもう少し複雑になる。最近のコンピュータシステムではディスクからの入出力を高速にするために、メモリ上にデータを溜めておいて一定の時間が経つとディスクに書き出すような技術が採用されている。そのため、コミットを発行したとしてもディスクに何も書き出すような技術が採用されている。この場合、コミットを発行済みであるから、耐久性の原則からデータベースにはきちんと書き込みを保証しなければならない。
    • コミットまたはロールバックを発行しないで終了したトランザクションのみでなく、コミットやロールバックを発行したトランザクションも最後まで処理しなければならないわけである。そこで、データベースへの更新がメモリ上だけで完了しているのか、ディスクに書き込まれたのかわかる仕組みが必要になる。それがチェックポイントである。メモリ上の更新が実際に書き込まれたタイミングを、チェックポイントとして更新ログなどに記録しておくのだ。
    • 最新チェックポイント以前の更新は実際にディスクなどに書かれていて、以降はメモリ上にしかないとわかるわけである。
      • 原因の例:DBMSやOSの異常終了、CPU・メモリなど処理系ハードウェアの障害や停電などによる異常終了
  • レベル3:メディア障害
    • 格納媒体の障害のためデータが壊れる。
    • データを別の媒体にコピーしておいて保存するしかない。
    • しかし、バックアップを取った後もデータは逐次更新されていく。そこで、データベースには更新ログ採用という機能がある。これは自動的にデータベースの中のデータを更新した場合に、その内容すべてを別の媒体(ログファイルなど)に記録していくという機能である。
    • 逐次行われる更新はログで二重化し、定期的にデータベース全体のバックアップを取っておけば万全の備えといえる。
      • 原因の例:ディスク装置などの格納媒体障害
  • レベル4:災害
    • 施設が破壊されることで復旧がほぼ不可能。
    • 大規模な災害の場合は、データのバックアップだけがあっても、それを使えるシステムが壊滅状態なのであまり意味がない。
    • 災害に対する復旧はディザスタリカバリと呼ぶ。
      • 原因の例:火災・地震などの災害によるコンピュータ施設の破壊、大規模なハードウェアの破壊

バックアップの種類

 障害管理の基本は二重化、すなわちバックアップである。バックアップにはフルバックアップ、差分バックアップ、増分バックアップなどがある。

バックアップのタイミング

 バックアップを取るタイミングに基づいて分類すると、次の2つになる。

  • オフラインバックアップ
    • 最も古典的なもの。
    • データベースのシステムやサービスを停止して、その間にバックアップを取る方式。
    • 一般的には、サービスを停止した夜間にバックアップを取って、朝またはサービスを開始するという運用である。
  • オンラインバックアップ
    • 現在では夜間にサービスを止めるといったことはほとんどできなくなっている。つまり、サービスを止めた夜間にバックアップを取ること自体が困難なのである。
    • そこで、サービスを止めないでバックアップを取る法式が考えられた。
    • もっとも一般的な方法は、多重ミラーリングの技術を使った方式で、次のような手順で実現される。
      1. データベースのディスクを多重ミラーリングしておく。
      2. そのうちの1台を一時的に切り離してオフライン状態にする。
      3. このディスクからバックアップを取る。
      4. そして、そのディスクをまたミラーリング状態に戻す。
      5. 戻されたディスクは切り離し中に他のディスクに行われた更新データを取り出し、自分のディスクに書き込む。
      6. 他のディスクと同じ状態まで更新が追いついた時点ですべてのディスクが一致した状態に戻る。

更新ログ

 バックアップは、一定期間ごとに取られるので、その間の更新データはそこには含まれていない。よって、もしデータベースが壊れた場合は、バックアップだけでは最新の状態まで復元することができない。データの保全性を保証するには、いかなる場合でも最新の状態に復旧できなければならない。そのために必要な機能が更新ログである。データベースではデータが更新される(追加、変更、削除)ときは、常に自動的に更新内容を更新ログファイルに記録し、それが確定してから実際のデータを更新する。つまり、更新のたびに作られる更新データのコピーが、更新ログである。これも原理は二重化である。

 更新ログは、データの更新のたびに自動的に取られる増分バックアップともいえる。

 更新ログには2つの目的がある。

  1. データを障害直前の最新の状態に復元すること。
  2. 不完全に終了したトランザクションが更新したデータをキャンセルすること。

 そのために、次のような2種類がある。

  • 更新前ログ
    • データを更新する前の内容を保存しておくことを目的としたログ。
    • このログを利用すれば、データを更新する前の状態に戻すことができる。
  • 更新後ログ
    • データを更新した結果の内容を保存しておくことを目的としたログ。
    • このログを利用すれば、バックアップ以降に行われた更新を再現していくことができる。
    • 更新前ログおよび更新後ログの特徴として、必ず更新した順番に時系列に並んでいるという点がある。

 バックアップや更新ログは、データベースのリカバリをするためのもである。バックアップ自体が目的ではなく、リカバリをするための手段であることを認識しておこう。そして、バックアップや更新ログがないとリカバリできないことも再認識しておこう。

リカバリの種類

 リカバリは障害の種類に応じて、次の4つに分けられる。

  • トランザクションのリカバリ
    • コミットまたはロールバックのどちらも発行せずに終了した場合は、トランザクションの異常である。
    • その状態を復旧するには、ロールバックを代行することになる。この復旧をロールバックリカバリという。
    • ロールバックリカバリは、現在からトランザクション開始時点までに向かって、更新前ログのデータを書き戻していけばよいわけだ。
  • システムのリカバリ
    • システムの障害の場合は、そのとき実行していたトランザクションすべてがトランザクション障害を起こすのに加えて、コミットが完了したトランザクションも実際にはディスクにデータが書き込まれない状態で終わっているものもある。これら2種類のトランザクションのリカバリが必要になる。 前者はロールバックを代行することになり、後者はコミットした更新を完遂する必要がある。これをロールフォワードリカバリという。
    • ロールフォワードはトランザクションの開始時点から現在に向かって、更新後ログのデータを書き直していけばいいわけである。
  • メディアのリカバリ
    • 格納媒体の障害の場合は、媒体内のデータ自体が保証されないので、新しい媒体や装置と交換するのが一般的である。
    • その場合、データが空っぽの状態なので、バックアップデータをリストアしなければならない。これをリストアリカバリという。
    • しかし、リストアしただけでは最新の状態には戻らないので、続けてロールフォワードリカバリを実行する必要がある。
    • ディスクのミラーリングをしておけば、ディスクの取替えをディスク間コピーの機能でリカバリできる。このように、メディア障害には多重ミラーリングが便利。
    • リストアリカバリには次の3パターンがある。
      1. フルリカバリ:フルバックアップのリスト
      2. 差分リカバリ:フルバックアップ以降の最後にとった差分バックアップを書き戻す。
      3. 増分リカバリ:フルバックアップ以降にとったすべての増分バックアップを順に書き戻す。
  • 災害でのリカバリ
    • 災害でのリカバリは、データベースのリカバリの範疇を超えている。
    • 災害のレベルに応じて、組織的に対応する必要があるだろう。
      1. 施設災害:マシン室やその中の設備の再投資
      2. ビル災害:新しいビルおよび新施設への再投資
      3. 都市災害:都市の再開発を兼ねた地域開発への再投資

[補講1]データベースの製品の中には、リストアリカバリも過去のある時点に戻すという意味でロールバックリカバリと呼んでいるものもある。

[補講2]個別リカバリは、バックアップを取った個別の方法に応じてデータを書き戻すリカバリ方式である。個別リカバリには、個別バックアップとペアで使用するユーティリティプログラムやツールなどを用意しているのが普通である。

リカバリのタイミング

  • オフラインリカバリ
    • メディア障害のリカバリは通常、サービスを停止してオフライン状態で行われる。
    • しかし、そのディスクに関係ないサービスは動かしておくことも可能。
    • よって、現状に合わせて診断するのが基本となる。
  • オンラインリカバリ
    • トランザクション障害のリカバリは自動的に行われるので、オンラインリカバリが基本となる。
    • システム障害の場合はシステムが止まるのだから、一旦オフラインになるが、基本的にトランザクションの復旧なので、システムを立ち上げ直したときにリカバリ機能を働かせる。大抵は自動的に働くが、製品によっては手動で起動するものもある。
    • ミラーリングを使ったバックアップ方式の場合、メディア障害でもオンラインリカバリは可能。

リカバリポリシー

 リカバリは必ずしも最新の状態に戻すことだけが目的とは限らない。場合によっては、昨日の状態に戻して本日のデータを入れなおした方が早く復旧できるかもしれない。

 リカバリのレベルには大きく分けて、完全リカバリと不完全リカバリの2つがある。重要なことはサービスを早く再開することであって、リカバリの方法ではない。サービスや業務の性質をよく検討して、どのレベルまで復旧するのがもっともよいのかを障害の種類に応じて決めておく必要がある。これをリカバリポリシーという。

ロック機能

 トランザクションがコミットを発行するまでの一時的な矛盾状態のデータを見て処理してしまうことがダーティーリードといい、他のトランザクションによる更新が消えてしまうことをコストアップデートという。このような状態を回避するためには、データ更新時は1トランザクションが占有するようにしばらく待ってもらうことである。

ロック制御

 複数のトランザクションが矛盾なくデータを更新するためには、排他制御が必要になる。目的のデータをロックして他のトランザクションが使わないようにすることをロック制御という。

 ロックには目的に応じて次の2種類がある。

  • 共有ロック
    • 同じデータを複数のトランザクションで同時にアクセスすることを許す。
    • 参照だけしかしない場合に使用するロック。
  • 占有ロック
    • 同じデータを複数のトランザクションで同時にアクセスすることを許さない。

ロック制御の原理

 トランザクションは、データにアクセスするときに共有ロックまたは占有ロックのどちらかをかけることになる。2つのトランザクションが同じデータに対してロックをかけた場合、自分と相手のロックの組み合わせに応じてお互いのそのデータへのアクセスが制限されるわけである。

 先発トランザクションAと後発トランザクションBが同じデータに共有ロックと占有ロックをかけた場合の組合わせを表にすると、次のようになる。

トランザクションB
共有ロック占有ロック
トランザクションA共有ロックお互いにOKAはOK、BはNG
占有ロックAはOK、BはNGAはOK、BはNG

[補足]表の中でNGとあるのは、相手のトランザクションが終了するまで待つようにするのが一般的。

 基本的にロックは早い者勝ちのルールになっている。こうした仕組みで、ダーティーリードとロストアップデートが回避できるわけだ。

デッドロック解除

 複数のトランザクションがデータに順次占有ロックをかけていくと、いくつかのトランザクションが動かなくなる現象が起こることがある。

例:トランザクションAがデータ1→データ2、トランザクションがデータ2→データ1の順に占有ロックしていくとする。

 トランザクションAがデータ1を占有ロックしたとき、トランザクションBがデータ2を占有ロックして、その後トランザクションAがデータ2を占有ロックしようとすると、すぐにトランザクションBが占有ロックをかけているので待たなければならない。その後、トランザクションBがデータ1を占有ロックすると、トランザクションAがすでに占有ロックをかけているので、トランザクションBが待ち状態になる。

 トランザクションAもBも、互いに相手がアンロックしない限り永遠に待ちつづけることになる。これがデッドロック状態である。どちらのトランザクションも間違った処理はしていなくても、ロックする順番がトランザクションの処理によって異なることから、このように悪循環を起こしてしまうことをデッドロックというのである。ここでは簡単に理解できるように2つのトランザクションの間でおこっているデッドロックを説明したが、現実には2つ以上の多くのトランザクションの間で悪循環を起こしていることもある。

 デッドロックを解除するにはいくつかの方法があるが、基本的には悪循環を起こしている占有ロックを異常終了させる。複数のトランザクションが占有ロックを主張するから硬直してしまうのであって、トランザクションのひとつが一時的に他のトランザクションに処理を譲れば、デッドロックが解除されて処理が進む。そして、処理を譲ったトランザクションも再度実行すれば、うまくいくのである。

 ロッドロックを起こさないためには、ロックすべきデータの順番を定めておくことである。例えば、どのトランザクションもデータ1、データ2、…の順位ロックするなどのように決定しておけば、悪循環は起こらない。つまり、排他制御は皆でデータベースを使う場合の同時更新によって起こる矛盾を防止する仕組みなのである。

データベースを矛盾から守る機能

矛盾するデータ修正を防ぐ制約機能

 まず、データの性質に基づいた、客観的な矛盾を解除するための機能についてみていく。これは整合性制約(または単に制約)と呼ぶ。

 制約には次のようなものがある。

  • NOT NULL制約
    • カラムに値が入っていない状態、即ちNULLの状態があってはならないことを宣言する制約。
  • 主キー制約
    • カラムを主キーとして利用するという定義。集合の原則と同じく、テーブル内の行は重複が許されない。よって、行を一意に識別する役割の主キーとなるカラムやカラムの組は、必ず重複がなくて、NULL値でもないという制約条件が必要になる。 主キー制約は、この条件を宣言する制約である。
  • 一意制約
    • カラムを一意キーとして利用するという定義。一意キーもテーブル内の行を一意に識別できるものでなければならない。しかし、主キー制約とは異なり、NULL値が入ることが許されている。つまり、NULL値はあってもよいが、それ以外の値は重複してはいけないという制約である。
  • 外部キー制約
    • 親子関係にある2つのテーブルで、子テーブルの外部キーと親テーブルの主キーで対応関係を表している場合に、子テーブルの外部キーの値は必ず親テーブルの主キーとして存在するものでなければならないという制約。
    • つまり、親のない子供を作らないという制約といえる。
  • チェック制約
    • カラムに入るべき値の範囲などを条件形式で指定する制約。
    • 例えば、「性別="女子"」という条件を学生名簿テーブルの性別カラムに定義することにより、性別に「"女子"」以外の値が入ることを自動的にエラーとしてくれる制約である。

 制約機能は、条件式で表すデータの保護機能といえる。

データの修正のたびにチェックする機能

 条件式では表せない制約条件は、プログラムロジックでチェックする必要がある。データが修正されるたびにチェックできればデータベースに矛盾が発生する前に検出することができる。このために提供されている機能がトリガーである。

 トリガーは、次のような条件のときに自動的に起動される。

  • データの挿入が行われる前
  • データの挿入が行われた後
  • データの削除が行われる前
  • データの削除が行われた後
  • データの更新が行われる前
  • データの更新が行われた後

 データベースの内容に変更を加えるタイミングでチェックロジックが起動されれば漏れがないわけである。チェックロジックはプログラムの形であっても、データベース操作言語のSQLでかかれていてもOKである。例えば、売上合計と消費税率と消費税額の間の関係に矛盾が発生しないようにするためには、「売上合計計算×消費税=消費税額」といったカラム間の関係をチェックするロジックを用意しておけば、更新が行われる度にこのチェックが行われることになる。

 トリガーによって起動されるプログラムは、チェックロジックだけでなく、データを更新するプログラムやSQLでも構わない。これを利用すると、あるデータを更新するトリガーが起動されて必要な更新を行うようにすることが可能である。このようにすれば、チェックすることなく正しい更新ができるようになる。トリガーで更新処理が起動されるということは、色々なテーブルにトリガーを定義しておくと、必要なところでトリガーが連鎖的に起動されて処理を完結してくれるような構成が可能である。

固定処理を登録する

 通常、ユーザーがデータベース内のデータにアクセスして追加や変更を行うためには、SQLを使って指示をしなければならない。また、一連の業務処理を行うには、SQLを組み合わせてアプリケーションプログラムを作成しておく必要がある。しかし、複数のユーザーに共通して利用される処理を、ユーザーごとに作成してクライアント上で動かしていたのでは効率が悪く無駄になってしまう。このような共通の処理は、なるべくデータベースの中に登録しておいて、皆で呼び出して利用する方が便利である。

 ストアドプロシージャは、単に処理ロジックに名前を付けて登録したものであるから、トリガーのように自動的に動作するものではなく、ユーザーがストアドプロシージャ名を指定して起動する。一度、処理ロジックの正しさが確認されていれば、誰が呼び出して使用しても正しく動作するから、データベースに矛盾が発生することを防ぐために効果がある。

 また、トリガーでは連鎖が膨大になったり永久ループするような複雑なチェックの場合は、チェックプログラムをストアドプロシージャとして登録し、データの更新が終わった後にそれを呼び出してチェックするように利用することができる。

 機密性の高いデータを特定プロシージャ呼び出しに限定すれば、セキュリティを向上させることもできる。

 ストアドプロシージャを使えば、複数のSQL文からなる手続きを減らすことができるが、データベースアクセスを細かい単位でプロシージャ化してしまうと、そのたびに通信が必要になるために処理性能は低下してしまう。

まとめ

  • 制約
    • 条件式などでシンプルに制約条件を定義する。
  • トリガー
    • 自動的に起動されるチェックロジックを定義する。
  • ストアドプロシージャ
    • 共通処理プログラムを自由に呼び出して使用できるように登録する。

 これらの特徴を理解して、それぞれを適所で活用することが重要である。

参考文献

  • 『ソフトウェア開発技術者 合格エッセンシャルハンドブック』