同時実行制御 排他制御 違い

本連載ではツールやフレームワークの機能を最大限利用しつつ、保守、開発効率を意識したWebフォームアプリケーションを開発する方法を学んでいきます。前回から2回に渡って、データの整合性をどのように確保していくかということを学んでいます。今回は排他制御についてです。

同時更新の問題

テキストエディターなどの基本的に1人で使う「スタンドアローン」なアプリケーションとは異なり、業務アプリケーションは大勢が同時に使うことが前提です。

大勢が同時に使う場合、1人で使う場合とは大きく異なる点が1つあります。それは、「同じデータをほぼ同時に編集する」というケースが発生するということです。

同じデータをほぼ同時に編集した場合、何も対策をとっていなければ、わずかな差で後から編集を行った操作が採用されることとなります。しかし、それでは具合が悪い場合もあります。

本連載で作成している会議室予約システムで考えてみましょう。

すでに登録されている予約情報の時間を2人がほぼ同時に更新したとします。このとき、先に変更を行ったユーザーは、他のユーザーがほぼ同時に同じ予約を変更したとは思っていません。もちろんエラーも発生しないため、自分の行った変更が正しく反映されたと思うでしょう。

しかし実際は、後から変更したユーザーの入力内容で予約情報が上書きされてしまっています。

図1 同時更新のイメージ

同時実行制御 排他制御 違い

もちろん、変更を行った後、予約参照画面で確認することはできますが、自分が行った変更が無事に行われたかどうかを毎回確認することは、あまり現実的ではありません。

そこで、こういった同時更新を防ぐために行うのが排他制御です。

排他制御

排他制御とは、一言でいえば同じデータを複数人が同時に変更できないようにする仕組みです。変更できないようにすることを俗に「ロックする」と呼びます。

排他制御には、大きく次の2つの方法があります。

  1. 悲観的(Pessimistic, ペシミスティック)排他制御
  2. 楽観的(Optimistic, オプティミスティック)排他制御

それぞれについて説明していきましょう。

悲観的排他制御

悲観的排他制御では、更新対象のデータを読み出してから更新を終えるまでの間、他のユーザーがそのデータに触れないようロックします。

図2 悲観的排他制御のイメージ

同時実行制御 排他制御 違い

この方法の特徴として、

  1. ロックを取得したユーザーのみ更新が行える。
  2. トランザクションが長くなるとロック期間が長くなり、全体としてのパフォーマンスが落ちる。

といった点があります。

従って、トランザクションが短く、頻繁に更新され、なおかつ同時更新が多発するような場合、例えば金額の管理に向いています。

楽観的排他制御

楽観的排他制御では、更新対象データを最初に取得した時点ではデータをロックしません。

そのかわり、データを更新する直前に、他のユーザーによって更新されていないかどうかをチェックします。この時初めて対象データをロックします。

そして、他のユーザーによってすでに更新されていた場合、更新処理をキャンセルします。ユーザーにはキャンセルしたことを伝え、もう一度処理をやり直してもらうよう促します。

図3 楽観的排他制御のイメージ

同時実行制御 排他制御 違い

この方法の特徴としては、

  1. 複数のユーザーが並行して更新処理を行える。
  2. 反面、データの衝突(コンフリクト)が発生しやすい。

といった点があります。

従って、更新頻度がそれほど高くなく、同時に編集するユーザーが少ないような場合、例えばマスターメンテナンスなどでよく用いられます。

■同時実行制御には、ロック法、時刻印法、楽観的法がある。
ロック法をもちいた場合は、デッドロックが発生する可能性がある。

複数のアプリケーションからの同時アクセスがあった場合、DBMSは、それぞれのアプリケーション
の操作が他方に影響しないように制御する必要がある。これを、同時実行制御(排他制御)という。
同時実行制御を行なうことで、データベースの独立性と一貫性を保障することができる。

