NFSベースのストレージを使用する場合は、2つの注意事項があります。ただし、注意しなければならない理由は、Berkeley DB Java EditionとBerkeley DBでは異なります。 第1に、JEでは、下層のストレージ・システムの責任でデータを確実に永続化する必要があります。すなわち、write()が呼び出された場合にはオペレーティング・システム・レベルで、fsync()が呼び出された場合には物理ディスク・レベルで永続化を実行する必要があります。 ただし、一部のリモート・ファイル・システム・サーバーでは、(パフォーマンスを最適化するため)書込み要求をサーバー側のキャッシュに格納し、データを物理ディスクに書き込む前にクライアント(この場合はJE)に制御を戻します。 JE環境ディレクトリのディスクがローカル・マシン上に存在する場合は、この動作でも問題ないのですが、リモート・ファイル・システムを使用した構成では、一般に(ファイル転送に)ステートレスなプロトコルを使用するため、問題が発生する可能性があります。 問題が発生するシナリオを以下に示します。(1)まず、JEがwrite()を呼び出します。(2)サーバーはデータを受信しますが、サーバーのディスクに物理的に書込んでいない状態です。(3)サーバーがwrite()から復帰してクライアントに制御を戻します。(4)その後、サーバーがクラッシュします。 このようなケースで、サーバーがクラッシュしたことをクライアント(JE)が認識していない場合(つまり、プロトコルがステートレスの場合)、後でJEがwrite()を正常に呼び出し、ログ・ファイルにデータが書き込まれたときに、JEのログ・ファイルに欠落部分ができて、データが破損する可能性があります。
JE 3.2.65以降のリリースでは、新しいパラメータje.log.useODSYNCが追加されました。このパラメータを指定すると、JE環境のログ・ファイルがO_DSYNCフラグを指定してオープンされます。 これにより、すべての書込みが物理ディスクに対して行われるようになります。 リモート・ファイル・システムの場合は、このフラグによって、サーバーのローカル・ディスクに対して物理的にデータが書き込まれるまでwrite()から復帰しないようにサーバーに指示が出されます。 ただし、このフラグを指定するとパフォーマンスが低下するため、ローカルの構成では決して使用しないでください。 逆に、リモート・ファイル・システム構成では必ずこのフラグを指定します。そうしないとデータが破損する可能性があります。
リモート・ファイル・システム構成でJEを使用する場合は、複数のファイル・システム・クライアントでシステムを構成しないようにしてください(つまり、複数のホストが1つのファイル・システム・サーバーにアクセスする構成は許可されません)。 この構成では、クライアント側にデータがキャッシュされ、各クライアント(JE)間でログ・ファイルの同期がとれなくなり、データが破損する可能性があるからです。 この問題を解決するには、O_DIRECTフラグを指定して環境ログ・ファイルをオープンするしかありませんが、この方法はJava VMでは使用できません。
第2に、Java Edition(JE)では、java.nio.channels.FileChannel.lock()によるファイル・ロック機能を使用しています。 Javaでは下層の実装を規定していませんが、大部分の実装ではおそらく、flock()システム・コールを使用しているものと思われます。 flock()がNFS越しに動作するかどうかはプラットフォームによって異なります。 Webで検索すると、Linux上でflock()が誤って正常ステータスを返してしまうというバグ・レポートが見つかります。
JEがファイル・ロックを使用する理由は次の2つです。
NFSでflock()を使用したときの問題を解決するには、当然ですが、JE環境にアクセスするときに単一のプロセスを使用するのがもっとも簡単な方法です。それができない場合で、なおかつNFS越しにflock()を呼び出す方法に依存できない場合には、アプリケーションの責任でライター・プロセスが1つしか接続されないようにします(1)。 1つの環境内で2つのライター・プロセスを使用すると、データベースが破損する可能性があります (これはプロセス・レベルの問題であってスレッド・レベルの問題ではない点に注意してください)。
ログ・クリーニングの問題(2)もアプリケーション・レベルで解決できますが、より複雑になります。 それには、Environmentにアクセスするプロセスが複数存在する場合は常に、ログ・クリーナーを無効にする(je.env.runCleanerプロパティをfalseに設定する)必要があります。 ファイルの削除操作に対するロック制御を正しく行わないと、リーダー・プロセスでたびたびcom.sleepycat.je.log.LogFileNotFoundExceptionが発生し、最新のスナップショットを取得するために、いったんクローズしてから再度オープンする必要があります。 こうした例外は極めてまれにしか発生しない場合もあれば、セットアップが機能しなくなるほど頻繁に発生する場合もあります。 ログ・クリーニングを実行する場合、アプリケーションは最初に、すべてのリーダー・プロセスがEnvironmentをクローズしている(つまり、すべての読取り専用プロセスがすべてのEnvironmentハンドルをクローズしている)ことを確認する必要があります。 すべてのEnvironmentハンドルがクローズされたら、ライター・プロセスが、Environment.cleanLog()とEnvironment.checkpoint()を呼び出してログ・クリーニングを実行します。 チェックポイントが完了すると、リーダー・プロセスは環境ハンドルを再オープンできます。
データファイルをBerkeley DBとBerkeley DB Java Edition間で共有できるかどうかについて、いくつか質問を受けています。 この2つの製品はオン・ディスク・フォーマットが異なるため、2製品間でデータファイルを共有することはできません。 ただし、両製品のデータのダンプとロード・ユーティリティ(com.sleepycat.je.util.DbDumpとcom.sleepycat.je.util.DbLoad)は同じフォーマットを使用しているため、2製品間でデータのインポートとエクスポートを行うことはできます。
また、JEのデータファイルはプラットフォームに依存しないため、マシン間で移動できます。 2つの製品はどちらもDirect Persistence Layer API、永続JavaコレクションAPI、同様のバイト配列ベースのAPIをサポートしています。
JEは、部分データに対するget()およびput()操作をサポートしています。 ただし、この機能は完全には最適化されていません。データベースに対して常にレコード全体が読み取られ、または書き込みされ、レコード全体がキャッシュされます。
ですから、現時点でget()およびput()による部分データ操作を使用する利点は、アプリケーションが用意したバッファにレコードの一部のみをコピーしたり、アプリケーションが用意したバッファからレコードの一部のみをコピーしたりするという点だけです。 この機能は、今後のリリースで最適化される可能性はありますが、それまでは、JEが高パフォーマンスLOBをサポートしているとは言えません。
部分的なデータに対するget()およびput()操作について詳しくは、こちらを参照してください。
キー・プリフィックスは、データベースへのデータ格納方法の1つで、Bツリー・キーの格納領域を節約するものです。 この方法は、同様のプリフィックスを持つ長いキーを使用したアプリケーションの場合に効果があります。 JEはバージョン3.3.62のキー・プリフィックスをサポートしています。 詳しくは、DatabaseConfig.setKeyPrefixingを参照してください。
キー圧縮については、現時点ではサポートしていません。 Berkeley DBとBerkeley DB Java Editionでキー圧縮をサポートすることを検討しましたが、使用するアルゴリズム、キー長、キーの実際の値に関して問題があることが判明しました。 たとえば、LZW圧縮アルゴリズムはうまく動作するものの、圧縮するサイズが大きくないとあまり効果がありません。 比較的サイズの小さいキーを個別に圧縮すると、LZW圧縮アルゴリズムでは、キー・サイズがかえって大きくなる可能性があります。
JE構成プロパティは、EnvironmentConfig、DatabaseConfig、StatsConfig、TransactionConfig、CheckpointConfigなどの基本API(com.sleepycat.je)のクラスを使用して、プログラムで指定できます。 レプリケーション(com.sleepycat.je.rep)パッケージを使用する場合は、ReplicationConfigを使用してReplicatedEnvironmentプロパティを設定できます。 DPL(com.sleepycat.persist)パッケージを使用する場合は、StoreConfigを使用してEntityStore構成プロパティを設定できます。 アプリケーションでは、これらのいずれかの構成クラスをインスタンス化して、目的の値を設定します。
Environment構成プロパティおよびReplicatedEnvironment構成プロパティに対しては、もう1つの構成オプションとしてje.propertiesファイルがあります。 EnvironmentConfigクラスおよびReplicationConfigクラスのget/setメソッドを使用して設定されるプロパティはすべて、環境ホーム・ディレクトリにje.propertiesファイルを作成して指定することもできます。 je.propertiesによって設定されたプロパティの方が優先されます。また、このファイルを使用すれば、アプリケーションを再コンパイルせずに構成を変更できます。 je.propertiesで指定できるプロパティはすべて、EnvironmentConfig.setConfigParamまたはReplicationConfig.setConfigParamを使用して設定することもできます。
EnvironmentおよびReplicatedEnvironmentのプロパティの一覧については、それぞれEnvironmentConfigクラスおよびReplicationConfigクラスに説明があります。 各プロパティのJavadocでは、指定可能な値、デフォルト値、可変プロパティかどうかについて説明しています。 可変プロパティは、環境をオープンした後でも変更できます。 これらのクラスに説明のないプロパティは試用段階であり、将来廃止されるものもあれば、昇格され正式に記載されるものもあります。
ID割当て機能は一般的に"シーケンス"と呼ばれます。シーケンスの機能は、SQL SEQUENCEの機能と同じです。 シーケンスの考え方は、パフォーマンスを低下させることなく値を効率的に割り当て、なおかつ値が重複しないように保証するというものです。
DPLを使用している場合は、@PrimaryKey(sequence="...")注釈でシーケンスを定義できます。 基本APIを使用している場合は、Sequenceクラスに低レベルのシーケンス機能が用意されています。使用例は<jeHome>/examples/je/SequenceExample.javaに掲載されています。
TupleBindingを使用してオブジェクトを格納している場合は、既存のデータベースを変換することなく、なおかつデータの互換性を維持したままで、フィールドをタプルに追加できます。 Direct Persistence Layer APIを介したアプリケーション・レベルのコーディングを一切行うことなく、クラスの進化がサポートされている点に注目してください。
Javadocから抜粋した次のコードは、タプル・バインディングを使用するように変更できます。 コレクションの概要
タプル・バインディングを使用すると、シリアル・バインディングに比べてディスクの消費領域を節約でき、実行速度も向上します。 ところが、いったんデータベースに書き込まれたタプルについては、そのフィールド順序を変更したり、フィールドを削除したりすることはできません。 許可されている型進化は、タプルの末尾へのフィールドの追加だけですが、これは、カスタムのバインディング実装によって明示的にサポートする必要があります。
具体的には、型変更が新規のフィールドを追加することだけに限定される場合は、TupleInput.available()メソッドを使用して、読み込まれていないフィールドが存在するかどうかをチェックできます。 available()メソッドは、java.io.InputStream.available()を実装したものです。 このメソッドは、読み込まれていないバイト数を返します。 戻り値がゼロより大きい場合は、読み込まれていないフィールドが少なくとも1つ以上存在します。
データベース・レコードの定義にフィールドを追加する場合は、TupleBinding.objectToEntryメソッドで、追加フィールドを含むすべてのフィールドを無条件に書き込む必要があります。
TupleBinding.entryToObjectメソッドでは、元々存在するすべての固定フィールドを読み取った後に、available()を呼び出します。 ゼロより大きい値が返されたら、レコードに新しいフィールドが追加されているということであり、そのフィールドを読み取ることができます。 ゼロが返されたら、そのレコードに新しいフィールドは含まれていません。
次に例を挙げます。
public Object entryToObject(TupleInput input) { // 最初に、元々存在するすべてのフィールドを無条件に呼び出す。 if (input.available() > 0) { // 1つ目の追加フィールドを読み取る } if (input.available() > 0) { // 2つ目の追加フィールドを読み取る } // その他の処理 }
以下に、JEを使用した簡単なサーブレットの例を示します。 このサーブレットでは、initメソッドでJE環境をオープンし、doGet()メソッドですべてのデータを読み取っています。
import java.io.*; import java.text.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; import com.sleepycat.je.Cursor; import com.sleepycat.je.Database; import com.sleepycat.je.DatabaseConfig; import com.sleepycat.je.DatabaseEntry; import com.sleepycat.je.DatabaseException; import com.sleepycat.je.Environment; import com.sleepycat.je.EnvironmentConfig; import com.sleepycat.je.LockMode; import com.sleepycat.je.OperationStatus; /** * 単純なサーブレット。 */ public class HelloWorldExample extends HttpServlet { private Environment env = null; private Database db = null; public void init(ServletConfig config) throws ServletException { super.init(config); try { openEnv("c:/temp"); } catch (DatabaseException DBE) { DBE.printStackTrace(System.out); throw new UnavailableException(this, DBE.toString()); } } public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { ResourceBundle rb = ResourceBundle.getBundle("LocalStrings",request.getLocale()); response.setContentType("text/html"); PrintWriter out = response.getWriter(); out.println("<html>"); out.println("<head>"); String title = rb.getString("helloworld.title"); out.println("<title>" + title + "</title>"); out.println("</head>"); out.println("<body bgcolor=\"white\">"); out.println("<a href=\"../helloworld.html\">"); out.println("<img src=\"../images/code.gif\" height=24 " + "width=24 align=right border=0 alt=\"view code\"></a>"); out.println("<a href=\"../index.html\">"); out.println("<img src=\"../images/return.gif\" height=24 " + "width=24 align=right border=0 alt=\"return\"></a>"); out.println("<h1>" + title + "</h1>"); dumpData(out); out.println("</body>"); out.println("</html>"); } public void destroy() { closeEnv(); } private void dumpData(PrintWriter out) { try { long startTime = System.currentTimeMillis(); out.println("<pre>"); Cursor cursor = db.openCursor(null, null); try { DatabaseEntry key = new DatabaseEntry(); DatabaseEntry data = new DatabaseEntry(); while (cursor.getNext(key, data, LockMode.DEFAULT) == OperationStatus.SUCCESS) { out.println(new String(key.getData()) + "/" + new String(data.getData())); } } finally { cursor.close(); } long endTime = System.currentTimeMillis(); out.println("Time: " + (endTime - startTime)); out.println("</pre>"); } catch (DatabaseException DBE) { out.println("Caught exception: "); DBE.printStackTrace(out); } } private void openEnv(String envHome) throws DatabaseException { EnvironmentConfig envConf = new EnvironmentConfig(); env = new Environment(new File(envHome), envConf); DatabaseConfig dbConfig = new DatabaseConfig(); dbConfig.setReadOnly(true); db = env.openDatabase(null, "testdb", dbConfig); } private void closeEnv() { try { db.close(); env.close(); } catch (DatabaseException DBE) { } } }
環境を作成した後、構成情報を取得するには、Environment.getConfig() APIを使用します。 次に例を挙げます。
import java.io.File; import com.sleepycat.je.*; public class GetParams { static public void main(String argv[]) throws Exception { EnvironmentConfig envConfig = new EnvironmentConfig(); envConfig.setTransactional(true); envConfig.setAllowCreate(true); Environment env = new Environment(new File("/temp"), envConfig); EnvironmentConfig newConfig = env.getConfig(); System.out.println(newConfig.getCacheSize()); env.close(); } }
この例を実行すると、次の情報が表示されます。
> java GetParams 7331512 >
Environmentを作成するときに使用したEnvironmentConfigプロパティを問い合わせるのではなく、getConfig()を呼び出す必要がある点に注意してください。
Berkeley DB Java Editionには、Concurrent Data Store(CDS)とTransactional Data Store(TDS)の2種類があります。 この2つの製品の違いは、トランザクションを使用するかどうかという点にあります。 公開APIメソッドEnvironmentConfig.setTransactional(true)を呼び出すと、名前のとおり、TDSが使用されます。
どちらの製品も複数の読取りスレッドと書込みスレッドの並列動作をサポートしており、なおかつ永続性とリカバリ可能性を備えたデータベースを作成します。 ここでは"永続性"という言葉をデータベース用語として使用します。つまり、データがディスクに書き込まれる(永続化される)ため、クラッシュ後にアプリケーションを再起動すると、クラッシュ前のデータの状態に戻るという意味です。 トランザクションを有効にすると、複数の操作を1つのアトミックな要素にグループ化したり、それらの操作を元に戻したり、永続化の粒度を制御したりできます。
たとえば、アプリケーションで2つのデータベースPersonとCompanyを使用しているとします。 新しいデータを挿入するには、2つの操作を実行する必要があります。すなわち、Personへのデータの挿入とCompanyへのデータの挿入です。 この2つの操作をグループ化して、両方の操作が正常に終了したときに初めて挿入が行われるようにするには、トランザクション制御が必要になります。
もう1つの問題は、JEでセカンダリ索引を使用するかどうかという点です。 たとえば、Personの住所フィールドにセカンダリ索引が作成されているとします。 Personデータベースとそのセカンダリ索引Addressを更新するには、JEに対して1つのメソッドを呼び出すだけで済みますが、この更新をアトミックに行うにはトランザクションを使用する必要があります。 そうしないと、ある時点でシステムがクラッシュしたとき、Personは更新されているがAddressは更新されていないという状態になる可能性があります。
トランザクションを有効にすると、Transaction.abort()を呼び出すことで、一連の操作を明示的に元に戻すことができます。 トランザクションを無効にした場合、すべての変更は、API呼出しから復帰した時点で確定します。
また、トランザクションを使用すると永続化を細かい粒度で制御できます。 Transaction.commitを呼び出すと、変更内容が永続化されリカバリ可能となることが保証されます。 CDSでは、トランザクションを使用しないため、永続性とリカバリ可能性を保証するには、最後にEnvironment.sync()を呼び出した時点まで戻る必要があります。これはコストの大きな操作になります。
Transaction.commitには、永続性とパフォーマンスのバランスを取るためにいくつかの種類が用意されています。これについては、このFAQの該当する項目を参照してください。
要約すると次のようになります。まず、CDSは次の場合に使用します。
TDSは次の場合に使用します。
2つの製品は1つのダウンロード用jarファイルにまとめられています。 どちらの製品を使用するかはライセンスの問題です。どちらを使用してもインストールに影響はありません。
はい。"表"がデータベース、"行"がキー/データのペア、"列"がアプリケーションでカプセル化されたフィールドにそれぞれ対応します。 アプリケーションは、個々のフィールド、つまりデータ値の"列"にアクセスするためのメソッドを自分で用意する必要があります。
データベースをオープンしたままにしても消費されるメモリ領域はごくわずかです。 アプリケーションでは一般に、環境を開いている間はデータベースをオープンしたままにしておきます。 例外は、多数のデータベースを使用するけれども、同時にアクセスする必要があるのはごく一部のデータベースだけというケースです。
オープンしているデータベースの数が多過ぎるためアプリケーションでメモリ不足が発生していることが分かった場合は、処理に必要なデータベースだけをオープンするようにしてみてください。 使用するたびにデータベースをオープンすることに伴うオーバーヘッドが原因で無視できないくらいパフォーマンスが低下する場合は、オープン状態のデータベースのハンドルをプールすることも検討します。
最小のキャッシュ・サイズは96KB(96 * 1024)です。 キャッシュ・サイズを設定するには、環境作成時に使用するEnvironmentConfigインスタンスに対してEnvironmentConfig.setCacheSize(96 * 1024)を呼び出すか、je.propertiesファイルのje.maxMemory propertyを設定します。
共通のJavaインタフェース・セットを用意して、そのインタフェース・セットをBerkeley DB JE APIとBerkeley DBのJava APIで実装することが合理的な選択かどうかについては、検討したことがあります。 この点は、JEの設計時にも検討しましたが、共通のJavaインタフェース・セットは用意しないという結論に達しました。理由は、一般に、そうした共通化を行うと、これらの"通常の"ユーザーはかえって混乱すると思われるからです。
JEを実行するには、Java SE 1.5以降が必須です。 今のところ、J2MEをサポートする予定はありません。
je.jarとアプリケーションのjarファイル(特に、SerialBindingによってシリアライズされるクラス)が同じクラス・ローダーのもとでロードされる点は重要です。 このため、サーブレット内で実行する場合は通常、je.jarとアプリケーションのjarファイルを同一ディレクトリに置く必要があります。
また、JVMの拡張ディレクトリにje.jarを格納してはならない点も重要です。 je.jarは、アプリケーションのjarファイルと同じ場所に格納してください。 拡張ディレクトリは、権限ライブラリ・コード用に予約されています。
je.jarがWEB-INF/libとearプロジェクトの両方に存在していたために、WebSphere Studio(WSAD)アプリケーションでクラスのロード時に問題が発生したケースがありました。 この問題は、earプロジェクトからje.jarを削除して解決しました。
JE環境に対して読取りおよび書込みを実行する場合は、JEのje.lckおよび*.jdbファイルが存在するディレクトリに読取り/書込みアクセス権を与えます。
JEに対して読取り専用アクセスを行う場合は、次のどちらかを実行します。
JEはJE環境ディレクトリが書込み可能であることを検出すると、je.lckファイルへの書込みを試みます。 また、JE環境ディレクトリが書込み禁止であることを検出すると、JE環境が読取り専用でオープンされていることを確認します。
Eclipseにm2eclipse Mavenプラグインをインストールしており、JEベースのプロジェクト向けにEclipseでMavenプロジェクトを作成しているとします。 この状態でBerkeley DB JE Mavenをデプロイするには、プロジェクトのpom.xmlにBerkeley DB JEの依存性とリポジトリを追加する必要があります。
<dependencies> <dependency> <groupId>com.sleepycat</groupId> <artifactId>je</artifactId> <version>4.0.103</version> </dependency> </dependencies>
必要なJEのバージョンがJE Mavenに含まれていることを確認してください。 JEの最新バージョンは次の場所にあります。 http://www.oracle.com/technology/software/products/berkeley-db/index.html
<repositories> <repository> <id>oracleReleases</id> <name>Oracle Released Java Packages</name> <url>http://download.oracle.com/maven</url> <layout>default</layout> </repository> </repositories>
変更したpom.xmlを保存します。 これで、(EclipseのMaven環境設定セクションで"Download Artifact Sources"と"Download Artifact JavaDoc"を選択している場合に)m2eclipseによってプロジェクト用のJEのjarおよびJavadocが自動的にダウンロードされ構成されます。
com.sleepycat.je.LockConflictExceptionが発生する原因としてもっとも多いのは、2つ以上のトランザクションが、互いに相手が獲得しているロックの解放待ちの状態となり、デッドロックに陥ってしまうケースです。 次に例を挙げます。
ロック・タイムアウト・メッセージを読むと、競合の原因が分かることがあります。 デフォルトのタイムアウト・メッセージには、競合が発生しているロック元プロセス、そのトランザクション、ロックの解放待ちのプロセスなどが表示されますが、トレースを有効にすることで、ロックが獲得された時点のスタック・トレースを表示することもできます。
デッドロック・メッセージにスタック・トレースを含めるには、je.propertiesファイルまたはEnvironmentConfigにje.txn.deadlockStackTraceプロパティを設定します。 メモリの消費量とプロセス処理コストの増大を抑えるため、スタック・トレースを有効にするのはデバッグ中だけに限定してください。
スタック・トレースを有効にすると、競合対象に関する詳細な情報が得られますが、相手トランザクションが獲得しているロックを調べる必要がある場合もあります。 これは、アプリケーションの現在のアクティビティに関する情報を介して、あるいは、je.txn.dumpLocksプロパティを設定することによって知ることができます。 je.txn.dumpLocksを設定すると、デッドロック例外メッセージにロック表全体のダンプが表示されるので、デバッグに使用できます。 ロック表全体の出力はかなりのサイズになることがありますが、レコード間のロック関係を調べるには有用です。
また、デッドロック自体に影響を与えるわけではありませんが、競合が発生するアプリケーションにおいて、ロック・タイムアウトのデフォルト設定値(je.lock.timeoutまたはEnvironmentConfig.setLockTimeout()で指定)が大きすぎることがあるため、注意が必要です。タイムアウト値を小さくすると、スループットが向上します。 ただし、タイムアウト値を小さくすることで改善されるのはパフォーマンスだけであって、デッドロックが解消されるわけではありません。
JE 4.0以降のリリースの4.x.y系、およびJE 3.3.92以降のリリースの3.3.x系では、NIOパラメータのje.log.useNIO、je.log.directNIO、je.log.chunkedNIOが廃止されました。 これらのパラメータを設定しても効果はありません。
JE 3.3.91以前のリリースでは、これらのNIOパラメータは機能しますが、JEでデータが破損する既知のバグがあるため、決して使用しないでください。
スレッドを停止するために、あるいはスレッド間の連携を行うために、アクティブなJEスレッドに対してThread.interrupt()を呼び出すことはお勧めできません。 JEの操作を実行しているスレッドを中断すると、データベースの状態が不定になります。 これは、操作が異常終了したときに、JEはI/O処理の最中であった可能性があるためです。これにより、あり得るすべての結果を調べて対処することが極めて難しくなります。
JEは、スレッドが中断されたことを検出すると、その環境を使用不可としてマーキングし、RunRecoveryExceptionをスローします。 これによってユーザーは、環境を再度使用するために、いったんクローズしてオープンし直す必要があることが分かります。 RunRecoveryExceptionがスローされない場合は、ほとんどの場合、あまり意味のない別の例外が発生するか、単に破損したデータが表示されます。
したがって、スレッド間の連携にはObject.notify()やwait()などの他のメカニズムを使用する必要があります。 たとえば、各スレッドで、実行中であることを判別する"keepRunning"といった変数を使用します。 スレッド内でこの変数をチェックして、変数の値がfalseならスレッドを終了します。 スレッドを停止する場合は、この変数にfalseを設定します。 スレッドが、他の作業を実行するためにウェイクアップされるのを待っている場合は、Object.notifyを使用してウェイクアップします。 スレッド間の連携には、この方法を推奨します。
何らかの理由でスレッドの実行を完全に中止する必要がある場合は、RunRecoveryExceptionが発生するのを待つようにします。 各スレッドでは、この例外を停止せよという指示とみなす必要があります。
Windows 7(ビルド7600時点)には、特定の条件下でJE 3.3.91以前のリリースが原因となって発生するI/Oバグがあると考えています。 このバグによってファイルが破損し、その後JEのチェックサム・メカニズムによってその破損が検出されます。 JE 4.0(およびそれ以降)には、このバグの発生を防ぐ"書込みキュー"メカニズムがJEに組み込まれています。 JE 3.3.91以前のリリースはWindows 7より前に出荷されたものであるためバグには気づかず、バグの発生を防ぐためのコードを含めることはできませんでした。 JE 3.3.92では、Windows 7で稼働しているかどうかを検出してバグの発生を防ぎます。 このバグについては、Microsoftに報告済みです([#17865])。
はい、可能です。 com.sleepycat.je.XAEnvironmentクラスは、javax.transaction.xa.XAResourceインタフェースを実装したもので、2フェーズ・コミット・トランザクションを実行するときに使用します。 このインタフェースには、2フェーズ・コミットに関連するメソッドとして、start()、end()、prepare()、commit()、rollback()が定義されています。 XAEnvironment.setXATransaction()は、ユニット・テスト用に公開されている内部エントリ・ポイントです。
XAの仕様には、暗黙のトランザクション(スレッドに関連づけられ、JE APIに渡す必要のないトランザクション)の概念が含まれています。これはJE 2.0でサポートされています。 XAResource.start()メソッドにより、JEトランザクションを作成し、呼出しスレッドに結合できます。 トランザクションをスレッドから切り離すには、end()メソッドを使用します。 スレッド結合トランザクションを使用した場合は、(get()やput()などのメソッドを介して)JE APIにTransaction引数を渡す必要はありません。 スレッド結合トランザクション環境にNULLを渡すと、暗黙のトランザクションを使用するようJEに指示が出されます。
以下に、XAEnvironmentと2フェーズ・コミットを使用した簡単な例を示します。
XAEnvironment env = new XAEnvironment(home, null); Xid xid = [something...]; env.start(xid, 0); // スレッド結合トランザクションが作成される ... transaction引数にnullを指定してget/putなどを呼び出すと、暗黙的トランザクションが使用される... env.end(xid, 0); // このスレッドと結合トランザクションとの関連を解除 env.prepare(xid); if (commit) { env.commit(xid, false); } else { env.rollback(xid); }
Berkeley DBには、問合せ言語は用意されていません。 その代わり、問合せを実行するためのAPIメソッドが用意されています。これらの問合せは、プライマリ索引またはセカンダリ索引の検索用として実装できます。 ワイルド・カード問合せおよび非キー問合せでは、索引全体をスキャンして、各キーまたは値(またはその両方)を調べる必要があります。
SQLデータベース、または問合せ言語が使えるその他のデータベース製品では、ワイルド・カード問合せまたは非キー問合せを実行すると、全索引スキャンが実行されます。 Berkeley DBでは、索引をスキャンするループをコーディングします。 ループをコーディングする必要はあるものの、SQL処理が必要なくなるので、SQLデータベースよりもパフォーマンスが高くなります。
Berkeley DBでは、単純なキー検索に加えて、接頭辞または範囲の問合せもサポートしています。 範囲問合せを使用すると、所定の範囲内のすべてのキーを検索したり、特定の接頭辞で始まるすべてのキーを検索したりできます。 基本APIによる範囲問合せについて詳しくは、以下を参照してください。
DPL APIによる範囲問合せについて詳しくは、以下を参照してください。
JE 4.0では、キー限定問合せを実行できます。この際に分離レベルをReadUncommittedとして構成すると、I/Oを大幅に削減できます。 JEのデータ・レコードは個別に保管されるため、データ・レコードがキャッシュに存在しない場合でもデータ・レコードの読取りI/Oは実行されません。 ただし、その他の分離レベルを使用する場合は、レコードのロックのためにデータ・レコードを読み取る必要があるためI/Oが発生します。
基本APIを使用してReadUncommittedのキー限定問合せを実行するには、DatabaseまたはCursorの任意のメソッドを使用して問合せを実行し、次のように指定します。
DPLを使用してReadUncommittedのキー限定問合せを実行するには、任意のEntityIndexを使用して問合せを実行し、次のように指定します。
Berkeley DBが直接サポートしているのは、1つのプライマリ・データベースの複数のセカンダリ・キーに対する論理積(AND)操作だけです。 2つ以上のプライマリ・データベースを結合することはできません。 DPLを使用している場合も同じことが当てはまります。 すなわち、1つのプライマリ索引の複数のセカンダリ・キーに対する結合操作しか実行できません。
たとえば、誕生日と好みの色という2つのセカンダリ・キーを持つPersonというプライマリ・データベース(または索引)があるとします。 結合APIを使用すると、特定の誕生日で特定の色が好みのすべてのPersonレコードを検索できます。
基本APIを使用している場合は、Database.joinメソッドを参照してください。 DPLを使用している場合は、EntityJoinクラスを参照してください。
複数のプライマリ・データベース(または索引)に対して結合操作を実行するには、一方のデータベース(または索引)の各レコードについて、また残りの1つ以上のデータベース(または索引)の検索についてループをコーディングする必要があります。
SQLデータベース、または問合せ言語が使えるその他のデータベース製品でも、結合操作を実行すると、同じようなループ処理が行われます。 Berkeley DBでは、結合操作を実行するためのループをコーディングする必要はあるものの、SQL処理が必要なくなるので、SQLデータベースよりもパフォーマンスが高くなります。
セカンダリ・データベースによって索引づけされた、ステータス、部門、給与という3つのフィールドを持つ単一のプライマリ社員データベースを使用したアプリケーションを例に考えてみます。 ユーザーは、特定のステータス、特定の部門、給与の範囲を指定して問合せを実行したいと考えています。 Berkeley DBでは結合をサポートしており、結合APIを使用して特定のステータスと特定の部門のAND(論理積)を選択できます。 ただし、結合APIを使用して給与の範囲を選択することはできません。 Berkeley DBでは範囲検索もサポートしているため、給与索引などのセカンダリ索引を使用して範囲内の値を反復処理することができます。 しかし、範囲検索と結合を自動的に組み合わせる方法は用意されていません。
範囲検索と結合を組み合わせるには、まず、Berkeley DB APIを使用して一方の操作を実行し、次に、その結果に対して"フィルタ"を適用するという形で他方の操作を実行します。 したがって、次の2つの方法が考えられます。
どちらの方法がより高いパフォーマンスを実現できるでしょうか。それは、結合と範囲問合せで、どちらが生成される結果セットの平均サイズが小さいかによります。 結合の方が生成される結果セットが小さくなる場合は、2番目の方法を使用し、そうでない場合は1番目の方法を使用します。パフォーマンスが重視される問合せでは、次に示す3番目の方法を検討してください。 事前に割当て済みの給与範囲に対してセカンダリ索引を作成します。たとえば、10,000~19,999ドルの範囲の給与に対してセカンダリ・キー1を、20,000~29,999ドルの範囲の給与に対してセカンダリ・キー2を割り当てる、といった具合です。
これらの給与範囲のいずれか1つが指定された問合せの場合は、3つのセカンダリ索引をすべて使用して結合を実行します。結合後のフィルタリングは必要ありません。 複数の範囲にまたがる問合せの場合は、複数の結合操作を実行してから、結果の論理和を求めます。 範囲の一部だけが指定された問合せの場合は、条件に一致しない結果を除外する必要があります。 この作業はかなり面倒になるかもしれませんが、必要ならそうした方法も可能です。 このような最適化を実行する前に、必ず問合せのパフォーマンスを計測しておき、最適化後に効果があったことを確認してください。
指定範囲を事前に定義した範囲だけに限定することができれば、条件指定も簡単になり、パフォーマンスも大幅に向上します。 この場合は、常に1つの結合操作だけ実行すればよく、フィルタリングは必要ありません。 この方法が実用的かどうかは、事前定義の範囲を使用するように問合せを制約できるかどうかによります。
一般に、範囲検索では、Cursor.getSearchKeyRangeメソッド、またはSortedSet.subSetメソッドとSortedMap.subMapメソッドを使用します。前者は基本APIを使用しているとき、後者はコレクションAPIを使用しているときに使用します。 どちらでも好みの方法を選択できます。
Cursor.getSearchKeyRangeを使用する場合は、getNextを呼び出して、結果レコードを順次チェックする必要があります。 この方法では、getNextによって返されるキーを確認して、自分で範囲の終わりをチェックする必要があります。 Cursor.getSearchKeyRangeでは、範囲の終わりを自動的に検出することはできません。
コレクションAPIを使用する場合は、subMapまたはsubSetを呼び出して、結果コレクションに対するイテレータを取得します。 このイテレータを使用すれば、範囲の始めと終わりが自動的に検出されます。
セカンダリ・データベースに重複値ソートが設定されている場合は、プライマリ・レコードの他のフィールドで重複値をソートできます。 たとえば、セカンダリ・キーがF1で、プライマリ・レコードにある別のフィールドF2を使用して重複値をソートするとします。 このとき、セカンダリ・キーとしてF1を使用し、重複値をF2でソートする場合を考えてみます。
Berkeley DBでは、セカンダリ・データベースの"データ"がプライマリ・キーになります。 セカンダリ・データベースで重複値が許される場合、重複値比較関数は、単純にそれらのプライマリ・キーの値を比較します。 したがって、重複値比較関数を使用してF2でソートすることはできません。なぜなら、比較関数ではプライマリ・レコードを使用できないからです。
Berkeley DBで提供されているキーおよび重複値比較関数の目的は、単純なバイト単位の比較によらない別の方法で値をソートできるようにすることです。 キーまたは重複エントリに含まれないレコード・データを使用して、キーまたは重複値をソートする方法を提供することが目的ではありません。 比較関数は非常に頻繁に呼び出される(Bツリー操作が実行されるときは常に呼び出される)ので、迅速に比較できることが重要になります。
F2でソートするには、次の2つの方法があります。
重複レコードとは、1つのデータベース内にある、同じキーを持つ複数のレコードのことです。 1つのキーに対して複数のレコードが存在するため、キーによる単純な検索は、そのキーに対するすべての重複レコードを検索するのには不十分です。
DPLを使用している場合は、重複レコードにアクセスするためのもっとも簡単な方法は、SecondaryIndex.subIndexメソッドを呼び出すことです。
基本APIを使用している場合は、目的のキーにカーソルを置き、後続のすべての重複レコードを取得する必要があります。 Getting Started with Berkeley DB Java Editionにそれぞれの方法について詳しい説明があります。カーソルを置く方法については Searching For Recordsを、残りの重複レコードを取得する方法については Working with Duplicate Recordsを参照してください。
Bツリーに基づく大半のデータ・ストアがそうであるように、Berkeley DB Java Editionは重複しないレコードの数を格納しないため、結果セットのサイズを取得するには、何らかのJEの内部的な処理、またはアプリケーション・レベルでのレコード走査が必要になります。 これは、一般に、リレーショナル・データベースにも当てはまります。実はリレーショナル・データベースでも、SQL文が実行された時点で、内部的にレコード数をカウントしています。 Berkeley DB Java Edition 3.1.0では、Database.count()メソッドが導入されました。このメソッドは、データベース内のすべてのキー/データ・ペアの数を返します。 このメソッドは、内部的に実装され最適化された方法でレコード走査を実行します。このメソッドによって、キャッシュ内のワーキング・セットが影響を受けることはありませんが、データベース内で並列に変更が行われると正確な結果が返されないことがあります。
トランザクションによって最新のレコード数を取得する、あるいは結合の結果に含まれるレコード数を取得するには、次のコードを実行します。
cursor = db.openCursor(...)
または
db.join(someCursors); count = 0; while(cursor.getNext(...) == OperationStatus.SUCCESS) { count++; }
アプリケーションでレコード数カウンタを実装する場合は、いくつかの方法で最適化できます。
count = 0; while (cursor.getNextNoDup(...) == OperationStatus.SUCCESS) { count += cursor.count(); }
このようにすれば、3000レコードすべて読む代わりに、3レコード(一意なキーごとに1レコード)読むだけで済みます。
ファントムとは、あるスレッドで一連の操作を実行している最中に、別のスレッドからレコードが挿入された場合に現れるレコードのことです。 たとえば、キー検索を実行しても見つからなかったレコードが、後で、同じキーで検索すると見つかった場合、そのレコードは別スレッドによって挿入されたものです。このようなレコードをファントムといいます。
ファントム、およびトランザクション・アプリケーションでファントムの出現を防ぐ方法については、Writing Transactional Applications with Berkeley DB, Java EditionのConfiguring Serializable Isolationの項を参照してください。
ファントムの出現は防ぎたいが、トランザクションは使用できないという場合があります。 たとえば、Deferred Writeを使用している場合には、トランザクションは使用できません。 キー検索の後にファントムが出現する場合は、putNoOverwriteを使用して挿入操作を実行するループを実行し、挿入に失敗したらキー検索を実行するようにすれば、ファントムの出現を防ぐことができます。
以下に、基本APIを使用してこの処理を実装したコードの概略を示します。
Cursor cursor = ...; DatabaseEntry key = ...; DatabaseEntry insertData = ...; DatabaseEntry foundData = ...; boolean exists = false; boolean done = false; while (!done) { OperationStatus status = cursor.putNoOverwrite(key, insertData); if (status == OperationStatus.SUCCESS) { /* 新しいレコードが挿入される */ exists = false; done = true; } else { status = cursor.getSearchKey(key, foundData, LockMode.RMW); if (status == OperationStatus.SUCCESS) { /* 既存のレコードが見つかる */ exists = true; done = true; } /* その他の場合はループを続行 */ } }
putNoOverwriteが成功した場合は、挿入されたレコードに対してカーソルが書込みロックを獲得するため、他のスレッドはそのレコードを変更できません。 putNoOverwriteが失敗した場合は、そのレコードがすでに存在するため、キー検索を実行してロックします。 検索に成功すると、既存のレコードに対してカーソルが書込みロックを獲得するため、他のスレッドはそのレコードを変更できません。 検索に失敗した場合は、別のスレッドが当該レコードを削除したということなので、ループに戻ります。
この手法では、"カーソル安定性"と呼ばれるJEカーソルの特性を利用しています。 カーソルは、特定のレコードに位置づけられると、他のスレッドでどのような操作が実行されても、その位置を維持します。 カーソル内の現在位置レコードはロックされるため、他のスレッドから変更することはできません。 この動作は、トランザクションを使用しているかどうかに関係なく、また遅延書込みを使用している場合でも、一貫しています。
この手法を使用する場合は、カーソルを使用してロックを獲得する必要があります。 Database.get、Database.putNoOverwrite、およびその他のDatabaseメソッドは、明示的なトランザクションの実行中に使用した場合のみ、ロックを獲得します。 これは、対応するDPLメソッドEntityIndex.getやPrimaryIndex.putなどにも当てはまります。
synchronizedやjava.util.concurrentなどで独自のロック手法を実装するより、この手法を使用することをお勧めします。 カスタムのロック機能はエラーが起こりやすいうえ、ほとんどの場合必要ありません。
ちなみに、"内容を保証しない読取り"(LockMode.READ_UNCOMMITTED)を実行した場合のJEのカーソル安定性の動作は若干異なります。 内容を保証しない読取りでは、カーソルの現在位置が特定のレコードに固定される点は同じですが、他のスレッドによる当該レコードの変更と削除が許可されます。
複数のスレッドで1つのデータベース・インスタンスを使用することは可能であり、JE4.0ではパフォーマンスは低下しません。
JE 3.3以前では、複数のスレッドで1つのデータベース・インスタンスを使用すると、若干のボトルネックが発生しました。 問題は、特定のDatabaseオブジェクトが、オープンされるカーソル・セットを保持する点にあります。 このカーソル・セットを使用して、close()が呼び出されたとき、すべてのカーソルがデータベースに対してクローズされたかどうかが確認されますが、それには、データベースを更新する前にJEがデータベースと同期する必要があります。 このため、複数のスレッドが同じデータベース・ハンドルを共有していると、同期化によるボトルネックが発生することになります。 マルチスレッドで処理を行う場合は、データベース・ハンドルを共有するもっともな理由がない限り、スレッドごとに個別のハンドルを使用することをお勧めします。
JE 2.1.30で、パフォーマンスを向上するため、2つの新しいロック・オプションが追加されました。
この2つのモードの一方がロックなしモードです。 EnvironmentConfig.setLocking(false)を指定すると、すべてのロックが無効となり、アプリケーションはロックによるオーバーヘッドから解放されます。 ロックなしモードを使用する場合は注意が必要です。 このモードは非トランザクション環境でしか使用できないため、アプリケーションは、データベース上で並列に動作するアクティビティが存在しないことを確認する必要があります。 ロックなしモードで並列に動作するアクティビティが存在すると、データベースが破損する可能性があります。 また、ロックなしモードではログ・クリーニングが無効になるため、アプリケーションが明示的にEnvironment.cleanLog()メソッドを呼び出して、ログ・クリーニングを行う必要があります。
2つのモードのもう一方では、je.lock.nLockTablesプロパティを使用します。このプロパティにはロック表の数を指定できます。 デフォルトは1で、この値を大きくするとマルチスレッド処理の同時実行性を向上できます。 このプロパティの値は素数で指定する必要があります。同時実行スレッド数を超えない最大の素数を指定するのが理想的です。
次のパラメータを指定してDbCacheSizeを実行することから始めましょう。
-records <count> # 全レコード数(キー/データのペア)、必須 -key <bytes> # 1レコードあたり平均キー・バイト数、必須 [-data <bytes>] # 1レコードあたり平均データ・バイト数(省略時は # 出力にリーフ・ノードのサイズが含まれない)
詳しくは、DbCacheSizeのJavadocを参照してください。
DbPrintLog -Sを実行すると、ログ統計情報のLN(リーフ・ノード)行の平均バイト数の列に、平均レコード・サイズが表示されます。
64ビットJVMのキャッシュ・サイズを測定するには、64ビットJVMに対してDbCacheSizeを実行する必要があります。
JEキャッシュ・メモリを十分に活用するために、64ビットJVMを使用しており、なおかつ最大ヒープ・サイズが32GB未満の場合には、圧縮OOP(-XX:+UseCompressedOops)を指定することを強くお勧めします。 参照ドキュメントの説明にあるとおり、Javaコマンドで明示的に指定していない場合でも圧縮OOPがデフォルトのJVMモードとなることがあります。 ただし、圧縮OOPが必要な場合は、DbCacheSizeまたはJEアプリケーションの実行時に、Javaコマンドに明示的に指定する必要があります。 明示的に指定しない場合は、圧縮OOPがJVMのデフォルト設定となっている場合でもJEがそれを検出せず、キャッシュ・メモリ・サイズの計算時に圧縮OOPが考慮されません。
読取り/書込みを行うアプリケーションでは、JEのキャッシュ・サイズを、アクティブなデータセットにあるレコードのBツリー内部ノード(BIN)をすべて保持できる十分なサイズに設定することを強くお勧めします。 DbCacheSizeを使用して、あるデータセットの内部ノードをすべて保持できるキャッシュ・サイズを見積もることができます。 アクセス・パターンにホット・スポットがある場合は、データセット全体の一部だけがアクティブなデータセットになるようなアプリケーションも存在します。 しかし、キーに対する純粋なランダム・アクセスや、その他顕著なホット・スポットが見られないアクセス・パターンの場合は、内部ノードをすべて保持できるようにキャッシュ・サイズを設定すべきです。
JEはほとんどのデータベース製品と同様に、読取り操作または書込み操作の実行に必要となるメタデータ(JEではBツリー内部ノード)がキャッシュ内に存在する場合に、もっとも優れたパフォーマンスを発揮します。 たとえば、Bツリー(BIN)の最下位にある内部ノードがキャッシュに存在しない場合、操作を実行するたびにファイル・システムからBINをフェッチする必要があります。 その結果、多くの場合はランダム読取りI/Oが発生します(ただし、BINがファイル・システム・キャッシュに存在していればストレージ・デバイスへのI/Oは発生しません)。
さらに、書込み操作の場合、BINはダーティ状態になります。 キャッシュがBINを保持できる十分なサイズでない場合、ダーティなBINがすぐにキャッシュから追い出され、その際に書込みが実行されます。 BINの書込みはバッファに送信される可能性があります。その場合、書込みバッファがいっぱいになる、ログ・ファイルがいっぱいになる、またはその他の操作によってバッファの書込みが実行されるまでは、バッファはファイル・システムにフラッシュされません。
最終的な結果として、すべてのBINがJEキャッシュに存在しない場合は、余分な読取りと書込みが必要になります。 すべてのBINがキャッシュに存在する場合は、最初のアクセスのときのみ読み取られ、チェックポイントのときのみ書き込まれます。 チェックポイント間隔は、書込みのコストと、クラッシュ発生時のリカバリ時間短縮とのトレードオフを考慮しながら選択できます。
前の段落で説明したパフォーマンスのトレードオフは、おおまかにいえば、多くのデータベース製品に適用できます。 特にJEでは、ログの構造化記憶域システムが利用されており、他のデータベース製品とは異なる特徴があります。 BINがダーティ状態になり、キャッシュからの追い出しによってログに書き込まれるたびに、少なくとも何らかの冗長な情報がログに書き込まれます。JEの追加専用ログに保存される他の要素と同様に、BINも上書きできないからです。 ログ内の冗長なBINエントリに対して、JEログ・クリーナーによるガベージ・コレクションを実行する必要があるため、さらにコストが増大します。
一般的に、JEログ・クリーナー・スレッドの動作は、レコードの更新を実行するアプリケーション・スレッドと同様の動作となります。 ログ・ファイルをクリーニングするときに、アクティブなレコードまたは内部ノードをログの末尾にコピーする必要があります。この処理は、(レコード内のデータは変更されないものの)レコードの更新と非常に似ています。 このような更新処理は、キャッシュに必要なBINが存在しないときにBINをキャッシュにフェッチする必要がある、その後BINが更新によってダーティ状態となる、さらにダーティなBINがキャッシュから追い出されてすぐにログにフラッシュされる可能性があるという意味で、アプリケーション主導の更新処理とほぼ同じです。 キャッシュ・サイズが小さく、アプリケーションの書込み率が高い場合、キャッシュからの追い出しとログ・クリーニングを繰り返す負のフィードバック・サイクルが生じ、パフォーマンスに重大な影響を及ぼす可能性があります。 おそらくこの点が、JEのキャッシュ・サイズを、アクティブなデータセット内の内部ノードをすべて保持できるサイズに設定すべきもっとも重要な理由となります。
なお、リーフ・ノード(LN)は前の2段落で説明した問題の影響を受けません。 リーフ・ノードはレコード・データを保持し、操作(挿入、更新、削除)の実行時にログに記録されます。これは、内部ノード(メタデータ)が操作のたびにダーティ状態になり、後のチェックポイントおよびキャッシュからの追い出しの際にログに記録されるのとは対照的です。 そのため、リーフ・ノードを保持できる十分なキャッシュ・サイズを設定すべきかどうかを判断する際に、前述の問題を考慮する必要はありません。 リーフ・ノードはJEキャッシュ、ファイル・システム・キャッシュ、またはその2つに保持される可能性があります。 実際、多くの場合は、操作の完了後JEキャッシュからすぐにリーフ・ノードを追い出す(そしてリーフ・ノードの保持をファイル・システム・キャッシュに任せる)方が、JVMのGCのコストが低くなるため効果的です。詳しくは、CacheMode.EVICT_LNを参照してください。 DbCacheSizeでは、内部ノードのみに必要となるメモリ容量と、内部ノードとリーフ・ノードに必要となるメモリ容量が出力されます。
読取り専用アプリケーションも、前述の問題の影響を受けません。 読取り操作のみを実行し、なおかつキャッシュで内部ノードをすべて保持していない場合、余分な読取りI/Oが必要になることがありますが、追加専用の構造化記憶域システムおよびログ・クリーナーが関係することはありません。
JEは、大半のデータベースと同様、データベース・オブジェクトがキャッシュ内に見つかったときにもっともパフォーマンスが高くなります。 JEがキャッシュからオブジェクトを削除するかどうかは、キャッシュ内オブジェクト追い出しアルゴリズムに基づいて判定されます。アプリケーションに合わせてこのポリシーを調整すると効果的です。 デフォルトのキャッシュ内オブジェクト追い出しポリシーはLRU(最低使用頻度)方式です。 キャッシュがいっぱいになると、最近使用されたデータベース・オブジェクトはキャッシュ内に残され、古いデータベース・オブジェクトがキャッシュから追い出されます。 LRU方式は、ワーキング・セットがキャッシュ内に収まる場合、一部のデータ・レコードの使用頻度が他のデータ・レコードに比べて高い場合、またはその両方が当てはまる場合に適しています。
JE 2.0.83で、LRUに代わるキャッシュ内オブジェクト追い出しポリシーが追加されました。このポリシーでは、主としてBツリー内のノード・レベルに基づいてキャッシュから追い出すオブジェクトを決定します。 次の両方の特徴を備えたアプリケーションでは、この方式のアルゴリズムを使用することでパフォーマンスを向上できます。
新しい追い出しポリシーを指定するには、je.propertiesファイルまたはEnvironmentConfigオブジェクトに次の構成パラメータを設定します。 je.evictor.lruOnly=false
Bツリーのノード・レベルに基づくアルゴリズムでは、より高いレベルのBツリー・ノードに使用頻度の低いノードがあったとしても、常に最低レベルのノードを最初に追い出します。 また、ダーティではないノードがまず追い出され、ダーティなノードはその後に追い出されます。 このアルゴリズムは、ランダム・アクセス・アプリケーションに利益をもたらします。このアルゴリズムでは、高レベルのBツリー・ノードが可能な限り長く保持されるため、ランダム・キーでアクセスしたときに、処理対象のBツリーの内部ノードがキャッシュ内に存在する可能性が高くなるからです。
je.evictor.lruOnly=falseを使用しているときに、je.evictor.nodesPerScanのデフォルト値を、デフォルトの10よりも大きな値(100など)に変更しようと考えることもあるでしょう。 nodesPerScanプロパティは、ノードを追い出すときに、候補として検討されるBツリー・ノードの数を制御します。 弊社で行ったテストでは、I/Oバウンドのシステムでこのプロパティの値を100に設定したときに良い結果が得られることが分かっています。 nodesPerScanの値が大きいほど、アルゴリズムが正確に動作するようになります。
ただし、極端に大きな値を設定しないでください。 ノードをキャッシュから追い出すたびに大量のノード数を候補として検討すると、追い出し処理のためにデータベース操作の完了が遅れ、アプリケーション・スレッドの応答時間が長くなります。 JE 4.1以降では、おもにCPUバウンドであるアプリケーションで極端に大きな値を設定すると、キャッシュからの追い出しの効率が低下することがあります。 デフォルト値から始め、段階的に値を増やしてアプリケーションにとって有益かどうかを見極めることが最善の方法です。
前述のキャッシュ管理ポリシーは、環境内のすべての操作に適用されます。 JE 4.0.103では、com.sleepycat.je.CacheModeクラスが新たに導入されました。このクラスは、操作レベル、データベース・レベル、または環境レベルでのキャッシュ・ポリシーをアプリケーションから指定するものです。 アプリケーションが特定のデータセットのアクセス・パターンを把握している場合、CacheModeの使用にもっとも適しています。 たとえば、特定のデータベースには1回だけアクセスし、再度アクセスする必要はないことをアプリケーションが把握している場合は、CacheMode.MAKE_COLDの使用が適切な場合があります。
疑わしい点がある場合は、特定のCacheModeディレクティブを使用することは避けるか、少なくともアプリケーション開発が完了し、総合的な観点でパフォーマンス・チューニングを行うまで検討を保留するのが最善の方法です。
環境統計情報を収集することは、JEのパフォーマンス・チューニングの最初のステップとして有効です。 次のコードを定期的に実行すると、過去の統計情報が表示され、次回の表示のために統計情報カウンタがリセットされます。
StatsConfig config = new StatsConfig(); config.setClear(true); System.err.println(env.getStats(config));
各フィールドについては、com.sleepycat.je.EnvironmentStatsのJavadocに説明があります。 キャッシュの動作はパフォーマンスに大きな影響を与える可能性があります。nCacheMissはキャッシュの利用率を示す指標となります。 キャッシュ・サイズ、データ・アクセス・パターン、キャッシュの追い出しポリシーを調整し、nCacheMissを監視すると良いでしょう。
トランザクションを使用するアプリケーションでは、nFSyncsをチェックして、ファイル同期という処理負荷の大きなシステム・コールの発行回数を監視してください。 コミットの永続性に関して、TxnWriteNoSyncやTxnNoSyncなど、別の方法を試してみると、パフォーマンスが向上することがあります。
nCleanerRunsとcleanerBacklogは、ログ・クリーニング動作の指標です。 je.cleaner.minUtilizationプロパティを調整することにより、ログ・クリーニング処理の回数を増やしたり減らしたりできます。 また、ログ・クリーニングをバッチで行うことにより(Environment.cleanLog()のJavadocを参照)、ログ・クリーニングの実行時期を調整できます。
nRepeatFaultReadsとnRepeatIteratorReadsの値が大きい場合は、読取りバッファ・サイズが最適ではない可能性があります。 読取りバッファの設定については、このFAQの該当する項目を参照してください。
あるユーザーから複数のデータベースを使用する場合の利点と欠点について質問がありました。 質問の内容は次のとおりです。 各システムが最低100人の顧客を処理できるアプリケーションを設計しています。 現時点で、約10個のデータベースが必要です。 次の3つの方法を検討しています。
3つの方法のすべてが、JEを使用する上で実用的なソリューションです。 どの方法が最適であるかは、さまざまなトレードオフによって変わります。
それぞれの顧客のデータが論理的に分離され、管理しやすくなります。 データベースの名前の変更、切捨て、削除を効率的に実行できます(Environment.renameDatabase、truncateDatabase、removeDatabaseを参照)。ただし、ログ・ファイルを直接管理する方法や個別の環境で管理する方法(方法2)と比較すると効率的ではありません。 データベースのコピーは、DbDumpおよびDbLoadユーティリティ、またはカスタムのユーティリティを使用して実行できます。
方法1を使用すると、複数の顧客のレコードに対して1つのトランザクションを使用することが可能です。トランザクションは複数のデータベースにわたって使用でき、またすべての顧客に対して1つの環境を使用するからです。 セカンダリは複数のデータベースにわたって使用できないため、セカンダリ索引を複数の顧客にわたって使用することはできません。
データベースのオープンとクローズにかかるコストは、環境のオープンとクローズにかかるコストより低いため、顧客のオープンとクローズにかかるコストは、方法2と方法3の中間に位置します。
1顧客あたりのオーバーヘッドは方法2よりも低いですが、方法3よりも高くなります。1データベースあたりのディスク・オーバーヘッドは約3~5KBです。 1データベースあたりのメモリ・オーバーヘッドはほぼ同様ですが、オープン状態のデータベースに対してのみ発生します。そのため、アクティブに使用していないデータベースをクローズすることで、オーバーヘッドを最小限に抑えることができます。 JE 3.3.62より前のリリースでは、このメモリ・オーバーヘッドは、データベースのクローズ時にも回収されないことに注意してください。 この理由で、多くのデータベースを使用する場合、JE 3.3.62より前のリリースでは方法1はお勧めしません。
データベース数が多くなるため、チェックポイント・オーバーヘッドは方法3よりも高くなります。 チェックポイント・オーバーヘッドの重要性は、データ・アクセス・パターンおよびチェックポイント頻度によって変わります。 このトレードオフについては、以下の方法3で説明します。
データベース名を使用して顧客を特定する場合の別の問題として、Database.getDatabaseNameはマッピング・ツリー内のレコードを線形に検索するため時間がかかります。 回避策としては、アプリケーション側でDatabaseへの参照とともにデータベース名を保管します。
各顧客に個別の環境ディレクトリを用意するため、それぞれの顧客のデータが物理的に分離され、管理しやすくなります。 顧客の削除とコピーはファイル・システムの操作として実行できます。
方法2を使用すると、複数の顧客のレコードに対して1つのトランザクションを使用することは不可能です。トランザクションは複数の環境にわたって使用できないからです。 セカンダリは複数のデータベースまたは環境にわたって使用できないため、セカンダリ索引を複数の顧客にわたって使用することはできません。
データベースのオープンとクローズにかかるコストは、環境のオープンとクローズにかかるコストより低いため、方法2の顧客のオープンとクローズにかかるコストはもっとも高くなります。 リカバリ時間を最小限に抑えるため、環境のクリーニングをクローズするようにしてください。
方法2では、1顧客あたりのオーバーヘッドがもっとも高くなります。1環境あたりのメモリ容量とディスク容量のオーバーヘッドに加え、各環境向けのバックグラウンド・スレッドも発生するからです。 JE 3.3.62以降では、すべての環境に対する共有キャッシュを使用できます。 方法2では、共有キャッシュを構成し(EnvironmentConfig.setSharedCacheを参照)、オープン状態のすべての環境のキャッシュ・サイズに対する影響が拡大するのを防ぐことが重要になります。 この理由で、JE 3.3.62より前のリリースでは方法2はお勧めしません。
環境数およびデータベース数が多くなるため、チェックポイント・オーバーヘッドは方法3よりも高くなります。 チェックポイント・オーバーヘッドの重要性は、データ・アクセス・パターンおよびチェックポイント頻度によって変わります。 このトレードオフについては、以下の方法3で説明します。
それぞれの顧客のデータはキー・プリフィックスを使用して論理的に分離されますが、このプリフィックスを考慮に入れたカスタム・ユーティリティを使用して顧客を管理する必要があります。 DPL(com.sleepycat.persistを参照)では、キー・プリフィックスに基づいたキーの範囲が使いやすくなるため、方法3でDPLを使用すると便利です。
方法3を使用すると、複数の顧客のレコードに対して1つのトランザクションを使用することが可能です。すべての顧客に対して1つの環境を使用するからです。 すべての顧客に対して同じデータベースが使用されるため、セカンダリ索引を複数の顧客にわたって使用することも可能です。
データベースも環境もオープン/クローズしないため、顧客のオープンとクローズにかかるコストはもっとも低く(0に近く)なります。
データベース数および環境数がもっとも少なくなるため、方法3では1顧客あたりのメモリ・オーバーヘッドとディスク・オーバーヘッドがもっとも低くなります。 通常、顧客キー・プリフィックスを重複して保管しないようにキー・プリフィックスを構成する必要があります(DatabaseConfig.setKeyPrefixingを参照)。
環境数およびデータベース数がもっとも少なくなるため、方法3ではチェックポイント・オーバーヘッドがもっとも低くなります。 チェックポイント・オーバーヘッドの重要性は、データ・アクセス・パターンおよびチェックポイント頻度によって変わります。 このトレードオフについて以下に説明します。
データベースのルートに対するチェックポイントの方が、他の部分に対するチェックポイントよりもコストがかかります。 この点が問題になるかどうかは、アプリケーションがデータベースにアクセスする方法によります。 たとえば、1つのデータベースに1000レコードを挿入する操作と1000のデータベースに1つのレコードを挿入する操作では、前者の方が若干コストが低くなる程度です。 しかし、チェックポイント発生回数で比較すると、後者よりも前者の方がはるかに少なくて済みます。 1000のデータベースに対して1つのレコードの更新操作を行った場合、 小さなテスト・プログラムでは、チェックポイントの作成に要する時間は約730ミリ秒です。 1つのデータベースに対して1000レコードの更新操作を行った場合、 同じ小さなテスト・プログラムでは、チェックポイントの作成に要する時間は約15ミリ秒です。
さらに、各環境にはチェックポイントの必要のある情報が含まれます。そのため、各環境を個別にチェックポイントする必要があり、全体のチェックポイント・オーバーヘッドは方法2よりもいくらか大きくなります。
一般に、JEでは、ワーキング・セットがキャッシュ内に収まる場合にもっともパフォーマンスが高くなります。 ただし、Javaのガベージ・コレクションとJE間の相互作用が原因で、小さいキャッシュの方がパフォーマンスが高くなるケースもあります。
JEは、データベース・オブジェクトへの参照を保持することでアイテムをキャッシュします。 キャッシュ・サイズによって指定されたメモリ容量を超えないようにするため、使用されなくなったオブジェクトに対する参照は解放されます。解放されたオブジェクト参照はJVMのガベージ・コレクション機能によって回収されます。 大半のJVMでは、世代別ガベージ・コレクションと呼ばれる方法を採用しています。 この方法では、オブジェクトを世代別に分類し、それらに異なるガベージ・コレクション手法を適用します。 若い世代の領域にあるアイテムをガベージ・コレクションで回収する処理は低コストで済むため、"部分GC"が使用されます。長期間存続しているアイテムの回収には、コストの高い"フルGC"が使用されます。
アプリケーションに、まれにしか再利用されないデータ・レコードにアクセスする傾向があり、なおかつJEのキャッシュ・サイズが大き過ぎる場合、JEのキャッシュには、アプリケーションで必要なくなったデータ・レコードが残ったままになります。 こうしたデータ・レコードは結局は古くなり、JVMによって古いオブジェクトに分類し直されるため、結果として、フルGCの実行回数が増えます。 JEのキャッシュが小さいと、JEが自分で、そうした一度使用されたレコードを参照解除してキャッシュから追い出す頻度が高くなります。その結果、JVMによるガベージ・コレクションの対象となるのは若いオブジェクトになります。
ガベージ・コレクションが本当に問題となるのは、アプリケーションの処理速度がおもにCPUの処理能力によって決まる場合だけです。 この均衡点を見つけるには、EnvironmentStats.nCacheMissesの値とアプリケーションのスループットを監視します。 nCacheMissesが0となるような最小のサイズまでキャッシュ・サイズを削減すると、最適なパフォーマンスが得られます。 JVMのGC統計情報を有効化するのも有益な方法です (Java SE 5 JVMでGC統計情報を有効化するには、"-verbose:gc"、"-XX+PrintGCDetails"、"-XX:+PrintGCTimeStamps"の各オプションを指定します)。
JEでは、ディスクからアイテムを読み取るとき、2つのパターンに従います。 最初のパターンでは、アプリケーションがDatabaseまたはCursor操作を実行中で、キャッシュ内でアイテムを検索できないため、シングル・データベース・オブジェクト(Bツリー・ノードまたは1つのデータ・レコード)は読取りに失敗します。 2番目のパターンでは、JEが、環境のスタートアップやログ・クリーニングなどのアクティビティの代わりにログの大半を占めるシーケンシャル部分を読み取り、1つまたは複数のオブジェクトを読み取ります。
シングル・オブジェクトの読取りでは、je.log.faultReadSizeに指定されたサイズの一時バッファが使用されますが、順次読取りでは、je.log.iteratorReadSizeに指定されたサイズの一時バッファが使用されます。 これらのプロパティのデフォルト値は、<jeHome>/example.propertiesに設定されており、現時点ではそれぞれ、2Kと8Kです。
メモリの消費量を抑えるには、読取りバッファのサイズはできるだけ小さくするのが理想的ですが、同時に、大半のデータベース・オブジェクトが十分に収まるくらいの大きさも必要です。 JEでは、シングル・オブジェクトを読み取る際にデータベース・オブジェクト全体をバッファに読み取る必要があるため、読取りバッファが小さ過ぎてシングル・オブジェクトが収まらないと、何度も繰り返し読取りが行われ、リソースが無駄に消費されます。 順次読取りを実行する際には、JEは、1つのデータベース・オブジェクトの部分をつなぎ合わせることができますが、読取りバッファが小さ過ぎて順次読取りが実行できないと、データのコピーが過剰に発生します。 EnvironmentStats内のnRepeatFaultReadsフィールドとnRepeatIteratorReadsフィールドは、シングル・オブジェクトとオブジェクト順次読取りで、失敗した読取り操作の数を示します。
nRepeatFaultReadsが0より大きい場合は、je.log.faultReadSizeの値を増やしてみてください。 nRepeatIteratorReadsが0より大きい場合は、je.log.iteratorReadSizeとje.log.iteratorMaxSizeの値を調整してください。
JEのログ・ファイルは追加専用であり、挿入、削除、変更されたレコードはすべて、現在のログ・ファイルの末尾に追加されます。 詳しくは、このFAQのJEログ・ファイルは通常のログ・ファイルと大きく異なるということですが、それはどのような点でしょうか。の項目を参照してください。
新しいデータは、いったん書込みログ・バッファに格納されてからディスクにフラッシュされます。 各ログ・バッファがいっぱいになると、writeシステム・コールが呼び出されます。 各.jdbファイルが最大サイズに到達すると、fsyncシステム・コールが呼び出され、新しい.jdbファイルが作成されます。
書込みログ・バッファのサイズとJEログ・ファイルのサイズを増やすと、writeおよびfsyncシステム・コールの呼出し回数が少なくなるため、書込みのパフォーマンスが向上します。 ただし、書込みログ・バッファのサイズを決定する際には、JEの総メモリ容量(je.maxMemoryまたはEnvironmentConfig.getCacheSize()によって指定される)とのバランスも考慮する必要があります。 メモリの空きスペースを書込みログ・バッファではなく、データベース・オブジェクトのキャッシュ用領域に充てた方が効果的な場合もあります。 同様に、JEログ・ファイルのサイズを増やすと、ログ・クリーナーが効率的にログを圧縮するのが難しくなる可能性があります。
書込みログ・バッファの数とサイズは、je.log.bufferSize、je.log.numBuffers、およびje.log.totalBufferBytesの値によって決まります。 デフォルトでは、3つの書込みログ・バッファが用意され、3つ合わせてje.maxMemoryの7%の領域を消費するように設定されています。 EnvironmentStatsのnLogBuffersフィールドとbufferBytesフィールドに、現在の設定が示されています。
アプリケーションで、書込みログ・バッファの数とサイズを変更して、パフォーマンスがどのように変化するか試してみると良いでしょう。 非トランザクション・システムは、バッファの数を2つに減らすとパフォーマンスが向上します。 書込み操作の多いアプリケーションは、ログ・バッファ・サイズを増やすとパフォーマンスが向上します。 これを行うには、je.log.totalBufferBytesを目的の値に設定し、je.log.bufferSizeを総バッファ・サイズ/バッファ数に設定します。 JEでは、書込みバッファ容量をje.maxMemoryの半分の値までに制限しているため、キャッシュ・サイズを増やさないと書込みバッファを目的のサイズまで拡張できないことがあります。
多くのユーザーが、チューニングや特殊な設定を一切行わずに、トランザクションを有効にしたときと無効にしたときに、パフォーマンスが著しく異なることを確認しています。
このように大きくパフォーマンスが異なる原因は、トランザクションの永続性(ACIDのD)にあります。 トランザクションを有効にすると、デフォルトの設定では完全な永続性が実現されます。つまり、トランザクションをコミットするたびに、トランザクション・データがディスクにフラッシュされます。 これにより、アプリケーションやOSがクラッシュしてもデータのリカバリ可能性が保証されますが、データが物理的にディスクに書き込まれるため、パフォーマンスの大幅な低下というペナルティを伴います。
トランザクション(原子性、つまりACIDのA)は必要だが完全な永続性は必要ないという場合は、永続性要件を緩めることも可能です。 トランザクションを使用するときは、次の3つの永続性オプションのいずれかを選択できます。
これらの各Transactionメソッドを呼び出すこともできますし、commitを呼び出し、環境パラメータを使用してデフォルト値を変更することもできます。 トランザクションを無効にした場合は、commitNoSyncに相当する永続性が実現されます。
commitSyncのパフォーマンスは、OS、ディスク、ドライバの組み合わせによって大きく変わる可能性があります。 一部のシステムは、アプリケーションがfsyncを要求した場合でも、ディスクへのフラッシュは行わず、バッファに書き込むように設定されています。 完全な永続性を保証する必要がある場合は、OS、ディスク、ドライバの設定をそのように変更する必要があります。
複数の永続性オプションが用意されていると、次のような疑問が湧きます。 非トランザクション・アプリケーションで、あるいはcommitNoSyncまたはcommitWriteNoSyncを使用したとき、変更内容を特定の時点で明示的にディスクにフラッシュするにはどうすれば良いのでしょうか。
トランザクション・アプリケーションで、変更内容をディスクに強制的に書き込むには、次の3つの方法があります。
非トランザクション・アプリケーションで、変更内容をディスクに強制的に書き込むには、次の2つの方法があります。
Berkeley DB Java Edition(JE)は、レコードをログに追加します。そのため、レコードは書き込まれた順に、つまり"時間的"順序で保存されます。 しかし、レコードがランダムなキー順で書き込まれる場合、つまり"空間的"順序が"時間的"順序と異なる場合は、キー順での読取りはログ(ディスク)の順序での読取りとなります。 ディスクの順次読取りは、ランダム読取りよりも高速です。 キーの順序がディスクの順序と異なり、なおかつオペレーティング・システムのファイル・システム・キャッシュもJEキャッシュも"ホット"ではない場合、読取りのたびにディスク・ヘッドを移動する必要があり、通常はデータベース・スキャンの速度が低下します。
キー(空間)の順序がディスク(時間)の順序と異なる場合に読取りパフォーマンスを向上する方法の1つに、Database.preload()メソッドを使用してデータベースをJEキャッシュに事前にロードする方法があります。 preload()は、キーの順序ではなくディスクの順序に従ってレコードを読み取るよう最適化されています。 詳しくは、こちらを参照してください。
JEキャッシュは、事前ロード対象のデータを保持できる十分なサイズである必要があります。十分なサイズでない場合は、パフォーマンスに悪影響を及ぼします。
別の方法としては、レコードを書き込むアプリケーションを、キーの順序に従ってレコードを書き込むように変更します。 レコードがキーの順序に従って書き込まれる場合は、カーソル・スキャンではディスクの並べ替え順のとおりにレコードが読み取られます。 ディスク・ヘッドのスキャン中の移動回数が最小限に抑えられ、移動する場合にも常に同じ方向に移動します。
レコードの並べ替えを実行するには、さまざまな方法があります。 アプリケーションをオフラインにできる場合は、DbDump/DbLoadを使用してレコードを並べ替えることができます。 DbDumpおよびDbLoadのドキュメントを参照してください。
アプリケーションをオフラインにできない場合は、次のいずれかの方法でカーソルを使用してキーを読み取り、並べ替えを実行できます。
アプリケーションのキャッシュ・サイズが大きい場合は、通常Java GCのチューニングが必要になります。 ほとんどの場合64ビットJVM(つまり-d64)、-serverオプションを指定し、-Xmxと-Xmsでヒープ・サイズおよびスタック・サイズを設定することになります。 アプリケーションが自身のデータ用の領域を十分に確保できるように、かつ過度なフルGCの実行を抑えられるように、キャッシュ・サイズはヒープ・サイズを上限として、ある程度余裕を持って設定してください。 通常は、Concurrent Mark Sweep GCでより予測可能なGCの結果となるため、Concurrent Mark Sweep GCがこの環境にとって最適であることが分かっています。 Concurrent Mark Sweep GCは、-XX:+UseConcMarkSweepGCを使用して有効にできます。
ベスト・プラクティスとしては、-XX:-DisableExplicitGCを指定してSystem.gc()の呼出しを無効にします。
役に立つ可能性のあるその他のJVMオプションは、-XX:NewSize(値として512mまたは1024mから始める)、-XX:MaxNewSize(値として1024mを試す)、-XX:CMSInitiatingOccupancyFraction=55です。 NewSizeは通常、全体のヒープ・サイズとの関係からチューニングします。そのため、このパラメータを指定する場合は、-Xmxの値も指定する必要があります。 この値を相対的に指定できる便利な方法として、-XX:NewRatioを使用できます。 ここで提示した値は出発点に過ぎません。 実際の値は、アプリケーションの実行時の特性に応じて変わります。
また、次の記事も参照してください。
Six Things Everyone Should Know about JE Log Filesを参照してください。
ログ・ファイルについて詳しくは、Getting Started with Berkeley DB Java Editionを参照してください。
前述のFAQで、JEログ・ファイルの基本事項と古いデータの概念について説明しています。 JEパッケージには、ログ・ファイルの利用レベルの測定に使用できるユーティリティが用意されています。 DbSpaceを実行すると、ログ・ファイルの圧縮度に関する情報を取得できます。 DbSpaceの使い方を以下に示します。
$ java -jar je.jar DbSpace usage: java { com.sleepycat.je.util.DbSpace | -jar je.jar DbSpace } -h <dir> # environment home directory [-q] # quiet, print grand totals only [-u] # sort by utilization [-d] # dump file summary details [-V] # print JE version number
上のコードを実行した結果、次のような出力が得られたとします。
$ java -jar je.jar DbSpace -h <environment directory> -q File Size (KB) % Used -------- --------- ------ TOTALS 18167 60
この出力から、18MBの環境で、使用済みディスク領域の60%をアクティブなJEログ・エントリが占有しており、40%が未使用であることが分かります。
チェックポイントは、JE環境を再オープンするときの所要時間を短縮するときに有用です。また、このFAQでも説明したとおり、ログ・クリーニングを有効にする場合にも使用できます。 デフォルトでは、JEはアプリケーションに対して透過的にチェックポイントを実行しますが、トラブルシューティングの際に、チェックポイントの場所を探すのは有用な方法です。 たとえば、チェックポイントが存在しない場合は、ログ・ファイルのクリーニングが実行されていない可能性があります。
以下に示すDbPrintLogユーティリティの非公開オプションを使用すると、チェックポイントの場所に関するサマリー情報がユーティリティの出力の末尾に追加されます。 たとえば、次のコマンドを実行したとします。
java com.sleepycat.je.util.DbPrintLog -h <environment directory> -S
出力結果は、次のようになります。
<省略> Per checkpoint interval info: lnTxn ln mapLNTxn mapLN end-end end-start start-end maxLNReplay ckptEnd <省略> 16,911 0 0 2 2,155,691 2,152,649 3,042 16,911 0x2/0x87ed74 83,089 0 0 2 10,586,792 10,581,048 5,744 83,090 0x3/0x90e19c 0 0 0 0 0 0 0 0 0x3/0x90e19c
最後の列が、最後に実行されたチェックポイントの場所を示しています。 スラッシュで区切られた最初の数字が.jdbファイル名、2番目の数字がファイルの先頭からのオフセットです。 この例では、最後に実行されたチェックポイントのファイル名は00000003.jdb、オフセットは0x90e19cです。 この場所より後ろにある.jdbファイル内の領域を、ログ・クリーナーで回収することはできません。
DbPrintLogは、ログのCkptStartエントリとCkptEndエントリを参照して、両エントリからチェックポイント間隔を計算するだけである点に注意してください。 CkptStartの位置がクリーニング処理によって削除されたファイルに含まれる場合、CkptStartは表示されません。 CkptEndも同じ理由で表示されないことがあります。またはシャットダウンが異常終了したためにチェックポイントが終了しなかった場合にも表示されません。
DbPrintLog -Sは膨大なリソースを消費する可能性があります。 必要なら、-sオプションを指定して、このユーティリティによって分析されるログの量を制限することもできます。
Berkeley DB Java Edition 3.0より、com.sleepycat.collectionsパッケージは、Javaコレクション・フレームワークと完全互換になりました。 以前のリリースでは、Collections.size()がサポートされていなかったため、コレクションのイテレータを明示的にクローズする必要がありました。 これらの非互換性が解決され、Javaコレクション・フレームワーク・インタフェースを使用する他のJavaライブラリとの完全な相互運用性が実現されました。
以前のバージョンでは、StoredCollectionが、iterator()の呼出し元コンポーネントに渡されるたびに、ユーザーはコンテキストを考慮する必要がありました。 当該コンポーネントがStoredIteratorクラスを認識していない場合、当然、StoredIterator.close()メソッドは呼び出されません。 これにより、クローズされていないカーソルが残されたままとなり、パフォーマンス低下の原因となります。 このトピックについて詳しくは、以前のバージョンのJavaコレクションAPIでは、JEコレクションAPIによって返されたイテレータを呼出し元が明示的にクローズする必要がありました。なぜですか。の項目を参照してください。
StoredIterator.close()を呼び出すようにコンポーネントを変更できない場合は、唯一の解決策として、StoredCollectionから標準のJavaコレクションに要素をコピーし、生成されたコピーを当該コンポーネントに渡します。 もっとも簡単な解決策は、StoredCollection.toList()メソッドを呼び出してStoredCollectionを標準のJava ArrayListにコピーする方法です。
StoredCollectionのサイズは大きいが、当該コンポーネントに渡す必要があるのはコレクションの一部の要素だけという場合に、コレクション全体でtoList()メソッドを呼び出すと、すべての要素がデータベースからメモリにコピーされてしまうので、望ましくありません。 必要な要素だけを含む標準のJavaコレクションを作成するには、次の2つの方法があります。
{ID, attribute}をキーに持つプライマリ・データベースと、{ID}をキーに持つセカンダリ・データベースがあるとします。 特定のIDを持つすべての属性にアクセスするにはどうすれば良いでしょうか。 つまり、セカンダリ・データベース内で特定のキーを持つすべての重複レコードにアクセスするにはどうすれば良いでしょうか。
ここで、上の質問には、実は一部おかしな点があることを認めなければなりません。 この例では、特定のIDに対応する属性を取得するのに、わざわざセカンダリ・データベースを作成および保守する必要などないのです。 プライマリ・データベースの特定のIDに対してSortedMap.subMap(fromKey,toKey)を呼び出せば、必要な属性だけが格納されたマップが生成されます。 fromKey引数には、特定のIDとゼロ(または属性の最小値)を指定します。 toKey引数には、fromKey引数に指定したIDより1つ大きいIDを指定します(属性は任意です)。 subMapの動作をより細かく制御する場合は、拡張メソッドStoredSortedMap.subMap(Object,boolean,Object,boolean)も使用できます。
この手法は、{ID, attribute}キーがソートされていないと使えない点に注意してください。 IDはキーの先頭フィールドなので、マップは最初にIDで、次にID内の属性でソートされます。 このタイプのソートは、タプル・キーには使用できますが、順次キーには使用できません。 一般に、順次キーでは、ソート順を確定できません。 タプル・キーを使用するには、TupleBinding、またはcom.sleepycat.bind.tupleパッケージ内の別のタプル・バインディング・クラスを使用します。
さて、重複レコードにアクセスする方法ですが、重複レコードを含むデータベースがある場合は、コレクションAPIを使用してさまざまな方法で重複レコードにアクセスできます。 具体的には、すでに説明したように、対象のキーについてsubMapまたはsubSetを作成する方法、またはStoredMap.duplicates()拡張メソッドを使用する方法などがあります。 これらについては、StoredMap.duplicates()、Retrieving Items by Index Keyチュートリアル、Using Stored Collectionsチュートリアルで説明しています。
Berkeley DB Java Edition 3.0より、com.sleepycat.collectionsパッケージは、Javaコレクション・フレームワークと完全互換になりました。 以前のリリースでは、Collections.size()がサポートされていなかったため、コレクションのイテレータを明示的にクローズする必要がありました。 これらの非互換性が解決され、Javaコレクション・フレームワーク・インタフェースを使用する他のJavaライブラリとの完全な相互運用性が実現されました。
以前のリリースで、StoredCollectionからイテレータを取得した場合は、取得されるイテレータは常にStoredIteratorなので、明示的にクローズする必要があります。 下層のカーソルが保持しているロックを解放するには、イテレータをクローズする必要があります。 パフォーマンスの低下を避けるため、カーソルは不要になった時点ですぐにクローズすることが重要です。
Javaイテレータ・インタフェースにはclose()メソッドが定義されていないため、StoredIteratorクラスのclose()メソッドを使用する必要があります。 あるいは、IteratorをStoredIteratorにキャストするのを避けるために、StoredIterator.close(Iterator)静的メソッドを呼び出すことも可能です。このメソッドの引数にStoredIterator以外を指定しても、何も実行されません。
例外がスローされた場合でも、イテレータが常にクローズされていることを確認するには、finally句を使用します。 次に例を挙げます。
Iterator i = myStoredCollection.iterator(); try { while (i.hasNext()) { Object o = i.next(); // 任意の処理を実行 } } finally { StoredIterator.close(i); }
永続性はEntity注釈のドキュメントに定義されています。このドキュメントでは、次のトピックについて解説しています。
プライマリ・キーとシーケンスについては、PrimaryKey注釈のドキュメントに説明があります。このドキュメントでは、キーに関する以下の一般的なトピックについて解説しています。
セカンダリ・キーとコンポジット・キーについては、SecondaryKey注釈およびKeyField注釈を参照してください。
エンティティは、PrimaryIndexによってプライマリ・キーを用いて格納されます。エンティティを格納する方法については、PrimaryIndexクラスのドキュメントに説明があります。
エンティティは、SecondaryIndexを使用して、セカンダリ・キーで検索および削除することもできます。この方法については、SecondaryIndexクラスのドキュメントに説明があります。 SecondaryIndexのドキュメントでは、プライマリ索引とセカンダリ索引によって提供される次の4つのマッピングについても解説しています。
PrimaryIndexクラスとSecondaryIndexクラスはどちらも、EntityIndexインタフェースを実装したものです。 EntityIndexのドキュメントに、上記の4つのマッピングとその具体例について、詳しい説明があります。 このドキュメントでは、データ・アクセスに関する以下の一般的なトピックについても解説しています。
DPLのリレーションシップは、セカンダリ・キーを使用して定義します。 セカンダリ・キーは、エンティティを検索するための代替方法を提供すると同時に、エンティティとキーの間のリレーションシップを定義します。 たとえば、電子メール・アドレスのセットから成るセカンダリ・キー・フィールドを持つPersonエンティティは、Personエンティティと電子メール・アドレスの1:多のリレーションシップを定義します。 Personエンティティは複数の電子メール・アドレスを持ち、どの電子メール・アドレスを使用してもアクセスできます。
セカンダリ・キーは別のエンティティとのリレーションシップも定義します。 たとえば、EmployeeエンティティがdepartmentNameという名前のセカンダリ・キーを持ち、このセカンダリ・キーがDepartmentエンティティのプライマリ・キーでもあるとします。 これにより、EmployeeエンティティとDepartmentエンティティの多:1のリレーションシップが定義されます。 このとき、departmentNameは外部キーと呼ばれ、外部キー制約によって、departmentNameキーが有効であることが保証されます。
簡単なキー・リレーションシップとエンティティ間リレーションシップはどちらも、SecondaryKey注釈を使用して定義します。 この注釈には、リレーションシップの種類、関連エンティティ、関連エンティティが削除された場合に実行するアクションを指定するプロパティを定義します。
リレーションシップの定義に関する詳細な情報と、リレーションシップにアクセスする方法については、SecondaryIndexのドキュメントを参照してください。このドキュメントには以下のトピックが含まれています。
エンティティ・オブジェクトから別のオブジェクトを参照する方法は2つあります。 1つは、埋込みと呼ばれる手法です。この手法では、参照されるオブジェクトを、単にエンティティ・クラスのフィールドとして定義します。 この手法を使用するための要件は、埋め込まれるオブジェクトのクラスを@Persistentとして定義することだけです。 次に例を挙げます。
@Entity class Person { @PrimaryKey long id; String name; Address address; private Person() {} } @Persistent class Address { String street; String city; String state; String country; int postalCode; private Address() {} }
埋め込まれるオブジェクトはエンティティと同じレコード、この場合はPerson PrimaryIndexに格納されます。 Addressオブジェクトにアクセスするには、PrimaryIndex内のPersonオブジェクトを検索し、Person.addressフィールドを調べるしかありません。 Addressオブジェクトに単独でアクセスする方法はありません。 同じAddressオブジェクトが2つ以上のPersonエンティティに格納されている場合は、住所の個々のコピーが各Personレコードに格納されます。
一方、特定の住所に単独でアクセスしたり、複数のPersonオブジェクト間で住所情報を共有したりしたい場合もあります。 その場合は、Addressクラスを@Entityとして定義し、セカンダリ・キーを使用してPersonエンティティとAddressエンティティ間のリレーションシップを定義する必要があります。 次に例を挙げます。
@Entity class Person { @PrimaryKey long id; String name; @SecondaryKey(relate=MANY_TO_ONE, relatedEntity=Address.class) long addressId; private Person() {} } @Entity class Address { @PrimaryKey long id; String street; String city; String state; String country; int postalCode; private Address() {} }
この手法では、Addressはプライマリ・キーを持つエンティティです。 AddressエンティティはPersonエンティティとは別に格納されるため、単独でアクセスできます。 Personエンティティを取得した後は、Address PrimaryIndexのPerson.addressIdの値を使用してAddressを検索する必要があります。 同じaddressId値を持つPersonエンティティが複数存在する場合、参照されるAddressは実質的に共有されます。
すべてのクラス(サブクラス、スーパークラス、埋込みクラス)を永続化するための要件については、Entity注釈ドキュメントの"Complex and Proxy Types"の項に解説されています。
注釈エンティティ・モデル(デフォルトのエンティティ・モデル)を使用する場合は、すべての永続クラスに@Persistentで注釈をつける必要がありますが、これにはいくつか理由があります。
まず、永続クラスのバイトコード注釈は、マーシャリングとアンマーシャリングを最適化するため、クラスがロードされたとき、注釈が存在するかどうかに基づいて実行されます。 この処理が効率的かつ正確に実行されるように、すべてのクラスに注釈をつける必要があります。
第2に、注釈は、クラスが進化したときバージョン番号として使用されます。 たとえば、スーパークラスのインスタンス・フィールドが変更された場合に、ソース・コードを自分で管理していないと、インスタンス・フィールドの進化を管理するのは難しくなります。 注釈を付加できない、または管理できない外部クラスを使用する場合に、PersistentProxyが必要になるのはこのためです。
スーパークラスまたは埋込みクラスのソース・コードが自分の管理下にない場合は、データを永続クラスにコピーまたは参照し、PersistentProxyクラスを使用して外部クラスを自分の管理下の永続クラスに変換することを検討してください。
JE注釈を使用することが望ましくない場合は、EntityModelをサブクラス化して、注釈によらないメタデータの記述を実装してください。 注釈は、インタフェースを実装したり、共通ベース・クラスをサブクラス化したりする方法に比べると、ずっと控えめな方法ですが、それでも望ましくないことがあります。 ただし、注釈を使用しない場合は、バイトコード注釈によるパフォーマンス向上の利点も得られない点に注意してください。
EJB3指向のJava Persistence APIはSQL指向であるため、SQLまたはその他の問合せ言語を追加しない限り、Berkeley DBで完全に実装することはできません。 DPLの設計段階において、この問題に関する公開議論がTheServerSideコミュニティで開催されました。
DPLのJava Persistence APIとEJB3のJava Persistence APIの相違点は、注釈の構文だけではありません。
一般に、Berkeley DBの利点は、SQLデータベースで実現可能なレベルよりも、パフォーマンスが高く、使用方法のモデルが単純であることです。 Berkeley DBでEJB3 Persistence APIを実装すると、こうした利点が部分的に失われてしまいます。
EntityStore内のすべてのデータベースをダンプおよびロードするには、それぞれDbDumpおよびDbLoadユーティリティを使用します。 これらのユーティリティを使用すると、環境内のログ・ファイルのセット全体をコピーする必要がない場合に、ストア内の必要なデータだけをコピー、バックアップ、リストアできます。
ダンプまたはロードするデータベースの名前を調べる方法については、EntityStoreドキュメントのDatabase Namesの項を参照してください。
また、データベースをダンプおよびロードするときの一般的なプロセスについて理解しておくことは有益です。 次の2点を覚えておいてください。
ある環境でストアをダンプし、ダンプした内容を別の環境にロードする手順を以下に示します。
CarbonadoはオープンソースのJava永続化フレームワークです。 SourceForgeでAmazonによって公開されています。
Carbonadoの管理下では、Berkeley DB C Edition、Berkeley DB Java Edition、またはSQLデータベースを下層リポジトリとして使用できます。 Carbonadoは、SQLベースのバックエンドをサポートする抽象化が必要な場合、またはBerkeley DBとSQLのデータベース間でデータを同期化する必要がある場合に、非常に有用な手段です。
Carbonadoでは、SQLデータベースとリレーショナル・モデルをサポートしているため、DPLとは異なる機能セットを提供しています。 以下の機能比較を参考にして、どちらのAPIを選択するか決めてください。
機能 | Carbonado | Direct Persistence Layer(DPL) |
SQLとリレーショナル・データベースのサポート | リレーショナル・モデルをサポート。 下層のリポジトリは、JDBC経由でアクセスされるSQLデータベース、またはBerkeley DBリポジトリのどちらかを使用可能。どちらも同じCarbonado APIを使用してアクセスできる。 問合せには、SQLではないがリレーショナル・モデルに従った簡単な問合せ言語を使用。 | SQLデータベースはサポートしていない。問合せ言語も用意されていない。 リレーショナル・モデルは使用できるが、APIは用意されていない。 問合せでは、プライマリ索引とセカンダリ索引に直接アクセスする。 |
2つのリポジトリの同期化 | Berkeley DBとSQLのリポジトリを含む任意の2つのリポジトリを同期化。 これにより、たとえば、SQLデータベースのフロントエンド・キャッシュとしてBerkeley DBを使用することも可能。 | サポート対象外。 |
CRUD操作 | CRUD操作は、ActiveRecordデザイン・パターンを使用して実行。 CRUDメソッドは永続オブジェクト上に存在する。 | データ・アクセス・オブジェクト(DAO)デザイン・パターンに従ってプライマリ索引とセカンダリ索引に直接アクセスしてCRUD操作を実行する。永続オブジェクトのメソッドは使用しない。 |
リレーションシップ | 1:1、1:多、多:1、多:多のリレーションシップをサポート。 永続オブジェクトのメソッドを使用してリレーションシップを走査。 X:1のリレーションシップでは、メソッドがオブジェクトをフェッチ。 X:多のリレーションシップでは、メソッドが返す問合せを実行したり、他の問合せと組み合わせて使用したりできる。 | 1:1、1:多、多:1、多:多のリレーションシップをサポート。 プライマリ索引とセカンダリ索引に直接アクセスしてリレーションシップを走査する。 |
トランザクション | トランザクション処理、およびANSIで規定されている4つの分離レベルをすべてサポート。 トランザクションはスレッドごとに暗黙に実行される。 | トランザクション処理と非トランザクション処理、およびANSIで規定されている4つの分離レベルをすべてサポート。 トランザクションはパラメータとして明示的に渡される。トランザクション:スレッドのM:Nの関係を許可。 |
永続オブジェクト・モデル | JavaBeanの各プロパティは永続化される。 リレーショナル・エンティティを表現するために、インタフェースと抽象クラスが定義されている。 Storableインタフェースの拡張および実装には、このインタフェースと抽象クラスを使用する必要がある。 インスタンスの生成には、Storageファクトリ・クラスを使用する。 リレーショナル・モデルに準拠するため、ネスト化されたオブジェクトはサポートされていない。またはオブジェクト・グラフは維持されない。 | POJO永続化をサポート。 一時的でないインスタンス・フィールドはすべて永続化される。 インタフェースを実装したり、ベース・クラスを拡張したりする必要はない。 インスタンスの生成には通常のコンストラクタを使用する。 オブジェクトおよび配列は任意の深さでネスト化でき、オブジェクト・グラフも維持される。 |
メタデータの記述 | キーやその他のメタデータの記述には注釈を使用する。 | キーやその他のメタデータの記述には注釈を使用する。 あるいは、注釈の使用を避けるため、外部のメタデータを使用することも可能。 |
クラスの進化 | フィールドおよび索引の追加と削除をサポート。 | フィールドとクラスの追加、削除、名前の変更、ワイドニング、変換、および索引の追加と削除をサポート。 |
コミット時ロック | Versionプロパティによってサポート。 | サポート対象外。 |
トリガー | すべての書込み操作(作成、更新、削除)でサポートされる。 | サポート対象外。 |
ラージ・オブジェクト | サポートされているが、部分的なget/put操作を使用してBerkeley DB向けに実装されているため、真のLOBがサポートされたときと同レベルのパフォーマンスは実現されない。 | サポート対象外。 |
シーケンス | サポート。 | サポート。 |
サポート | Carbonadoのオープンソース・コミュニティによって提供される。 | OracleとBerkeley DBのオープンソース・コミュニティによって提供される。 |
JE HAでレプリケートされたアプリケーションはその性質上、ネットワーク経由で通信する分散アプリケーションです。 適切な構成を行うよう注意しておくことで、稼働中の分散環境で診断や修正を行うことが難しいような問題を防ぐことができます。 以下のチェックリストは、重要な構成上の問題について最初から検討しておくために使用するものです。 また、アプリケーションのデプロイ後に予期しない問題が発生したときや、不注意による構成変更が行われたときに確認すべきリストでもあります。
また、複数のマシンを使用する場合は、どの2台のマシンの組合せにもネットワーク・パスが存在することを確認します。 ネットワーク・パスの存在を確認するには、pingなどのネットワーク・ユーティリティを使用します。
java -jar je.jar DbGroupAdmin -groupName <group name> -helperHosts <host:port> -dumpGroup