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

Laravel Cron任务重复发送投诉提醒邮件问题求助

问题排查: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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 04:25:18