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

Spring Cron调度器生产环境重复执行问题:通知邮件多次发送

解决Spring Cron调度生产环境重复发送邮件的问题

先把你提供的配置格式化出来,方便查看:

<bean id="EmailNotificationScheduledTask" class="com.prism.utils.EmailNotificationTask" />
<task:scheduled-tasks scheduler="scheduler">
    <task:scheduled ref="EmailNotificationScheduledTask" method="run" cron="0 0 10 * * MON"/>
</task:scheduled-tasks>

本地运行正常但生产环境重复发邮件,这种情况基本都是环境差异导致的,下面是最常见的几个原因和对应的解决办法:

1. 生产环境多实例部署(最大概率)

本地开发一般是单实例运行,但生产环境为了高可用性,往往会部署多个节点/实例。每个实例都会独立解析Cron表达式并触发任务,自然就会出现重复发送邮件的情况。

解决思路:引入分布式锁
在任务执行前,先尝试获取一把分布式锁,只有成功抢到锁的实例才能执行发送逻辑,其他实例直接跳过。用Redis实现的示例代码如下:

public class EmailNotificationTask {
    @Autowired
    private StringRedisTemplate redisTemplate;
    // 锁的唯一标识,避免和其他任务冲突
    private static final String TASK_LOCK_KEY = "email_notification_task_lock";
    // 锁的过期时间,要长于任务预期执行时长,防止死锁
    private static final long LOCK_EXPIRE_MS = 300000; // 5分钟

    public void run() {
        // 尝试获取锁,成功返回true,失败返回false
        Boolean isLocked = redisTemplate.opsForValue()
                .setIfAbsent(TASK_LOCK_KEY, "running", LOCK_EXPIRE_MS, TimeUnit.MILLISECONDS);
        
        if (isLocked == null || !isLocked) {
            log.info("Another instance is handling the email task, skip this run.");
            return;
        }

        try {
            // 这里放置你的邮件发送逻辑
            sendNotificationEmails();
        } finally {
            // 任务完成后释放锁,必须放在finally中,避免异常导致锁长期占用
            redisTemplate.delete(TASK_LOCK_KEY);
        }
    }
}

2. Spring上下文被重复加载

有时候生产环境的部署配置会导致Spring容器被多次初始化——比如Tomcat中配置了多个WebContext,或者打包时不小心重复引入了配置文件,每个容器都会注册一遍你的调度任务,进而重复触发。

解决思路:排查并避免重复初始化

  • 检查生产服务器的配置(比如Tomcat的server.xml),确保只有一个WebContext加载你的应用;
  • 核对Spring配置文件,避免重复定义<task:scheduled-tasks>或者EmailNotificationScheduledTask这个Bean;
  • 如果使用注解配置,确保@EnableScheduling只添加在一个配置类上,不要多处重复配置。

3. 时区配置不一致(小概率但易忽略)

你的Cron表达式是每周一10点执行,但如果本地和生产环境的时区不同(比如本地用北京时间,生产服务器用UTC时区),可能会导致任务触发时间错位;就算不是重复发送的直接原因,也建议统一时区配置避免其他问题。

解决思路:显式指定时区
在调度配置中添加timezone属性,强制所有环境使用同一时区,比如北京时间:

<task:scheduled ref="EmailNotificationScheduledTask" method="run" cron="0 0 10 * * MON" timezone="Asia/Shanghai"/>

4. 任务逻辑未做幂等处理

就算是单实例部署,如果你的任务逻辑没有检查邮件是否已经发送过,也可能因为任务重试、应用异常重启等原因重复发送邮件。

解决思路:添加幂等校验
在发送邮件前,先查询数据库中的发送记录,标记当天的邮件是否已发送:

public void run() {
    LocalDate today = LocalDate.now();
    // 查询数据库中是否存在今日的发送记录
    boolean hasSentToday = emailSendRecordRepo.existsBySendDate(today);
    if (hasSentToday) {
        log.info("Email notifications already sent today, skip.");
        return;
    }

    // 执行邮件发送逻辑
    sendNotificationEmails();
    // 将发送状态记录到数据库
    emailSendRecordRepo.save(new EmailSendRecord(today));
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.20 11:35:54