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
相关产品推荐
相关产品推荐

