You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel Redis队列发送Nexmo短信触发限流及重试超限异常排查

问题分析

你遇到的Illuminate\Contracts\Redis\LimiterTimeoutException和后续的MaxAttemptsExceededException,核心原因是Redis限流锁的竞争与任务重试次数的冲突,再加上代码里的一个小拼写错误雪上加霜。即使移除Nexmo逻辑问题依然存在,说明问题完全出在限流和队列重试的交互上。

先拆解几个关键问题:

  1. 限流逻辑的工作机制:Redis::throttle('throttle:sms')->allow(1)->every(10) 表示10秒内只允许1个任务执行。当队列中有多个任务时,后续任务会因无法获取锁进入回调,被release(10)放回队列等待10秒。但如果任务的最大重试次数设置得过低,这些反复等待的任务很快就会耗尽重试次数,触发MaxAttemptsExceededException。
  2. 拼写错误导致未捕获异常:你代码里的catch(Exeption $e)是拼写错误(应该是Exception),这会导致Nexmo发送时的异常无法被捕获,直接让任务失败重试,进一步加重队列的重试压力。
  3. 多Worker进程竞争:如果启动了多个Worker处理sms队列,它们会同时竞争限流锁,导致更多任务无法获取锁,更快耗尽重试次数。

解决方案

1. 优先修正异常捕获的拼写错误

这是最容易被忽略的点,直接导致异常无法被处理:

// 错误写法
catch(Exeption $e){ log::warning($e); }

// 正确写法
catch(\Exception $e){ 
    Log::warning("SMS发送失败: " . $e->getMessage()); 
}

(注意加上命名空间前缀\,或者在文件顶部引入use Exception;)

2. 增加任务的最大重试次数

默认的任务重试次数通常是3次,远远不够应对10秒间隔的限流场景。在你的SendSMS任务类中添加:

// Laravel 8+ 使用 maxTries
public $maxTries = 20; // 允许最多重试20次,每次间隔10秒,最长等待200秒

// 或者设置截止时间(推荐),2小时内允许重试
public function retryUntil()
{
    return now()->addHours(2);
}

// Laravel 7及以下使用 tries
public $tries = 20;

3. 确保单个Worker进程处理SMS队列

多个Worker会同时竞争限流锁,导致大量任务无法获取锁并触发重试。启动Worker时明确使用单进程:

php artisan queue:work --queue=sms --timeout=60 --sleep=5 --max-tries=20
  • --sleep=5:让Worker在无任务时等待5秒再轮询,减少Redis查询压力
  • --max-tries=20:全局设置该Worker处理任务的最大重试次数(也可以在任务类中单独设置)

4. 优化限流逻辑(可选,但更稳定)

替换Redis::throttle为RateLimiter facade的attempt方法,逻辑更清晰,且避免回调嵌套:

use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Log;

public function handle()
{
    $throttleKey = 'throttle:sms';
    
    // 尝试获取限流锁:10秒内允许1次执行
    $isAllowed = RateLimiter::attempt(
        $throttleKey,
        1, // 允许的执行次数
        function () {
            Log::info($this->notifId . " startAt : " . date('H:i:s'));
            
            try {
                $before = microtime(true);
                $response = Nexmo::message()->send([
                    'to' => $this->to,
                    'from' => '16105552344',
                    'text' => $this->message
                ]);
                
                $notif = NotificationHistory::find($this->notifId);
                $notif->nexmo_message_id = $response->getMessageId();
                $notif->save();
                
                $after = microtime(true);
                Log::info("任务执行耗时: " . ($after - $before) . " sec");
            } catch (\Exception $e) {
                Log::warning("SMS任务失败: " . $e->getMessage());
            }
        },
        10 // 锁的有效期10秒
    );

    // 如果无法获取锁,延迟10秒再执行
    if (!$isAllowed) {
        Log::error("无法获取SMS限流锁,延迟10秒重试");
        return $this->delay(10);
    }
}

验证方法

  1. 先修正拼写错误,启动单个Worker
  2. 分发8个任务,观察日志:每个任务应该间隔10秒执行一次,不会触发MaxAttemptsExceededException
  3. 如果仍然有问题,检查Redis连接是否正常,或者调整maxTries到更大的值(比如30)

内容的提问来源于stack exchange,提问作者Alexis

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.08 16:32:35