ACID 操作:Actian Zen がフラット ファイルより優れたデータ ストアである理由

一見したところ、アプリケーション データをフラット ファイルに書き込むことは最も簡単な解決策のように思えますし、データをファイルに書き込むことだけが要件である場合には、妥当であるかもしれません。しかし、ファイルのデータを読み取ったり更新したりする必要があることに気付くとすぐに、ほとんどの場合、データの整合性を確実にするためにアプリケーションで行わなければならない作業がさらに多くあることがわかります。

最も基本的なデータ ファイル ストレージの実装でさえ、多くのコードを必要とします。古くから、フラット ファイルのストレージを扱いやすくするための試みが無数にありました。

特別なファイル形式がありましたが、中でも、MS-DOS や初期の Windows の時代からの初期化(.ini)ファイル、カンマ区切り値(CSV)ファイルは、ファイルの解析をしやすくしようとしたことから生じました。後に、Microsoft は XML 形式の app.config ファイル(.NET)を実装しました。このファイルは、開発者が構成ファイルに値を保存し直すのを防ぐために、意図的に読み取り専用のアプリケーション構成形式として作成されました。おそらく、そのアイデアは app.config ファイルを管理するための簡単な API を作成することでした。

最も単純なケースでも、データを後で使用するためにフラット ファイルに保存しておくと、多くの場合、自分で余分な作業を作ることになります。

フラット ファイルへの書き込み

C++ の簡単な例として、後で使用できるように 2 つの値を保存する例を見てみましょう。システムの日付時刻を取得し、実行可能ファイルの名前とそれが実行された最終日時をファイルに保存するだけのプログラムです。

#include <iostream>
#include <fstream>
#include <iomanip>
#include <ctime>
using namespace std;
string getDateTime();
int main()
{
     string outFileName = "lastRunTimes.txt";
     ofstream outfile (outFileName,ofstream::binary);
     string delimiter = ",";
     string programName = "saveValues";
     string lastRunTime = getDateTime();
     outfile.write(programName.c_str(),programName.length());
     outfile.write(delimiter.c_str(),delimiter.length());
     outfile.write(lastRunTime.c_str(),lastRunTime.length());
     outfile.close();
     return 0;
}
string getDateTime(){
     auto t = std::time(nullptr);
     auto tm = *std::localtime(&t);
     std::ostringstream oss;
     oss << put_time(&tm, "%m-%d-%Y %H:%M:%S");
     return oss.str();
}

ファイルのデータは次のようになります。

saveValues,01-21-2020 14:10:07

もちろん、ここでは例を短くするために、ファイル名と実行可能ファイル名のどちらもハード コーディングされています。さらに、ファイルに既にデータがあるかどうかを判断するためのチェックがないため、プログラムが実行されるたびにファイルは上書きされます。また、エラーのチェックもないため、現在の作業ディレクトリに書き込めなくても、プログラムはエラーを返すことなく失敗します。

この小さなプログラムには多くの問題があります。最も重要なのは、非常に多くのコードがほとんど作業をしていないことです。

さて、多数のサービスがファイルへの書き込みを行って最終実行時刻を更新できるように、プログラムの変更が必要になる状況が発生したとします。この場合、サービスの実行可能ファイルの名前を入力引数として取り、.exe の名前と最終実行時刻を含む新しい行を書き込むよう、元のコードを変更できることがわかります。

しかし、サービスがその最終実行時刻の行を既にファイルに書き込んでいたら、どうなるでしょうか? その場合、ファイルに行を追加するだけでなく、ファイルを更新するコードが必要になります。

さらに、複数のサービスがプログラムを使用して lastRunTimes.txt ファイルに書き込むので、このファイルが 1 つのサービスからの書き込みによってロックされているときに、別のサービスがファイルにアクセスして最終実行時刻を更新しようとした場合には、どうなるでしょうか?

待って!他にもあります。キューの問題の解決方法として、多くのプロセスがファイルにアクセスできるよう、ファイルの書き込み中にロックしないことにしたらどうなるでしょうか? その場合、多くの呼び出し元プロセスによってプロセスが同時に開始され、それぞれが他の書き込みを待たずに自身のデータをファイルに書き込むため、ファイルに書き込まれたデータは、ほとんどの場合、完全に同期しなくなります。ファイルにデータを書き込むこの簡単なプロセスはすぐに巻き戻せますが、非常に複雑になります。

