【PHP】try-lockを簡易実装する一つの方法

一つしかない資源(リソース)へ同時に更新、書き込みが発生する事態は極力避けたいです。FastCGI(nginxやapache)で動作させるPHPでは、同時間に複数のアクセスがあったりするので、一つのリソースに対する排他制御が必要になることがあります。

PHPではlockステートメントがないみたい

C#のような言語では、lockステートメントやMonitor.TryEnterなどを利用することで実現できます。PHPではこのようなステートメントや高級な仕組みが用意されていません。

PHP でもコンパイル指定次第でセマフォ(sem_get 、sem_acquire)やMutexが使えます。レンタルサーバー上にあるPHPでは利用できないことが多いですね😭
セマフォはサーバー上資源を利用する仕組みだと思っています。そのためデットロック等の異常事態が発生した際、復旧にはサーバー管理者権限が必要になったりするので仕方かも・・

セマフォやMutexを利用しない排他制御の手段は、ファイルロック(flock)です。
【排他制御の注意点】

  1. ロックの機構は完璧な制御が出来ないと思っています。
  2. 全く同時のタイミングで複数のリクエストが来た場合、どのようなロック実装手段でも想定外の事態が起こりやすいです。
  3. そのため、排他制御で資源を獲得できた場合でも資源更新エラーに備える必要があると思っています
  4. ロックの機構は極力シンプルに、素早く完了させる必要があります
  5. ロックしたまま、異常終了してしまうと誰も獲得できない事態が発生します。注意深く設計する必要があります。

色々考えるととても面倒ですね。手を抜いて、カンタンに実装できる方法がこちらです。

PHPのtry-lockはSQLiteを使うのがカンタンでした♩

SQLiteはどのレンタルサーバーでも利用できる状態になっていると思っています。環境を気にせずに利用できます。
SQLiteは同時書き込みに対応していません。同時に書き込みトランザクションは1つだけという制限を持っています。
これを利用することで自前でflock等の実装をする必要がなくなります。多くのユーザーに利用された安心で簡単な実装ができるという考えです。
実装例は↓のような感じです。

<?php
class MyLock {
    private ?SQLite3 $lockObject = null;
    function __construct($lockfile){
        $this->lockObject = new SQLite3( $lockfile );
        $this->lockObject->enableExceptions(true);
    }
    public function tryAquire() : bool {
        try{
            $this->lockObject->exec("BEGIN IMMEDIATE;");
            return true;
        }catch(Exception $e){
            return false;
        }
    }
    public function release(){
        if( is_object($this->lockObject) == false ){
            return ;
        }
        try{
            $this->lockObject->exec("end");
        }catch(Exception $e){
            return ;
        }
    }
}
  • とてもカンタンな実装にしているので不足しているポイントがあればご自身で成長させてください。
  • tryAquire()は、ロックを試みてロックできない場合は、false。獲得できた場合trueを返します。
  • release()は所有者の確認をしないロック解除メソッドです。

使い方は、上記のソースをinclude、requireした後、こんな感じで処理します。

$lock = new MyLock("file.lock"); 
if( $lock->tryAquire() ){
    // 資源が獲得できた場合の処理
}else{
    // 獲得できなかった場合の処理
}

FastCGIで動作させる場合、PHPの処理が終わったタイミングで自動開放されるので資源の開放処理は不要です。

SQLiteを使うことで、シンプルに実装することができました。

まとめ:ロックの競合は発生します。

単純明快なコードですが、同タイミングで複数スレッド、プロセスによるアクセスでtryAquireは間違いを犯すことがあります。

稀な頻度ですが、競合発生します。

資源が獲得できた場合に別のSQLiteデータベースに対する更新処理を行っています。
この更新処理でdatabase is lockが発生します。発生事態は稀ですが、そういうことが起こる前提でご利用ください。

失敗した場合、ユニークなファイルで所定ディレクトリに格納し、獲得できたタイミングでまとめて処理するようにしていますよ。

コメント

タイトルとURLをコピーしました