■ロック法
ロック法は、アプリケーションがデータベースにアクセスする際、アクセス対象となるデータを
他のトランザクションからアクセスできない状態(ロック状態)にする方法である。
ロックの対象には、レコード(行)、テーブル(表)、データベースがある。ロックの対象が
細かくなれば、同時実行できるトランザクションが多くなり、トランザクションの待ち時間も少なく
なるが、ロックを管理するオーバヘッドが大きくなる。
ロックのタイプには、共有ロックと占有ロックがある。占有ロックされたデータは、他のトラン
ザクションからはアクセスすることができないが、共有ロックは参照のみ可能(共有ロックをかける
ことが可能)である。

■時刻印方式
時刻印方式は、時刻印(タイムスタンプ)を用いて排他制御を行なう方式である。データを更新する
際に、トランザクション発生時刻を記録する。データを更新する前に記録された発生時刻を検索し、
自トランザクションより、発生時刻が遅いトランザクションによりデータが更新されている場合は
ロールバックする方式である。
時刻印方式はロックを用いないため、ロック法のようなロック待ちやデッドロックといった問題
が発生しない、しかし、長時間に渡って実行されるトランザクションほどロールバックの可能性が
高くなってしまう問題や、更新中のデータを他のトランザクションから参照できてしまう問題がある。

■楽観的方式
トランザクション開始時の更新前データを保存し、書き込み時に比較し変更されている場合は、
ロールバックする手法を楽観的方式という。時刻印方式同様、デッドロックやロック待ちは発生
しないが、安全性にかけるため、更新頻度のデータベースでしか利用できない。

フィギュアなどキャラ&ホビー格安。あみあみ楽天
ゴルフプラザ グリーンフィル
さかいや・オンラインショップ
ケンコーコム
上海問屋

トランザクションと同時実行制御

ここからは、トランザクションの2つ目の重要な機能である「同時実行制御」について見ていきます。

私たちがデータベースを利用するとき、1人で占有しているという贅沢なことはまずありません。自分以外にも多くの人が、検索、更新、削除といった多様な処理を同時並行で実行しています。しかし、私たちはそのことを意識しません。ユーザが「今は誰それが私と同じテーブルを更新しているから、この処理は後でやろう」と考えなければいけないようなシステムは、実用に堪えないでしょう。せいぜいあるとすれば、混みあったときにパフォーマンスが悪くなることを懸念して作業をずらす配慮をするぐらいです。

私たちがこのように、データベースを使うに際して他人の存在に無頓着でいられるのは、DBMSがうまい具合に複数のユーザの処理をスケジューリングし、結果の整合性を担保してくれているからです。この性質をACIDのI(Isolation:分離性または独立性)と呼びます。

本節では、DBMSがどうやって分離性を保証しているのか、そのしくみを詳しく見ていくことにしましょう。そこでまずキーワードとして登場するのが、Serializabilityという概念です。日本語では、直列化可能性とか逐次化可能性と呼びます。

直列させれば分離性の担保ができる

硬い言葉ですが、その定義は別に難しくありません。今、たとえばTa、Tb、Tcという3つの同時実行されているトランザクションがあったとしましょう。これらの結果が(更新に限らず検索の結果も)⁠正しい」ことは、どういう場合に言えるのでしょうか。

