この特集のトップページへ
Chapter 1:クライアント/サーバーアプリケーションの仕組み

見出し 1.1.3 トランザクション処理の必要性
 前項で述べたどのモデルでアプリケーションを開発するとしても,クライアント/サーバーモデルでアプリケーションを構築する場合には,必ず考えるべきことがある。それは,クライアント/サーバーモデルのアプリケーションでは,クライアントがデータベースに対して同時にアクセスすることがあるため,排他ロックの仕組みなどを考えなければならないということである。

 本節では,どのようにして同時アクセス時の問題を解決すればよいのかについて説明する。

●ロック機構

 当然のことながら,同時に複数のユーザーがデータベーステーブルの同じレコードを書き換えようとしたときには,問題が生じる。そのため,一般的に,あるレコードに対して誰かが編集作業を開始したら,それ以外のユーザーにはレコードの編集を許さないという仕組みを実装する。つまり,レコードを編集可能なユーザーは常に1人だけとして,ほかのユーザーはその編集作業が終わるまで待機させるのである。このように,特定の操作ができる人を1人に絞り,複数人によって同時に操作させないようにすることを,「レコードをロックする」あるいは「レコードにロックをかける」という。

 すべての操作でロックをかけるのが一番安全であるが,やみくもにロックをかけてしまうと,パフォーマンスは低下する。なぜなら,ロックをかければ,そのあいだほかのユーザーは処理を待たなければならないからである。以上の説明からわかるように,ロック機構を的確に利用しなければ,アプリケーションの性能問題を引き起こしかねない。

 ここでは,ADOのロック機構を例にとって,ロックをどのように使うべきなのかについて説明する。

○ADOのロック機構
 ADOでは,RecordSetオブジェクトを生成してデータベースのテーブルを開くときに,生成したRecordSetオブジェクトに対して利用可能なロック機構を選択するようになっている(詳細は,「Appendix A ADOコンポーネントによるデータベースアクセス」で解説)。ADOで利用可能なロック機構は,次の4種類である。

  1. 読み取り専用adLockReadOnly
    読み取り専用である。このモードで作成したRecordSetオブジェクトは読み取り専用で,データベースに対して書き込むことはできない。
  2. レコード単位の排他的ロックadLockPessimistic
    レコードを編集する場合,編集開始時点でロックがかかり,ほかの人はアクセスできなくなる。
  3. レコード単位での共有的ロックadLockOptimistic
    レコードを更新した場合にロックがかかる。レコードを編集している最中でも,ほかの人はそのレコードにアクセスできる。同時に更新された場合には,先に更新したものが優先され,あとで更新しようとしたものは排除される(エラーとなる)。
  4. 共有的バッチ更新adLockBatchOptimistic
    レコードを即座に更新するのではなく,ある処理をまとめて一気に更新するバッチ更新モードで使われるロック機構である。複数のデータベース操作を連続して行う場合(たとえば,深夜にデータベースに格納されたデータを集計して別のデータベースに保存するなど)に便利である。

 レコードは,複数のユーザーが同時に書き込むことは許されない。レコードにデータを書き込む場合(レコードの追加・更新・削除)には,レコードにアクセスするユーザーを1人に絞る必要がある。そうしないと,レコードの一部のフィールド(列)しか書き込まれていないレコードを別のユーザーが読み取ってしまうなどの問題が起こる。そこで,ADOを使ってデータベースにアクセスし,データベースにデータを書き込む場合には,上記の「レコード単位での排他的ロック」もしくは「レコード単位での共有的ロック」のいずれかを用いことになっている。


