Jet Engineのキャッシュとその制御

2001.10.24

Jetエンジンを使ったデータベース操作で、レコードの追加や変更が他のタスクから見えない、場合によっては、同じプログラム・コードの中でも同様の事態が起こることがある、という疑問が寄せられることがあります。この問題にはデータ・キャッシュが絡んでいるのですが、Jetの低レベル動作についてはあまり知られていないことや、幾分複雑な仕組みになっていることもあり、この点について広く理解されているとはいえない状況です。

みなさんよくご存知のように、Jetエンジンはファイル共有型データベースです。ということは、データの入出力に関して一つの管理プログラムが統制しているわけではなく、各クライアントに搭載されているエンジンが各個ばらばらにmdbファイルにアクセスしているということです。

このために、マルチユーザー環境での実行制御にさいしては、ロック機構に熟知するだけでなく、エンジンの低レベル動作についてよく理解しておく必要があります。とりわけ問題になるのが、キャッシュにまつわる 動作です。キャッシュの働きによって実行速度や同時実行性(Concurrency)に大きな影響が出ます。その点から言っても、本稿で説明する内容は重要です。

ということで、ここでは、Jetのキャッシュの仕組みとそのコントロールの仕方について整理しておきます。

Jetエンジンにかかわるキャッシュ機構

データベースは速度が命です。もちろん、安定性や信頼性といったものも同様に重要ですが、遅いデータベースでは話になりません(世の中には結構あるようですけれど)。Jetエンジンは、データ入出力の速度を上げるため、データ・キャッシュを最大限活用しています。

具体的なキャッシュの働きは後述しますが、キャッシュの動作はある程度の範囲で変更することができます。キャッシュの動作を全面的に変更するには、レジストリに書き込まれた値を変更します。Jetエンジンを制御するレジストリ値が書き込まれている位置は、
    \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft
    \Jet\version\Engines\Jet version\
です(versionのところには、3.5や4.0といったバージョン番号が入ります)。以後の説明で出てくるレジストリの値はすべてこの位置に書かれています。

さて、データ・キャッシュには、読み込み時の役割と、書き込み時の役割があります。前者をRead Cache、後者をWrite Cacheと呼んでいます。じつは、Jetの動作に関係するものに、もう一つ、OS自体が持つキャッシュ機能があります。これら3つのキャッシュ機能について、次節以下でそれぞれ解説します。

最初に、Jetのデータ・キャッシュの構造ですが、キャッシュはページ単位で管理されます。1ページはJet 3.5までは2KByte、Jet 4.0ではデータの符号化方式がUnicode(UTF-16)に変更された関係で4KByteになっています。これは、mdbのデータ構造 に対応しています。キャッシュのページとmdb上のページが1対1に対応しています。

キャッシュは、レコードセットを開くときや、SQL のアクション・クエリーで実行の元になるレコードを読み込む場合に作られるほか、 新規レコードを保存する際にも新たに配置されます。 キャッシュはメモリ上に展開されるので、マシンの搭載メモリによって制限されます。あまり大きなキャッシュを確保してしまうと、OSの仮想メモリとの関係でディスクのスワッピングが発生してしまうためです。確保される最大メモリ量はレジストリのMaxBufferSizeで設定されます。初期設定は0になっており、これはJetの自動制御に任せるという設定です。この場合、Jet 3.xでは、
    (マシンの総RAM量MByte - 12) / 4 + 512KByte
が最大キャッシュ・サイズになります。ただし、Jet 3.5 では、13824KByte (=13.5MByte)を上限とします。

プロセスに対していくつのキャッシュが作られるかは、データアクセス・コンポーネントによって異なります。DAOではWorkspaceごとに一つ作られます(DBEngine全体で一つと書いてある文献もあり、どちらが正しいかははっきりしません。いずれにしろ単一のプロセスで複数のWorkspaceを扱うことはまずないので、実用的にはどちらであっても問題ありません)。ADOではConnectionごとにキャッシュが作成されます。このことは非常に重要な点です。詳しくは後述します。

Read Cacheの働きとその制御方法

Read Cacheは、データの読み出し速度を向上させます。テーブルにアクセスするごとにいちいちディスクから読み出すのではなく、メモリ上から読み出すことができるからです。しかし、キャッシュのデータが対応するディスク上のデータと不一致を起こす可能性があります。他のユーザーやプロセスによってレコードが書き換えられているかもしれないからです。

この問題を回避するために、Read Cacheでは下のデータが書き換えられていないかどうかを定期的にチェックします。mdbファイルのヘッダ部分にはページごとの更新情報が記録されており、現在キャッシュに保持されているデータよりディスク上のデータが新しい場合には、ディスクからキャッシュへの再読み込みが行なわれます。

このチェックの間隔はレジストリの PageTimeout で設定されており、初期設定値は5秒です。