これに対する答えは、⁠3つのトランザクションが順次実行された場合と同じ結果が得られる場合だ」というものです。要するに、並行で実行されるケースを考えるから話がややこしくなるわけで、そもそも並行でない(=直列に)実行される場合の結果を考えて、それと同じならOK、ということです。直列実行とは、自分のトランザクションの裏側にほかのトランザクションがいないケースですから、これ以上ない厳格な定義です。DBMSが何らかの方法で、並行実行されているトランザクション群にこの性質を担保できれば、⁠正しい結果」を常に保証できる、ということが言えるわけです(図4⁠。

図4 直列に実行された場合と結果が同じなら結果の正しさを担保できる
同時実行制御 排他制御 違い
同時実行制御 排他制御 違い

どうすれば直列させられるの?

それでは、並行実行されているトランザクション群を直列にするためには、具体的にどんな方法を使えばよいのでしょう。

真っ先に考えつく単純な方法は、⁠本当にトランザクションをシーケンシャルに実行するようスケジューリングする」というものです。Taが最初に開始されていたら、終わる前にTbが始まったとしても、待機させます。同様に、Tcも待たせます。こうすると、DBMSはある瞬間においては常に1つのトランザクションしか実行されていないことになります。これは必ず直列させられる方法ですが、これを同時実行制御と呼ぶのは、同時実行してないのですから「看板に偽りあり」です。

それでも、この方法が実用的なら使って悪いことはないのですが、実際はこの荒っぽいやり方は、多くの場合にトレードオフを許容できないぐらいのパフォーマンス低下(スループットおよびレスポンスタイムの悪化)を引き起こしてしまいます。

ではどうすればよいか? そこでDBMSが取り入れた方法が、ロックによる解決です。

ロックによる解決

ロックとは、⁠鍵」という名前のとおり、ある資源に対してほかのユーザが使用できないよう鍵をかけることです。データベースにおける資源とは、テーブル、インデックス、シーケンス、ビューなどオブジェクト全般が該当します。

ロックの種類には一般に共有ロック(Sロック)と排他ロック(Xロック)の2つがあり、SはShared、XはeXcludedの略です。それぞれ読み込みロック/書き込みロックとも呼ばれます。

排他ロックのほうはイメージしやすいものです。テーブルのある行を更新しようと思えば、その行に対するほかのトランザクションのアクセスを一切禁止する必要があります。一方、共有ロックのほうは、ほかの共有ロックと両立するという特性を持っています。これは「ロック」という言葉の意味からは矛盾しているように感じられますが、共有ロックは読み取り(SELECT)の対象にかけられるものであるため、ほかのSELECTを禁止する必要がないからです。だから、共有ロックといえども、排他ロックとは両立することはできないのです。

表2は、ある資源XにトランザクションAが先にロックをかけていて、トランザクションBがあとからロックを取得しようとした場合の両立可能性を示すマトリクスです。両立可能なのは、共有ロック同士のみ、ということがわかります。

このように、使う資源を必要レベルに応じて占有/共有する、という方法で、DBMSは直列可能性を担保できます。しかし、このロック方式もまた、無視できない問題を2つ抱えています。

表2 共有ロック(S)と排他ロック(X)の両立可能性

A
X S
BX × ×
S ×

ロックのコスト:スラッシングとデッドロック

スラッシング

DBMSがロックを行うとき、具体的にはロックの取得と解放という2つの動作をしています。そして、ロックが取得されている資源にほかのトランザクションがアクセスをかけてきたら、共有ロック同士でない限り「ブロック」を行います。あとから来たトランザクションをロック解放まで待たせるわけです。よく駅や空港のトイレで行列ができるのを見かけますが、あれなどまさにブロックされたトランザクション群の典型です[5]⁠。トイレの便座を共有ロックで、というわけにはいきませんね。

このしくみから必然的に導かれる結果は、並行トランザクション数が一定数を超えると、1つのトランザクションが待機させられる頻度と時間が増え、平均のパフォーマンスが悪くなるということです。システムの特性(更新が多いのか、検索が多いのか)やハードウェア性能にも左右されるため、一般的な閾値(いきち)は負荷試験をやって測るしかありませんが、このようにロックによるパフォーマンス低下が起きる現象をスラッシングthrashing)と呼びます。

