Laravel Cache 加锁

Reading time ~1 minute

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

挂载网络文件夹后网络故障时文件操作命令卡死

挂载 NFS 或者 Samba 的时候,经常会由于网络故障导致挂载好的链接断掉。此时如果尝试进行 ls、cd、df 等各种命令,只要与此目录沾上边,就会卡住。如果使用了类似 oh-my-zsh 这种配置的,只要在网络目录中,弹出命令提示符前就会直接卡住。这个时候第一反应就是...… Continue reading

路由折腾记 第四弹

Published on September 02, 2017