Laravel Cron任务重复发送投诉提醒邮件问题求助
问题描述
我开发了一个支持用户提交品牌投诉的网站,需求为用户创建投诉3天后发送一次提醒邮件及站内通知,已通过is_reminder_sent字段控制仅发送一次。该代码在本地环境运行正常,但部署到Ubuntu服务器后,Cron任务每分钟会重复发送数十封邮件(有时40-50封),请求排查问题原因。
相关代码
COMPLAINTREMINDER.php
protected $signature = 'complaints:reminder'; public function handle() { $complaints = Complaints::with('author')->whereIn('status', ['now_active', 'answered'])->where('approved_at', '<=', Carbon::now()->subHour(72))->get(); foreach($complaints as $complaint) { $subject = "random subject"; $text = "randomtext"; $textPanel = "Text for notification panel"; if ($complaint->is_reminder_sent == 0) { $this->sendMailByZoho($complaint->author->email, $subject, $text); $this->sendPanelNotification($complaint->author, $textPanel, 'memberNewComplaint', 'type'); $complaint->update(['is_reminder_sent' => '1']); } } return 0; }
KERNEL.php
protected function schedule(Schedule $schedule) { $schedule->command('complaints:reminder')->everyMinute(); }
CRONTAB
* * * * * cd /path_to_my_project && php artisan schedule:run >> /dev/null 2>&1
排查原因及解决方案
1. 并发执行引发竞态条件
当前Cron每分钟执行一次schedule:run,如果complaints:reminder命令执行时间超过1分钟(比如处理大量投诉、邮件接口响应慢),下一分钟的Cron会启动新进程,多个进程同时处理同一批投诉——此时is_reminder_sent字段还未完成更新,就会导致重复发送。
解决方案:给调度任务加锁,确保同一时间仅一个进程执行:
protected function schedule(Schedule $schedule) { $schedule->command('complaints:reminder')->everyMinute()->withoutOverlapping(); }
2. 时区不一致导致查询范围异常
本地环境与服务器时区不匹配,会让Carbon::now()->subHour(72)计算出的时间出现偏差,可能每次查询出大量不符合预期的投诉,甚至重复读取已处理的记录。
解决方案:
- 修改Laravel配置文件
config/app.php,设置正确时区,例如:'timezone' => 'Asia/Shanghai', - 同步服务器系统时区,执行命令:
timedatectl set-timezone Asia/Shanghai
3. 数据库脏读导致重复处理
循环中更新is_reminder_sent时,若未锁定记录,其他进程可能在更新完成前就读取到is_reminder_sent=0的条目,进而重复发送。
解决方案:查询时添加行锁,同时提前过滤未发送的投诉,减少循环内判断:
$complaints = Complaints::with('author') ->whereIn('status', ['now_active', 'answered']) ->where('approved_at', '<=', Carbon::now()->subHour(72)) ->where('is_reminder_sent', 0) ->lockForUpdate() ->get();
4. Cron任务重复注册
服务器上可能存在多个用户设置了相同的Cron任务,或同一用户的crontab中重复添加了该条目,导致每分钟多次触发schedule:run。
解决方案:执行crontab -l查看当前用户的Cron列表,删除重复条目。
5. 邮件/通知发送失败未更新状态
若sendMailByZoho或sendPanelNotification执行失败(如接口超时、网络问题),后续的$complaint->update可能无法执行,导致任务下次仍会处理该投诉。
解决方案:添加异常捕获与事务控制,确保状态更新逻辑可靠:
foreach($complaints as $complaint) { $subject = "random subject"; $text = "randomtext"; $textPanel = "Text for notification panel"; if ($complaint->is_reminder_sent == 0) { try { $this->sendMailByZoho($complaint->author->email, $subject, $text); $this->sendPanelNotification($complaint->author, $textPanel, 'memberNewComplaint', 'type'); $complaint->update(['is_reminder_sent' => 1]); } catch (\Exception $e) { \Log::error("投诉ID {$complaint->id} 发送提醒失败:{$e->getMessage()}"); // 可选:若无需重试,可直接标记为已发送;若需重试则保留is_reminder_sent=0 // $complaint->update(['is_reminder_sent' => 1]); } } }
内容的提问来源于stack exchange,提问作者TURGUT SALGIN