ACID トランザクション

このような種類の課題は、ACID という用語で呼ばれるコンピューター サイエンスの概念の焦点です。ACID は次の単語の頭文字です。

  • Atomicity(原子性)– データに対するあらゆる変更は、別のプロセスに割り込みされる前に、成功するかエラーを返して完了することを保証します。
  • Consistency(一貫性)– データの状態は、すべての呼び出し元プロセスに対して同じです。
  • Isolation(独立性)– 単一のプロセスによってデータが更新されている間、他のプロセスは、それがコミットされるまで変更を読み取ることができないことを保証します。更新プロセスは完了するまで他のプロセスから分離されているため、部分的に更新されたデータを読み取る機能はありません。これと対照的な状況として、ファイルは新しい 10 バイトで更新されるのに、5 バイトだけが更新された時点で別のプロセスが現れてファイル ストリームを読み取るという状況があります。これはダーティ リードとなります。
  • Durability(永続性)– 電源が切れたり、他の致命的な障害が発生しても、コミットされたデータは新しい状態のデータを表します。データはコミットされたら、揮発性 RAM だけでなく、何らかの形式の永続記憶域に書き込まれます。

それを念頭において、Zen トランザクショナル インターフェイスの Btrieve 2 を使用して問題をどのように解決できるかを考えてみましょう。ユーザーがプロセスの名前(実行可能ファイル名)を提供できるようにし、そのプロセスが実行された最終日時を保存するようにしたいと考えます。

Btrieve 2: ProcessLogger を使用するプログラムを呼び出します。ProcessLogger を使用すると、ユーザーはそれを呼び出して、プロセス名や開始日時、プロセスを開始したユーザー名を記録することができます。

これは少し不自然ですが、Btrieve 2 API を使用した場合にデータの更新と保存がどれほど簡単になるかを示す例として適しています。不自然であっても、アイデアは十分に現実的です。

完全なコードを GitHub リポジトリから取り込み、それを Visual Studio(2017 以降)でビルドして試してみることができます。

ProcessLogger プログラム

ユーザーがサーバー上でプロセスを開始するたびに、中央のファイルにログ エントリを書き込むことができるようにする必要があります。

ここではシンプルなコンソール アプリケーションを作成しますが、このアプリケーションを、入力(実行可能ファイル名、ユーザー名)を受け取り、前回の開始時刻をファイル(この例では Exe.Info という名前)に記録する 1 つのサービスと考えることができます。

課題は、複数のプロセスで同時にファイルを更新したいということです。また、複数のユーザーが手動でプロセスを開始しようとする可能性があり、そうすると、ログ データをファイルに書き込む必要があります。

2 つのプロセスが同時にログ ファイルに書き込まないようにするのも一仕事です。しかし、Btrieve 2 API を使用すれば物事ははるかに簡単になります。その理由は、出力ファイルをデータベースのように管理する責任から解放されるためと、Zen エンジンは更新を処理する際、複数プロセスによるファイルへの同時書き込みから更新が保護されるようにする方法がわかっているためです(原子性と独立性)。これは、ファイルからデータ読み取ったとき、目にするデータは最後に書き込まれたものであるという信頼を与えます(一貫性)。そして、もちろん、システムがデータをコミットすることを知っているので、ディスク障害やメモリからバイトをフラッシュすることについて心配する必要はありません(永続性)。

次のコマンド ライン入力を使用して、ファイルにレコードを書き込むことができます。

c:/>ProcessLogger [process name] [user name]
c:/>ProcessLogger calcSales.exe jim.smith

ProcessLogger を実行した場所に Exe.Info ファイルが存在しない場合は、ファイルが作成されるとともに、レコードが追加されます。レコードには、プロセスが実行された最終時刻を示す現在の日時を表すフィールドが含まれます。

ファイルのレコードは次のような内容になります。

record: (calcSales.exe, jim.smith, 02-06-2020 11:29:30)

この作業はすべて、ProcessLogger コード内でいくつかの Btrieve 2 API メソッドを呼び出すだけで行われます。

