前两天在写程序的时候,需要在内存中维护一个任务队列,但是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。