Perl/ActivePerl For UNIX/Linux/Windows
 
TryThe Homepage
初めてのCGI
CGI 研究室
ダイナミックCGI
ダウンロード
サービス
サーバ構築(Windows)
データベースアクセス
有料サービス
FAQ
お問い合わせ
このページを印刷
ロックファイルを考える
CGIでファイルを操作すると、しばしばデータが消失することが有ります。 これは、同時アクセス(書き込み)による衝突です。 他の多くのサイトでもこれを避ける方法が紹介されていますが、私の経験上どれも不十分で、 現に有名サイトのカウンタさえ破壊されていることがしばしばです。 このコーナーでは、衝突しない。万が一衝突してもデータを破壊しないロック機能を考えてみましょう。
  1. 現在もっとも採用されているロック機能
    foreach (1 .. 10) {
        if (symlink($datafile,$tmpfile)) { last; }
        sleep(1);
    }
    open(OUT,"$datafile) || die "Can't open tmp file.\n";
    print @DATA;
    close(OUT);
    unlink($tmpfile);
    このコードを1つずつ解析してみましょう。
    foreach (1 .. 10) {
        if (symlink($datafile,$tmpfile)) { last; }
        sleep(1);
    }
    これは、UNIXのsymlink関数を使って、1つのファイルに2つめのファイル名を作ろうとしています。
    symlink関数は成功すると「1」を、失敗すると「0」を返します。
    この場合失敗する原因は現在、$tmpfileがすでに存在するからです。(他のプロセスが書込み中)
    成功すればループから抜出し、ファイルに書き込んでいます。
    open(OUT,"$datafile) || die "Can't open tmp file.\n";
    print OUT @DATA;
    close(OUT);
    失敗すると、1秒間に1回、10回までトライします。
    書き込み後、ダミーファイルを削除してロックを解除しています。
    unlink($tmpfile);

  2. 欠点
    このロック方式には多くの欠点が有ります。
    まず、symlink関数が衝突することです。 symlinkが衝突すると何も返してくれません。 これが10回続くと通常のロックなしと、同様に書込んでしまいます。
    つぎに、たとえロックに成功しても、書き込み時のサーバーダウンにはどうしようもありません。
    衝突したと同じ事です。
    それは、データファイルが書き込み可能のパーミッション「666」だからです。
    これではロックファイルとは呼べません。

    アルゴリズムとしてもおかしいところが有ります。
    ロックに失敗した場合もunlink($tmpfile);が実行されると言うことです。

  3. テンポラリーファイルを使う
    これらの問題を解決するため、いろいろな方法を考えてみましょう。
    まず、テンポラリーファイルを使う方法です。
    perlにも実行プロセスを管理するプロセス番号が有ります。これは決して重複することがありません。
    このプロセス番号のテンポラリーファイルを作成し、データはここに書き出すのです。
    同じファイルに書き込むのではありませんから決して衝突することがありません。
    #perlのプロセス番号のテンポラリーを作成
    $tmp_dummy = "$$\.tmp";
    open(TMP,">$tmp_dummy") || die "Can't create tmp file.\n";
    close(TMP);
    #パーミッションを変更
    chmod 0666,$tmp_dummy;
    #プロセステンポラリーファイルへデータを書込む
    #別々のファイルに書き込むので衝突する可能性は0に等しい
    open(TMP,">$tmp_dummy") || die "Can't open tmp file.\n";
    print TMP @DATA;
    cloce(TMP);
    rename($tmp_dummy,$datafile);
    最後にファイル名を戻しています。
    この方法だとデータファイルのパーミッションも「644」読込み専用のままですから、サーバーダウンによる消失も回避できます。
    これでも問題は有ります。リネームする時に衝突することです。
    リネームは、データ書き込みの数百分の一の時間で処理できますが、衝突する可能性は0ではありません。
    ここまでのロック方ならWindowsサーバでも使用できます。

  4. テンポラリーファイルのロックファイル
    リネームの衝突を避けるため、テンポラリーファイルのロックファイルを作成してみましょう。
    foreach (1 .. 10) {
        $locked = link($tmp_dummy,$tmpfile);
        if ($locked == 1) { last; }
        sleep(1);
    }
    #ハードリンクに成功すればリネームしてロックを解除
    if ($locked == 1) { rename($tmpfile,$datafile); }
    unlink($tmp_dummy);
    このコードは、ハードリンクを使用してテンポラリーファイルにロック用として、もう1つのファイル名を付けています。 link()関数も、symlink()関数同様に成功すれば「1」を、失敗すれば「0」を返します。
    成功した場合だけリネームしています。
    成功しなければリネームしないのですから衝突することはありません。
    アルゴリズムも問題ありませんね。
    こうすることでファイルが無いのに削除するようなこともありません。

    symlink()関数ではなくlink()関数を使用するのは、 現在でもsymlink()をサポートしないサーバーが多いためです。

  5. 完成かな?
    これで一応のロックファイルが完成しました。少々の事では衝突しない。万が一衝突してもデータファイルは破壊されないシステムです。
    #perlのプロセス番号のテンポラリーを作成
    $tmp_dummy = "$$\.tmp";
    open(TMP,">$tmp_dummy") || die "Can't create tmp file.\n";
    close(TMP);
    #パーミッションを変更
    chmod 0666,$tmp_dummy;
    #プロセステンポラリーファイルへデータを書込む
    #別々のファイルに書き込むので衝突する可能性は0に等しい
    open(TMP,">$tmp_dummy") || die "Can't open tmp file.\n";
    print TMP @DATA;
    close(TMP);
    #リネームするタイミングを計る
    foreach (1 .. 10) {
        $locked = link($tmp_dummy,$tmpfile);
        if ($locked == 1) { last; }
        sleep(1);
    }
    #ハードリンクに成功すればリネームしてロックを解除
    if ($locked == 1) { rename($tmpfile,$datafile); }
    unlink $tmp_dummy; #プロセステンポラリーを削除する
    では、これで十分でしょうか? いいえ、このシステムでは、10秒間リネームのタイミングを計り、ロックに成功したらリネームしていますが、
    失敗した場合は、クライアントからのリクエストがキャンセルされてしまいます。
    共有で使用しているサーバーですし、ページが表示される時間が遅くなってしまいますので、永遠に待たせるわけにも行きません。 何かいい方法はないものでしょうか?

  6. プロセスチェック
    前節までは、link()でリネームのタイミングを計っていましたが、これでは低速です。 もっと早くタイミングを見付け、link()が1回で成功するようにしてみましょう。 link()が1回で成功すれば、リネームもすぐに実行できます。
    #テンポラリーファイルが存在すれば他のプロセスがロックしている
    $tmpflag = 1; #テンポラリーの初期化
    if (-f $tmpfile) {
        $tmpflag = 0; #テンポラリーが有れば他のプロセスがロック中
        foreach (1 .. 10) {
            sleep(1);
            #1秒に1回監視し、テンポラリーがなくなれば抜出する
            unless (-f $tmpfile) { $tmpflag = 1; last; }
            #10秒間トライしても駄目ならリクエストをキャンセル
        }
    }
    このコードを追加することで、どうせ駄目なら無駄なコードは実行しないシステムの出来上りです。

  7. 完成
    #他のプロセスのロック状態をチェック
    foreach (1 .. 10) {
        #1秒に1回監視し、テンポラリーがなくなれば抜出する
        unless (-f $tmpfile) { $tmpflag = 1; last; }
        $tmpflag = 0; #テンポラリーが有れば他のプロセスがロック中
        sleep(1);
    }
    #10秒間トライしても駄目ならリクエストをキャンセル
    if ($tmpflag == 1) {
        #perlのプロセス番号のテンポラリーを作成
        $tmp_dummy = "$$\.tmp";
        open(TMP,">$tmp_dummy") || die "Can't create tmp file.\n";
        close(TMP);
        #パーミッションを変更
        chmod 0666,$tmp_dummy;
        #プロセステンポラリーファイルへデータを書込む
        #別々のファイルに書き込むので衝突する可能性は0に等しい
        open(TMP,">$tmp_dummy") || die "Can't open tmp file.\n";
        print TMP @DATA;
        close(TMP);
        #リネームするタイミングを計る
        #ロックされていなければロックする
        foreach (1 .. 10) {
            if (link($tmp_dummy,$tmpfile) == 1) {
                #ハードリンクに成功すればリネームしてロックを解除
                rename($tmpfile,$datafile);
                chmod 0644,$datafile;
                last;
            }
            sleep(1);
        }
        unlink $tmp_dummy; #プロセステンポラリーを削除する
    }

  8. Windowsサーバーを考える これでUNIX用は少々の事では壊れないロックファイルが完成しましたが、 link()関数や、symlink()関数をサポートしないNTサーバでは使用出来ません。 そこで今度は、Windowsサーバでも使用できる方法を考えてみましょう。考え方はまったく同じです。
    #共通のテンポラリーファイルが存在するかを調べる
    foreach (1 .. 10) {
        #無ければロックされていないので旗を立てて脱出する
        unless (-f $tmpfile) { $tmpflag = 1; last; }
        #有ればロック中なので旗を下げて1秒待つ
        $tmpflag = 0;
        sleep(1);
    }
    #旗が立っていれば、ロックを実行
    if ($tmpflag) {
        $tmpflag = 0;
        #プロセステンポラリーを作成して書き込む
        $tmp_dummy = "$$\.tmp";
        if (open(TMP,">$tmp_dummy")) {
            close(TMP);
            chmod 0666,$tmp_dummy;
            if (!open(TMP,">$tmp_dummy")) { &error(bad_tmpfile); }
            print TMP @DATA;
            close(TMP);
            #リネームのタイミングを計る
            foreach (1 .. 10) {
                #念のためにもう一度ロックを調べる
                unless (-f $tmpfile) {
                    #ロックファイルが無ければ作成してロックする
                    if (open(TMP,">$tmpfile")) {
                        print TMP "$datafile TMP File\n";
                        close(TMP);
                        #ロックしている間にリネームする
                        rename($tmp_dummy,$datafile);
                        #ロック用テンポラリーファイルを削除する(ロック解除)
                        unlink $tmpfile;
                        $tmpflag = 1;
                        last;
                    }
                }
                #ロック中ならリネームできないので1秒待つ
                sleep(1);
            }
            #もし、リネームに失敗してダミーのテンポラリーが残っていたら削除する
            if (-f $tmp_dummy) { unlink $tmp_dummy; }
        }
    }
    これでWindowsサーバでもほぼ完璧なロックが出来るでしょう。

    Windowsサーバのにはすでに存在するファイル名にリネームできない設定になっている物もあります。
    この場合は、テンポラリを作成するロックシステムは使用できません。
    多少ロック能力は低下しますが、次の方法でロックできます。

  9. リネーム、Perlのプロセスが取得できないサーバを考える 数多いサーバの中にはPerlのプロセス番号が取得できない物も有るようです。
    そこで、多少ロックする能力は低下しますが使用できるロックシステムを考えましょう。
    これは、link()や、symlink()も使用しませんのでほとんどのサーバで有効です。
    sub data_save {
        local($tmpfile) = $datafile;
        $tmpfile =~ s/(\w+)\.\w+$/$1\.tmp/i;         local($flag) = 0;
        #10秒待っても書き込めない場合は諦める
        foreach (1 .. 10) {
            #テンポラリーファイルが存在するか確認
            unless (-f $tmpfile) {
                #ロックファイルが無ければ作成してロックする
                if (open(TMP,">$tmpfile")) {
                    print TMP "$datafile TMP File\n";
                    close(TMP);
                    #ロックしている間に書き込む
                    if (open(TMP,">$datafile")) {
                        print TMP @DATA;
                        close(TMP);
                        $flag = 1;
                        #ロック用テンポラリーファイルを削除する(ロック解除)
                        unlink $tmpfile;
                        last;
                    }
                }
            }
            #ロック中なら1秒待つ
            sleep(1);
        }
        $flag;
    }

  10. 最後に
    たった3行で済んでいた書き込みコードがこんなに長くなってしまいましたが、効果抜群です。
    中途半端なロックなら速度を低下させるだけで、効果も期待できません。
    大切なデータは、少し位面倒でも強力なロックファイルを使用することをお勧めします。

    最新版は、当サイトがフリーで公開しております関数ライブラリ「perl-lib.pl」に収録されています。
Copyright 2004 Terra. All rights reserved. No reproduction or republication without written