Laravel Redis队列发送Nexmo短信触发限流及重试超限异常排查
问题分析
你遇到的Illuminate\Contracts\Redis\LimiterTimeoutException和后续的MaxAttemptsExceededException,核心原因是Redis限流锁的竞争与任务重试次数的冲突,再加上代码里的一个小拼写错误雪上加霜。即使移除Nexmo逻辑问题依然存在,说明问题完全出在限流和队列重试的交互上。
先拆解几个关键问题:
- 限流逻辑的工作机制:
Redis::throttle('throttle:sms')->allow(1)->every(10)表示10秒内只允许1个任务执行。当队列中有多个任务时,后续任务会因无法获取锁进入回调,被release(10)放回队列等待10秒。但如果任务的最大重试次数设置得过低,这些反复等待的任务很快就会耗尽重试次数,触发MaxAttemptsExceededException。 - 拼写错误导致未捕获异常:你代码里的
catch(Exeption $e)是拼写错误(应该是Exception),这会导致Nexmo发送时的异常无法被捕获,直接让任务失败重试,进一步加重队列的重试压力。 - 多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); } }
验证方法
- 先修正拼写错误,启动单个Worker
- 分发8个任务,观察日志:每个任务应该间隔10秒执行一次,不会触发
MaxAttemptsExceededException - 如果仍然有问题,检查Redis连接是否正常,或者调整
maxTries到更大的值(比如30)
内容的提问来源于stack exchange,提问作者Alexis
相关产品推荐
相关产品推荐