Read Cacheにより、データの読み込み性能は向上しますが、他のユーザーもしくはプロセスがたまたまキャッシュに対応するディスク上のページを書き換えても、最大で5秒間はそれを認識できないという問題が発生します。この問題を回避するには、レジストリを書き換えるという方法もありますが、その影響はマシン・グローバルであり、動作性能にも影響するので、得策ではありません。しかし、プログラム的に強制的にキャッシュを更新する方法があります。

ディスクから強制的にキャッシュに読み込むには、

〔DAO〕
    DBEngine.Idle dbRefreshCache
〔ADO――JROへの参照設定が必要〕
    JetEngine.RefreshCache

 を実行します。これは、Jet 3.5 以降で可能です。このコードを実行することで、現在のプロセス上で動作しているすべてのキャッシュが再読み込みされます。

注意が必要なのは、レコードセットなどを新しく開く場合でも、キャッシュのリフレッシュが必要なことがあることです。それは、開こうとしているデータ・ページが、たまたま、以前の操作でキャッシュに入っている場合がありうるからです。予想外の問題を引き起こさないように、レコードセットを開く前にリフレッシュすることは良い習慣です。

Write Cacheの働きとその制御方法

Jetがデータをmdbファイルに書き出す際にも、キャッシュが作用します。これをWrite Cacheと呼びます。Jet 3.0 以降、別スレッドによる非同期遅延書き込み (lazy write)がサポートされています。そして、Write Cache にデータがある間は、書き込みロックがかかる仕組みになっています(implicit transaction)。たとえば、INSERT INTOなどのレコードを追加するアクション・クエリー(SQL)で大量のデータ書き込みが発生するとき、Jetは最大32ページごと(Jet 3.0では8ページ固定)にキャッシュを追加生成していきます。そして、新しいキャッシュ・ページの追加が行なわれなくなって一定時間が過ぎた後に、別スレッドがこのキャッシュからディスクへの書き込み(flush)を開始します。SQLによる書き込みだけでなく、コンポーネントのAddNew/Edit〜Updateといったメソッドを使った場合でも事情は同じです。

書き込みの遅延時間ですが、mdbを共有モード(通常)で開いたときと、排他モードで開いたときで異なります。この間隔は、共有のときは、レジストリの SharedAsyncDelay値により、排他のときは、ExclusiveAsyncDelay値により決まります。デフォルトでは、前者は 50ms、後者は2sに設定されています。 しかし、50msでは、大量のデータを書き込む場合には短すぎ、何度もフラッシュ動作が発生してしまうため、全体の処理速度を引き下げてしまいます。

そのため、この間隔を自動的に長くする設定が Jet 3.5 から用意されました。これが、レジストリのFlushTransactionTimeout値です。初期設定では500msです。この値が0以外のとき、SharedAsyncDelayおよびExclusiveAsyncDelayの値はいずれも無視されます。ですから、現在では、実際の書き込み遅延時間はどんな場合でも0.5sであると言えます。

しかし、この遅延書き込みは、マルチユーザー環境では問題になることがあります。それは、一つにはimplicit transactionにより、意図しないレコード・ロックがかかることであり、もう一つは、追加したレコードが他のユーザーからはキャッシュがフラッシュされるまで見えないことです。これを解消するのが同期書き込みです。同期書き込みを行なうには、explicit transactionを実行します。

Jet はデータ更新のさいに、必ず暗黙的なトランザクション(implicit transaction)を行います。これは、先ほど述べたように、 更新ページに対して書き込みロックが働く、ということです。これに対して、BeginTrans/CommitTransは明示的トランザクション(explicit transaction)と呼ばれます。

前述のように、書込み時には、implicit transactionでは遅延(非同期)書込み、explicit transactionでは、遅延を生じない同期書込みが行われるようになっています。これも、レジストリで設定されており、前者はImplicitCommitSync = no、後者はUserCommitSync = yesという設定です。

Write Cacheについてまとめると、次のようになります。

非同期(トランザクションなし)の場合、実際に書き込み動作が始まるのは、[1] Write Cacheが、レジストリのMaxBufferSizeに指定したサイズを超えたとき、もしくは、[2] 最後の書込み動作以降にFlushTransactionTimeoutに設定された時間が経過したときであり、このうちどちらか 短い方の条件が成立したときになります。

マルチユーザー環境などで、書き込み遅延が発生しては困るときには、同期書き込みを利用します。同期書き込みを行なうには、BeginTrans 〜 CommitTransという、明示的なトランザクションを利用します。CommitTransを実行すると、遅延なしにディスクに書き込みが行なわれます。トランザクションは、DAOではWorkspaceの、ADOではConnectionのメソッドです。

OSのWrite Cacheとその制御