コンパクト化したコードは次のようになります(詳しくは GitHub リポジトリをご覧ください)。

// create the file (FILE.INFO) where data will be stored
createInfoFile(&btrieveClient, exeInfoFileName.c_str())
btrieveFileAttributes.SetFixedRecordLength(TOTAL_EXEINFO_RECORD_LENGTH)

この 2 番目のコード行で、データを保持する固定長レコード(最大レコード長)が作成されます。この長さは任意で 122 バイトに設定しており、.exe の名前を 50 文字、ユーザー名を 50 文字、特殊な日時形式を 19 文字、それに加えて終端文字を保持できるようにしています。

DateTime を使いやすい文字列に書式設定するために、DateTime ヘルパー メソッド(string getDateTime)がコードに追加されています。その後、ファイルを開くだけで作業することができます。

btrieveClient->FileOpen(btrieveFile, fileName, NULL,
Btrieve::OPEN_MODE_NORMAL))

初めにデータの最初の列(exe 名)にインデックスを作成して、後で exe 名によってレコードを検索できるようにします。

btrieveKeySegment.SetField(0, 51, Btrieve::DATA_TYPE_ZSTRING)
btrieveIndexAttributes.AddKeySegment(&btrieveKeySegment)
btrieveFile->IndexCreate(&btrieveIndexAttributes)

インデックス作成後、ユーザーからコマンド ラインでデータを提供され(calcSales.exe, jim.smith)、.exe 名が指示されたら、新しいレコードを追加します。

btrieveFile->RecordCreate((char*) &record, TOTAL_EXEINFO_RECORD_LENGTH))

常に、最大レコード長(122)を超えないようにします。Btrieve API は、レコードの作成時に送信される値を適用します。

その後、レコードを検索したい場合には、次のように Btrieve API の RecordRetrieve メソッドを呼び出すことにより検索を行えます。

btrieveFile->RecordRetrieve(Btrieve::COMPARISON_EQUAL,
Btrieve::INDEX_1, (char *)key.c_str(),
key.size(),
(char*)&record, TOTAL_EXEINFO_RECORD_LENGTH )

サンプル プログラムでは、キー(.exe 名)を ProcessLogger に提供することによりレコードを取得できます。コマンド ラインは次のようになります。

C:\>ProcessLogger calcSales.exe
record: (calcSales.exe, jim.smith, 02-06-2020 11:29:30)

Btrieve 2 API を使用すると、すべてのプログラムを共有データ ソースに書き込むようにすることができ、上書きなどの原因でデータが破損することを心配する必要はなくなります。

ACID についての話に戻る

Btrieve 2 API を介してデータにアクセスすることにより、元の saveValues プログラムには備わっていなかったデータ保護レイヤーの恩恵を受けられるようになりました。

RecordCreate() を呼び出せば、データに関するレコードの挿入、すべてのインデックスの更新、および失敗の通知(アトミシティ)は、基盤となる Zen エンジンに依存できることが保証されます。

また、データの一貫性も依存することができます。複数のプロセスがデータを更新しようとしている場合でも、Zen エンジンによってデータ更新が管理されることがわかっています。API は独立性の課題を円滑に処理するため、複数のプロセスがデータにアクセスしたときでも、挿入や更新が成功するか失敗するかにかかわらず、データの状態がわかります。

主要なポイントは、ACID に関連するこれらの懸念事項はすべて Zen エンジンによって処理されるということです。それにより、ACID 原理に確実に対応しているコードの設計とテストに時間を費やすことなく、データの読み取りと保存に集中することができます。代わりに、その基盤を提供するために Zen エンジンに依存することができます。

まとめ

GitHub リポジトリからコードを取り込んで、試してみてください。そして、Actian サイトにある例を見てください。Btrieve のはじめにページは、取り掛かるのに最適な場所です。私が ProcessLogger サンプルから始めた場所もここです。また、すべての Btrieve 2 メソッドのドキュメントも確認してください。Btrieve クラスの一覧は非常に役に立ちます。最も使用するクラスは、BtrieveClient と BtrieveFile です。

見てきたとおり、データの整合性を確保することは、自身で処理を試みる場合には特に重要な問題です。しかし、そうする必要はありません。その責任は Btrieve に任せ、それらの懸念事項を処理させましょう。