One Point! レコードの読み取り時には,「読み取りロック」と呼ばれるロックがかかり,そのレコードを読み込んでいる場合には,別のユーザーは書き換えられないようになる(読み込みは許される)。

 排他的ロックと共有的ロックのどちらを使うのかは,場合によって異なる。排他的ロックを使った場合には,誰かが編集作業に入ると,ほかのユーザーはそのレコードにアクセスできなくなる(書き込みだけでなく読み取りも不可になる)。よって,編集時間が長い場合には適さない。共有的ロックを使った場合には,誰かが編集作業に入っても,ほかのユーザーは問題なくそのレコードにアクセスし,レコードを読み取ることはもちろん,編集することもできる。しかし,最終的にデータベースを更新できるのは,最初に更新作業をかけたユーザーのみである。その後,編集中のほかのユーザーがデータを更新しようとしても,そのアクセスは拒否される(Fig.1-7)。一般的には,レコード単位の排他的ロックが好まれ,レコードの編集時間を短くするようなプログラムスタイルにすることが多いが,レコードの参照割合が極めて高く,更新割合が低いような場合には,共有的ロックが用いられる。

Fig.1-7 排他的ロックと共有的ロックの違い
fig.1-7 fig.1-7

 排他的ロックを使うにせよ共有的ロックを使うにせよ,ロックのタイミングが異なるだけで,最終的にデータベースにデータを書き込むときには,複数のユーザーが同じレコードに対して更新をかけることは禁止される。よって,複数のユーザーが同じレコードに対して更新をかけてデータベースのテーブルが壊れてしまうようなことはないため,データベースの破壊という観念がいえばロック機構について開発者はあまり気にすることはない。


One Point! ロックの範囲がどこまで及ぶのかは,データベースエンジンによって異なる。1つのレコードだけをロックすれば十分な場合でも,その前後のいくつかのレコードをまとめてロックするデータベースエンジンも存在する。これは,レコードを1つ1つロックすると,処理が煩雑になり,パフォーマンスが低下する場合があるためである。たとえば,Access 97は「ページ」という単位でロックを管理しており,ロックの単位は4Kバイトになる(Access 2000では,レコード単位でロックするオプションが設けられた)。

○カーソルタイプ
-
 クライアント/サーバーアプリケーションでロック機構と並んで説明しておかなければならないのが,カーソルタイプである。クライアント/サーバーアプリケーションでは,複数のユーザーが同時にデータベースにアクセスする。すなわち,データベースを参照する場合,ほかのユーザーが更新した結果も見ることになる。その見え方を設定するのが,カーソルタイプである。ADOの場合には,ロックタイプと同様,Recordsetオブジェクトでデータベースのテーブルを開くときにカーソルタイプを指定できる。

 ADOでは,次の4種類のカーソルタイプが用意されている。

  1. 前方スクロールタイプカーソルadOpenForwardOnly
    データベースのテーブルを開いた瞬間のレコードのみを参照できる。レコードを開いた時点でのコピーが渡されると考えるとわかりやすい。このカーソルで開いたレコードは,読み取り専用である。また,テーブルを開いたあとにほかのユーザーが更新した結果を参照することはできない。さらに,取得したレコードは先頭から順にしか参照できず,たとえば,5番目のレコードを参照したのち,4番目のレコードを参照するというように,前に戻ってレコードを見ることはできない(ただし,5番目のレコードを参照したのち,いったん先頭のレコードまで移動したのちに4つ進めて4番目のレコードを参照することはできる)。
  2. キーセットカーソルadOpenKeyset
    ほかのユーザーによるレコードの更新を参照することができる。しかし,ほかのユーザーによって追加されたレコードや削除されたレコードは,参照することができない(自分が追加したレコードや削除したレコードは参照できる)。
  3. 動的カーソルadOpenDynamic
    ほかのユーザーによるレコードの追加・更新・削除といった操作すべてを参照できる。
  4. 静的カーソルadOpenStatic
    前方スクロールタイプカーソルと同じだが,後ろに戻ってレコードを参照することができる。

 最も多機能なのは,動的カーソルである。動的カーソルを使えば,自分がデータベースのテーブルを更新することもでき,さらには,ほかのユーザーが加えた操作の結果を動的に参照することもできる。

 しかし,動的カーソルはリソースを多く必要とするので,あまり多用すべきではない。逆に,最もリソースが少なくてすむのは,前方スクロールタイプカーソルである。読み取り専用で,しかもほかのユーザーが更新した結果を参照しなくてもよいのであれば,前方スクロールタイプカーソルを使うべきである。