キャッシュを使うのはJetエンジンだけではありません。OSにもWrite Cacheがあります。 しかしこれは、Jetのデータ・キャッシュとは独立した、OS自体がディスクI/Oの効率化のために用意している機能です。そのため、Jetがディスクに書き込み動作を行 なっても、それが本当にディスクに書き込まれたのか、OSのキャッシュ上にとどまっているのかは、Jet側からは知るよしもありません。このため、まだ OS のキャッシュ上にデータがある状態で別のプロセスが読み込みを行なおうとしたときに、問題が生じることがあります。

これを解消するために、Jet が OS のキャッシュをバイパスして直接ディスクに書き込むよう指示することができます。具体的には、DAOでは、CommitTransメソッドの引数で、ADOでは、Connectionオブジェクトのダイナミック・プロパティで設定します。DAOでは、書き込み時点での設定なので、その書き込みにだけ作用しますが、ADOではConnectionのプロパティを変更するので、元に戻さない限りその設定が有効になります。また、このような指定が有効なのは、Windows OSに対してだけであり、たとえばmdbファイルがNetWareファイル・サーバー上などにある場合には効果がありません。

〔DAO〕
    Workspace.BeginTrans
    ...
    Workspace.CommitTrans dbForceOSFlush

〔ADO〕
    Connection.Properties("Jet OLEDB:Transaction Commit Mode") = 1

※キャッシュバイパス時に1、通常動作のときに0となります。

トランザクションを使った上に、OS上でダイレクト書き込みを行なうことで、いちおう、書き込みの遅延問題は解消されます。

まとめ

Jetエンジンのキャッシュの動作とその性質については以上です。

ここで述べたことは、シングル・ユーザー環境では同一のキャッシュ上のデータだけを利用するため、ほとんど問題にはなりません。気をつけなくてはいけないのは、同じテーブルのデータを複数の処理で利用する際の、implicit transactionくらいです。

しかし、マルチユーザー環境の場合、および、ADOでも、一つのプロセス内で複数のConnectionを利用している場合(たとえば、レコードセット作成とグリッド・コントロールなどで別々のConnectionを張っている場合など)には、ここで述べたような“キャッシュによる遅延”が問題になります。自分がプログラムを作成するに当たって、どのような動作環境で動かすプログラムなのかを良く考えて、適正な処理を行なうことが、効率的で安全なデータ処理を行なううえで必要です。

ただし、いくら同時制御の対策を施しても、ファイル共有型データベースという制約上、ディスクI/Oの際にネットワーク伝送路やLANカードなどでのロスタイムが生じることがあり、本当にクリティカルな場面での適正なマルチユーザーが実現できない場合があります。これは、Jetエンジンの性格から来る限界であり、よく承知しておく必要があります。

 

参考文献

本稿をまとめる上で参考にした文献とそのリンクを掲げておきます。ぜひ、原文献に当たってみてください。

  1. Microsoft Jet データベースエンジンプログラマーズガイド 改訂新版, Dan Haught,Jim Ferguson, アスキー(マイクロソフト・プレス),ISBN4-7561-2189-6
    (現在品切れ状態)

  2. White Paper "Microsoft Jet 3.5 Performance Overview and Optimization Techniques"
    http://download.microsoft.com/download/access97/whitep6/1/WIN98/EN-US/V35perf.exe
  3. HOWTO: Implement Multi-user Custom Counters in DAO 3.5
    http://support.microsoft.com/support/kb/articles/Q191/2/53.ASP
    [VB] DAO でユーザー定義カウンターを実装する方法
    http://www.microsoft.com/JAPAN/support/kb/articles/j046/2/69.htm
  4. HOWTO: Implement Multiuser Custom Counters in Jet 4.0
    and ADO 2.1
    http://support.microsoft.com/support/kb/articles/Q240/3/17.ASP
  5. HOWTO: Synchronizing Reads and Writes Between Two DAO
    Processes
    http://support.microsoft.com/support/kb/articles/Q180/2/23.ASP
  6. HOWTO: Synchronize Writes and Reads with the Jet OLE DB
    Provider and ADO
    http://support.microsoft.com/support/kb/articles/Q200/3/00.ASP
  7. PRB: Single-User Concurrency Problems With ADO and Jet
    http://support.microsoft.com/support/kb/articles/Q216/9/25.ASP
  8. DAO から ADO への移植――Jet Provider と組み合わせた ADO の使用
    http://www.microsoft.com/JAPAN/developer/data/techmat/ado/dao2ado_8.asp
    http://msdn.microsoft.com/library/en-us/dndao/html/daotoadoupdate_topic9.asp
  9. Understanding Microsoft Jet Locking, Kevin Collins
    http://www.microsoft.com/accessdev/articles/jetlund.htm

※ 本稿における誤りおよび疑問点は、support@canalian.comまでお寄せください。