前两天在写程序的时候,需要在内存中维护一个任务队列,但是Laravel的Cache是没有锁的,所以在往任务队列添加任务,在并发的时候可能会出问题,那么就需要手动来实现加锁。
思前想后,又看了半天Laravel的文档,发现Cache有一个add方法,当某一数据不在缓存中是才将其保存,如果该项实际上 已经添加 到缓存中,那么 add 方法将返回 true 。否则,此方法将返回 false。
通过add,我们就有了如下的加锁办法:
/**
* 从缓存中获取全局任务列表
*
* @return array 任务列表
*/
public static function getTaskListFromCache() {
/* 手动加锁以防止同时操作任务列表冲突 */
while (!Cache::add('task_list_lock', true, 1)) {
}
Log::notice('task list 加锁');
$task_list = Cache::get('task_list');
/* 若任务列表未初始化则初始化之 */
if (is_null($task_list)) {
return [];
}
return $task_list;
}
这样,所有的并发会在Cache::add处串行起来,解锁时forget这个锁即可。
这样看起来似乎挺美好的,但是真正跑起来会发现总是会被锁卡住,仔细一想便会发现,原因其实很简单,如果程序正常运行了整个加解锁流程肯定没有问题,可是,如果程序在加锁后、解锁前这段时间内崩溃了,锁就没有被清除,导致后续的程序一直等不到锁。
那么有原因了,对症下药,我们在laravel的异常处理中,针对所有异常退出,都清除本程序加的锁即可:
function unlock_task_list() {
global $task_list_locked;
if (!empty($task_list_locked)) {
Cache::forget('task_list_lock');
$task_list_locked = false;
Log::notice("task list 解锁(异常处理)");
} else {
Log::notice("task list 未加锁(异常处理)");
}
}
App::error(function(Exception $exception, $code)
{ {
/* 异常退出时清除全局任务列表的锁 */
unlock_task_list();
Log::error($exception);
});
相应地,需要修改加锁部分添加全局变量表示是否被本进程加锁,以防止异常退出时清除了其它进程加的锁:
/**
* 从缓存中获取全局任务列表
*
* @return array 任务列表
*/
public static function getTaskListFromCache() {
/* 手动加锁以防止同时操作任务列表冲突 */
while (!Cache::add('task_list_lock', true, 1)) {
}
global $task_list_locked;
$task_list_locked = true;
Log::notice('task list 加锁');
$task_list = Cache::get('task_list');
/* 若任务列表未初始化则初始化之 */
if (is_null($task_list)) {
return [];
}
return $task_list;
}
需要注意的,如果有其它异常捕捉返回了Response,会导致程序不进入上面的基础异常捕捉,于是需要在该异常处理中也调用unlock_task_list。