やりたいこと
トレイトを読み込むだけでお手軽にロック機能を使えるようにしたい。
※バッチやキュージョブなど多重実行されたら困る時などに使用する。
ロックにはMySQLの名前ロック(get_lock/release_lock)を使用します。
そのため、データベースがMySQL以外では使用できません。
名前ロックについてはこちらを参照
MySQL 5.6 リファレンスマニュアル::12.18 その他の関数
環境
Laravel 5.4.25
PHP 7.1.3
MySQL 5.7.19
作ったもの
<?php
namespace App\Utils\Traits;
use DB;
/**
* 名前ロックトレイト
* MySQLの名前ロックを使用したロック機能を提供する
*/
trait NamedLockable
{
/**
* ロック中の名前
* @var string
*/
private $locked_name;
/**
* ロック処理
*
* @param string $name
* @param int $time
* @return bool
*/
public function lock($name, $time = 60)
{
$ret = DB::select('select get_lock(?, ?) as result', [$name, $time]);
if (!object_get(array_first($ret), 'result')) {
return false;
}
$this->locked_name = $name;
return true;
}
/**
* ロックを解放
* 名前指定がなければ前回ロックした名前のものを解放する。
*
* @param string $name
*/
public function unlock($name = null)
{
if (null !== $name || null !== ($name = $this->locked_name)) {
DB::select('do release_lock(?)', [$name]);
$this->locked_name = null;
}
}
}
(追記:2018-01-24)
MySQL 5.7.5 以降、ロック名は64文字以内という制限ができたようです。
$nameは、64文字以内となるようにしてください。
※もしくはsha1関数などを使用して64文字以内に収まるよう改良するなど。
https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
補足1
DBファサードや、Laravelのヘルパー関数を使用しているので、そのまま使えるのは、Laravel or Lumenフレームワークのプロジェクトだけです。
補足2
unlock時にいちいち名前を指定するのが面倒だったので、private変数に名前を突っ込むようにしました(前回分のみ)。ただ使用する側のクラスからは、private変数にアクセスできてしまうので注意。
※複数ロックする場合は、名前を指定して解放してください。
使い方
簡単なartisanコマンドを作成して試してみます。
ロックを取ってからしばらくsleepし、ロックを解放するだけのプログラム。
<?php
namespace App\Console\Commands;
use App\Utils\Traits\NamedLockable;
use Illuminate\Console\Command;
class LockSleeper extends Command
{
use NamedLockable;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'lock:sleeper';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Lock and sleep. After 10 seconds, release the lock.';
/**
* Execute the console command.
*/
public function handle()
{
$this->line('I will go to bed.');
// ロックが取れるまで1秒頑張る。
if (!$this->lock('LockSleeper::NowSleeping', 1)) {
throw new \Exception('Oh my god! My room is locked!');
}
$this->line('Good night!');
sleep(10);
// ロックを解放
$this->unlock();
$this->line('Good morning!!');
}
}
KernelにLockSleeperクラスを登録
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
// 追加
\App\Console\Commands\LockSleeper::class
];
// 以下省略
コマンド実行(2画面で同時実行)
php artisan lock:sleeper
後から実行した方は、1秒後にロックを取れずエラーとなれば成功です。
簡単ですね。
以上です。