redis设置锁,(20)秒还未设置成功,请检查redis是否正常

  1. 问题
  2. 分析
  • 总结
  • 问题

    昨晚上线之后,出现了不少次邮件告警:

    您好  
    #redis设置锁,(20)秒还未设置成功,请检查redis是否正常  
    key: per:sorce-do-task:task_id:task_scanuser_id:8268817, value: per:sorce-do-task:task_id:task_scanuser_id:8268817
    

    分析

    看告警内容,是 redis 设置锁没有成功,一直等待这个锁。看了一下告警处的代码:

    
    /**
     * 设置锁,一定要返回成功,否则一直等待
     * @param $key
     * @param $value
     * @param int $ttl 过期时间
     * @param int $maxsleeptime 最长等待时间,超过该时间发邮件提醒
     * @return mixed
     */
    function set_synchronized_lock($key, $value, $ttl = 0, $maxsleeptime = 20)
    {
        $CI = &get_instance();
        $CI->load->driver('cache');
        $sleep = 0;
        while (!$CI->cache->redis->savenx($key, $value, $ttl)) {
            sleep(1);
            $sleep++;
            if ($sleep > $maxsleeptime) {
                $content = "#redis设置锁,($maxsleeptime)秒还未设置成功,请检查redis是否正常<br>key: $key, value: $value";
                send_email('REDIS_LOCK_NOTIFY', [], [$content]);
                break;
            }
        }
        return true;
    }
    

    这里循环间隔1s不断用 savenx 方法来设置锁(获取锁),然后它失败了,每次失败sleep+1,等到sleep超过最大$maxsleeptime时间(预设为20s)时就发邮件告警。

    是哪些原因会导致它失败呢?

    进入 savenx 源码看,实际上调用的是 redis 的 setnx 命令,这个命令的官方说明是:

    Return Integer reply, specifically:
    
    1 if the key was set
    0 if the key was not set
    

    也就是说在不断循环的尝试过程中,这个 key 表示的值已经存在,也就是说锁一直在被别人占用。

    这个时候,我们需要去看是谁占用了这个 key,我们通过告警邮件中的 key-value 信息找到报错业务处的代码:

    public function do_task_post()
    {
        $lock_key = 'per:sorce-do-task:task_id:' . $task_ids . 'user_id:' . $this->userData['user_id'];  
        $this->set_lock($lock_key, '做积分任务加锁');
        try {
            ...    
            return $this->sendFail(null, '没有可完成的任务');
        } catch(Exception $e) {
            ...
        } finally() {
            $this->release_lock($lock_key, '做积分任务解锁');
        }
    }
    

    这个业务加解锁相关的业务抽象如上,第4行是加锁,第12行是解锁。

    这个业务之前一直正常,理论上锁不应该一直占用。唯一释放锁的地方就是 finally 语句块中的 release_lock(),这个方法本身没有改动过,应该不会突然就一直释放失败了。还有一种可能就是它没有执行。

    是不是因为有慢查询导致它没有执行?这次没有修改到这个业务,看了业务代码,没有慢查询。这个原因PASS。

    我们看到 try 中有 return 语句,是不是这个 return 语句提前返回了?

    本地调试了几次确实提前返回了,切换到上线前的分支版本又调试了几次,发现是先释放锁再返回的。仔细对比代码,发现这次修改了 $this->sendFail() 方法,把里面的 return 改为了 exit 了。原来是这个原因?是它影响了 finally 语句块的执行。

    public function sendFail($data , $msg){  
        $response = [  
            'code' => -1,  
            'data' => $data,  
            'msg'  => $msg  
        ];  
        return; // 昨晚上线后是 exit;
    }
    

    那么 finally 语句跟 returnexit 有什么关系呢?我找了下官方文档看,果然有解释:官方文档关于 finally 解释如下:

    finally 代码块可以放在 catch 之后,或者直接代替它。 无论是否抛出了异常,在 try 和 catch 之后、在执行后续代码之前, 放在 finally 里的代码总是会执行。

    值得注意的是 finally 和 return 语句之间存在相互影响。 如果在 try 或 catch 里遇到 return,仍然会执行 finally 里的代码。 而且,遇到 return 语句时,会先执行 finally 再返回结果。 此外,如果 finally 里也包含了 return 语句,将返回 finally 里的值。

    总结

    • try-catch-finally 的执行顺序是:try 语句 - (exit) - finally语句 -(finally return) - (try return)
    • 改基类代码时要慎重,测试覆盖全。
    • 禁止使用 exit,因为有的框架可能还会在控制器返回之后,会有后置默认处理。

    转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 jaytp@qq.com