●トランザクション処理の必要性

 以上で述べたように,データベースの更新時にデータベースエンジンがロック機構を制御してくれるのであれば,あとはほかのユーザーの更新を参照するために動的カーソルを利用すれば,複数のユーザーでデータベースにアクセスした場合の問題点を解決できると思われるかもしれない。しかし実際には,ロック機構やカーソルタイプの設定だけでは,不十分なことがある。

 複数のユーザーが同時にデータベースにアクセスしたときの問題は,よく銀行の入出金処理を例にとって説明される。ここでもその例に倣い,銀行の入出金処理でどのような問題が起こり得るのかを考えてみよう。

 まず,入金処理と出金処理を考える。一般的に,入金処理と出金処理は,次のように実装される。

【入金処理】
  1. 現金を受け取る
  2. 口座の残高を参照する
  3. 受け取った額だけ,2)の残高に加算する
  4. 3)の結果を口座に書き込む
【出金処理】
  1. 口座の残高を確認する
  2. 出金したい額だけ,1)の残高から減算する
  3. 現金を差し出す
  4. 2)の結果を口座に書き込む

 次に口座間の振り込み処理を考える。振り込み処理は,入金処理と出金処理を組み合わせたものとなり,次のようにして実現できる。

【振り込み処理】
  1. 自分の口座の残高を確認する
  2. 振り込みたい額だけ,1)の残高から減算する
  3. 振込先の口座の残高を確認する
  4. 振り込む額だけ,3)の残高に加算する
  5. 4)の結果を振込先の口座に書き込む
  6. 2)の結果を自分の口座に書き込む

 このとき,振り込み処理が同時に発生すると,問題が生じることがある。ここでは,次のようなシチュエーションを考える。

  1. Aという人の残高は,10万円である
  2. Aという人が,Bという人に3万円を振り込もうとしている
  3. Cという人が,Aという人に対して5万円を振り込もうとしている

 このとき,Aという人の口座の残高について考えると,最初に10万円あり,そこから3万円を差し引いて7万円,そこにCという人から5万円が入金されるので,最終的に12万円(10万円−3万円+5万円)の残高になるのが正しい。しかし,上記の一連の処理が,Fig.1-8のような順番で実行されると,残高は15万円になってしまう。

Fig.1-8 残高が間違ってしまう処理順序
fig.1-8

 Fig.1-8で発生する問題の原因は,処理順序にあるのではなく,レコードの編集時または更新時にしか,ロック機構が作動しない点にある。Fig.1-8の各振り込み処理は,次のようにしたのであった。

  1. 自分の口座の残高を確認する
  2. 振り込みたい額だけ,1)の残高から減算する
  3. 振込先の口座の残高を確認する
  4. 振り込む額だけ,3)の残高に加算する
  5. 4)の結果を振込先の口座に書き込む
  6. 2)の結果を自分の口座に書き込む

 このときロックがかかるのは,データベースに書き込むとき,すなわち5)と6)の処理だけである。ほかのユーザーは,5)と6)の処理中以外ならば,自由に口座にアクセスできる。そのため,1)〜6)の処理中に別の処理が割り込み,問題の原因となったのである。

 この問題を解決するには,1)〜6)の処理中に別の処理を実行できないようにすればよい。ある程度本格的なデータベースエンジンには「トランザクション機能」と呼ばれる機構が搭載されており,ある一連の操作をまとめて処理し,そのあいだには別の処理が割り込まないように制御できるようになっている。トランザクション機能でひとまとめにする一連の操作(上記の例でいえば,1)〜6)の操作)の単位を「トランザクション」と呼び,その一連の操作を実行することを「トランザクション処理」と呼ぶ。

 ADOでは, ADODB.ConnectionオブジェクトのBeginTransメソッドとCommitTransメソッドで一連の操作を囲むことにより,その部分の操作内容はトランザクションとして扱われる(List 1-2)。つまり,BeginTransメソッドとCommitTransメソッドで囲まれた部分のデータベース処理中は,ほかの処理が割り込まないようになる。

注意 BeginTransメソッドを呼び出してトランザクション処理を開始した場合,CommitTransメソッドを呼び出すまで,その結果はデータベースに反映されない。