一度スラッシングが発生するところまでトランザクションの多重度が上がると、それ以上の多重度では性能は劣化する一方となります(図5⁠。そのため、対策としては限界多重度を超えない程度に流量制限を行うか、ロック粒度を小さくするなどアプリケーションロジックの見直しを行うか、などを考えなければなりません。

図5 ロックによるスラッシングの発生
同時実行制御 排他制御 違い
『Database Management Systems 3rd ed.』P.534の図を一部日本語訳

デッドロック

他方デッドロックは、スラッシングのような程度の問題とは異なり、論理的な問題であり、それだけに発生条件も厳密です。簡単に言えば、複数のトランザクションが複数の資源をロックする場合(今は単純化のため、2つとしましょう⁠⁠、互いに相手の資源解放を待つ状態となり、永遠に待機する閉路に陥ることです(図6⁠。

図6 デッドロック
同時実行制御 排他制御 違い
  • Aが資源Xを排他ロックする
  • Bが資源Yを排他ロックする
  • Aが資源Yを排他ロックしようとするが、Bの先行ロックにより待機状態となる
  • Bが資源Xを排他ロックしようとするが、Aの先行ロックにより待機状態となる

このデッドロックを解消する方法は、どちらか一方のロックを強制的に解放してやることです。デッドロック検知のしくみを持っているDBMS[6]もありますし、またトランザクションに待機の上限時間を設定することで、長時間待機しているトランザクションを破棄(アボート)させるという方法もあります。

アプリケーションの作成側としてデッドロックを発生させないため注意することは、論理的に図6のような閉路を作らないよう設計することはもちろん、なるべく不要なロックを取得しない、ロックの粒度を可能な限り小さくする、といった方針に従うことが必要となります。

私たちは不完全さに我慢できるか?

ここまでで、トランザクションの結果の正しさを保証する基準としての直列化可能性という概念、そしてそれを実現するための一般的なメカニズムがロックだが、この方法にはスラッシングとデッドロックという代償が発生することを見てきました。残念ながらこの問題を副作用なく解決する手段はありません。前回も述べたように、⁠この世にフリーランチはない」は絶対の法則です。

ロックのコストを下げる方法として、一つの道があります。それは、トランザクションが直列化可能ではないことを認めて、いい加減な結果に我慢するという妥協策です。実は、多くのDBMSがこの妥協策を採用していて、直列化可能であるよりも低いレベルの分離性をサポートしています。

ANSI標準では、次のような4つのトランザクションの分離レベルが定義されています。すでに見たように、一番下の直列化可能が最も厳しく、トランザクション相互の干渉を一切許さず、上にいくほどほかのトランザクションの干渉を受けやすくなります。

  • 非コミット読み取り(Read Uncommitted)
  • コミット済み読み取り(Read Committed)
  • 再読み込み可能読み取り(Repeatable Read)
  • 直列化可能(Serializable)

いい加減であることの代償は?

直列化可能以外の3つのレベルにおいては、トランザクションの相互干渉による分離性の侵犯が起きます。それは、次の3つの現象として現れます。

ダーティリードTaが列の値を変更しているが、まだコミットしていない場合でも、Tbが変更後の値を読み出す。たとえば、あるテーブルの列値が「10」であるレコードをTaが「20」に変更した場合、コミット前でもTbがSELECTした結果が「20」になる。確定前の「汚れた」データを読み出してしまうことから付いた名前繰り返し不可能な読み出し最初に、Taがある列値「10」を読み出したとする。そのあと、Tbが列値を「20」に変更し、コミットも行った。そのあと、Taが再度SELECTを実行すると、⁠10」ではなく)変更後の「20」が読み出される。Taが最初に読み出した値「10」が再現しないことから付いた名前 ファントム最初に、Taが範囲検索を行い、3行のレコードを読み出したとする。そのあと、Tbがちょうどその範囲に収まるデータを1行INSERTし、コミットも行った。そのあと、Taが再度同じSELECT文を実行すると、選択されるレコード数が4行になる(もし直列化可能であれば、最初と同じ3行が選択されなければならない⁠⁠。消えたり現れたりするデータが「幽霊」に似ていることから付いた名前

これら3つの現象がどの分離レベルで起きるかをマトリクスにまとめると、表3のようになります。

表3 ダーティリード、繰り返し不可能な読み出し、ファントムの発生と分離レベルの関係(Yは発生し得ることを、Nは発生しないことを示す)

分離レベルダーティリード繰り返し不可能な読み出しファントム
非コミット読み取り Y Y Y
コミット済み読み取り N Y Y
再読み込み可能読み取り N N Y
直列化可能 N N N

直列化可能の場合は、一切の干渉が起きないため、3つの現象すべてと無縁です。それから一段階ずつ緩くなるにつれて、干渉による侵犯も1つずつ増えていくことになります。

そうすると、気になるのはどの程度までのいい加減さなら認められるか、という問題です。これはシステムに求められる厳密性にも依存するので一概に決めることはできないのですが、ほとんどのDBMSが「コミット済み読み取り」をデフォルトの分離レベルとして設定しています(表4⁠。

表4 DBMSと分離レベルの対応(◎=デフォルト、○=対応、-=非対応)

分離レベルOracleSQL ServerDB2PostgreSQL MySQL(InnoDB)
非コミット読み取り
コミット済み読み取り
再読み込み可能読み取り
直列化可能

PostgreSQLは、非コミット済み読み取りと再読み込み可能読み取りを設定した場合、それぞれ1つ上位の分離レベルで動作する

これはつまり、ダーティリードのみ阻止して、残り2つの現象は許容するということです。まあだいたい、これぐらいが一般的なシステムの要件としても許容できるレベルということなのでしょう。

データベースの基本精神

速くて不味いのと、遅くて美味しいのと、どちらか選びなさい。

まとめ

本稿では、リレーショナルデータベースにおけるトランザクションの概念と、その内部的な扱い方について見てきました。データベースにとってトランザクションが満たすべき重要な性質は、ACIDという4つの概念によって定義されています。それらを満たすためにDBMSが採用している方法が、WAL、ロールフォワード/ロールバック、ロックなどの技術です。

これら個々の技術に共通することは、厳密さとパフォーマンスのトレードオフに対する妥当な解を見つけるための努力だ」ということです。データファイルを直接更新するコストが十分に小さければWALは必要ありませんし、複数の分離レベルもまた、いい加減さをどの程度許容するかを選ぶためのアイデアです。トレードオフは、地球上のすべての物が重力の法則に縛られるように、システムの世界を支配する第一原理なのです。

それでは、今回のポイントをまとめましょう。

  • リレーショナルデータベースのトランザクションはA(原子性)によって、⁠すべてか無」の原則に支配されている
  • DBMSは更新ログ(ジャーナル)へ先行書き込みを行うしくみ(WAL)を持っている
  • 同時実行制御はロックのメカニズムで実現される
  • ロックは厳密さを保証する方法だが、常にパフォーマンスとのトレードオフとなるため、⁠I 分離性)のレベルについて選択が必要

今回の演習問題は次のものです。

演習問題

あなたの使うDBMSが、チェックポイントを実行してデータファイルへの変更データの同期を行うタイミングを調べなさい。

演習問題の解答は筆者のWebサイトで公開します。それでは、次回またお会いしましょう。

参考資料

『データベースシステム概論 第6版』

(C.J.Date著、藤原譲訳、丸善、1997年)

トランザクションについては「第13章 復旧⁠⁠、⁠第14章 並行性」でコンパクトに解説されており、入門に最適です。本書はおそらく日本語で読める一番優れた概論ですが、10年以上前に書かれているため、やや情報は古くなっています(原書は第8版まで出ていますが、邦訳は第6版が最新⁠⁠。『Database Management Systems 3rd ed.』

(Raghu Ramakrishnan、Johannes Gehrke著、McGraw Hill Higher Education、2002年)

「Chapter 16 Overview of Transaction Management⁠⁠、⁠Chapter 17 Currency Control」および「Chapter 18 Crash Recovery」がトランザクション制御について簡明かつわかりやすい情報を与えてくれます。

同時実行制御の目的は?

同時実行制御(排他制御)とは、データベースに複数トランザクションがあった場合、データの整合性(一貫性)を保つための機能です。

排他制御の仕組みは?

排他制御とは、複数の主体が同じ資源を同時に利用すると競合状態(race condition)が生じる場合に、ある主体が資源を利用している間、別の主体による資源の利用を制限もしくは禁止する仕組みのこと。

排他制御 なぜ?

データの不整合を防ぐ「排他制御」 複数のスレッドから1つの共有のリソースにアクセスすると、データの不整合が起こることがある。 しかし厄介なことに、データの不整合は一見しただけでは分からないことが多く、発見が困難なバグとなってしまう。 それを避けるために排他制御が必要となってくる。

排他的ITの意味は?

ITパスポート試験 用語辞典 複数のアクセスができる共有資源(データやファイル)に対して、同時アクセスによる不整合の発生を防ぐため、一方のプロセスが処理中の場合、他方のプロセスが利用できないようアクセスを制限し、処理を制限することである。