One Point! データベースに詳しい人ならば,Fig.1-8の処理において,2)と6),4)と5)をそれぞれ同時に処理すれば,問題は解決すると思われるだろう。実際,口座の残高を参照してから口座の残高を計算し,そのあと口座に書き込むのではなく,口座の残高を参照と同時に更新してしまうこともできなくはない(具体的に,たとえば3万円減らすのであれば,“UPDATE テーブル名 SET 残高=残高-30000 WHERE 口座番号=更新したい口座番号”とすればよい)。そうすれば,ロック機構によって問題は解決される。しかし,すべての処理がロック機構で十分かといえば,そうではない。ロック機構は,あくまでもレコード単位でしか機能しないという点に注意してほしい。すなわち,レコードやテーブルをまたぐような一連の操作を実行したい場合には,トランザクション処理が必要となる。

○トランザクションとロールバック
-
 トランザクション機能は,一連の操作をするときに,ほかの処理が割り込まないようにするだけではなく,もう1つ重要な役割をもっている。それがロールバック機能である。

 トランザクションは,一連の操作を示す単位であるが,その一連の操作のどれかが失敗したときのことを考えてみよう。操作に失敗するケースとしては,データベースに書き込めないなどの異常があった場合があり得る。たとえば,AがBに対して振り込みを行う,次のトランザクションを考える。

  1. Aの口座の残高を確認する
  2. 振り込みたい額だけ,1)の残高から減算する
  3. Bの口座の残高を確認する
  4. 振り込む額だけ,3)の残高に加算する
  5. 4)の結果をBの口座に書き込む
  6. 2)の結果をAの口座に書き込む

 ここで,最後の6)の処理に失敗したとしよう。このときはどうなるだろうか。そう,Aの口座の残高は減らないまま,Bの口座に入金されてしまう。つまり,Aは自分の懐を痛めることなく,Bに入金できるのである。この処理は,もちろん正しくない。

 ではどのようにして回避すればよいのかというと,単純に,実行した一連の操作をすべて,実行しなかったことにすればよいのである。つまり,6)で失敗したならば,1)〜5)の処理を併せて取り消せばよいのだ。

 1)〜6)のような簡単なトランザクションであれば,結局のところ,4)で加算した額だけ,Bの口座から減算する処理だけですむので,アプリケーション側でも楽に対応できる。しかし,今度はBの口座から減算するという処理に失敗したらどうするのかという問題もある。

 このような問題を解決するため,トランザクション処理では,一連の操作が1つでも失敗したならば,データベースエンジンが一連の操作をすべて取り消す。この処理を「ロールバック処理」と呼ぶ。ロールバック処理がどのように実装されているのかはデータベースエンジンによって異なるが,一般的には「トランザクションログ」と呼ばれるログ領域によって実現されることが多い。トランザクションログには,データベースに対して行われた処理内容が逐次記録されている。もしトランザクション処理中に何らかのエラーが発生したときには,トランザクションログを辿って,トランザクション処理に入るまえの状態まで,データベースの内容を復元するのである。

 このような仕組みになっているため,トランザクションという単位で括られた一連の操作は,「すべてが成功する」か「すべてが失敗する」のいずれかの状態しかとらない。一連の操作中の一部は成功して,一部は失敗する,というような中途半端な状態にはならないのである。この機構は,障害に備えるためには極めて重要である。

 なお,ロールバック処理は,データベースの異常が発生した場合などにはデータベースエンジン側で自動的に実行されるが,トランザクションを中止するようにアプリケーション側からデータベースエンジンに依頼することもできる。ADOの場合には,ADODB.ConnectionオブジェクトのRollBackTransメソッドを呼び出すと,トランザクションを明示的に失敗させることができる(List 1-3)。トランザクションに失敗すれば,データベースエンジンはロールバック処理を実行し,行ったすべてのデータベース操作がトランザクション処理に入るまえの状態に戻される。RollBackTransメソッドを利用すると,アプリケーション側でエラーが発生したときなどに,データの復元処理をデータベースエンジンに任せることができるため,便利である。

prev Chapter 1 5/11 next