Spring Boot多Nginx服务器下避免定时任务重复执行方案咨询
问题解答
@Transactional能不能解决重复执行问题?
不能。你当前的逻辑是先查询当日报表是否存在,再执行生成逻辑。就算给addDailyReports加上@Transactional,默认事务隔离级别下,两台服务器的并发请求会同时读到count=0的快照(快照读),进而都执行报表生成逻辑,最终还是会产生重复数据。事务只能保证单节点内的操作原子性,解决不了跨节点的并发竞争问题。
可行的解决方案
1. 数据库悲观锁优化查询逻辑
给查询语句加上排他锁,让第一个执行的事务锁住相关数据,后续事务必须等待前一个事务完成后才能查询,避免并发判断的问题。修改你的查询代码:
// 替换原有查询方法,新增带排他锁的查询 @Query(value = "SELECT COUNT(*) FROM your_table WHERE report_date = ?1 AND report_frequency = ?2 FOR UPDATE", nativeQuery = true) Long getCountByReportdateAndReporeFrequencyForUpdate(LocalDate reportDate, String frequency); @Override @Transactional public String addDailyReports(LocalDate date, Long branchCode) { LocalDate reportDate = (date != null) ? date : LocalDate.now(); // 使用加锁的查询方法 Long count = branchUserMakerRepos.getCountByReportdateAndReporeFrequencyForUpdate(reportDate, "Daily"); if (count > 0) { return count + " reports exist for the report date " + reportDate; } // 报表生成逻辑... }
注意要给addDailyReports加上@Transactional,确保锁的范围覆盖整个查询+生成逻辑。
2. 分布式锁
用Redis或ZooKeeper实现分布式锁,只有获取到锁的服务器才能执行定时任务,其他节点直接跳过。以Redis为例,执行任务前先尝试获取锁:
public void runDailyTask() { String lockKey = "daily-report-generate-lock"; // 用SETNX命令加锁,设置过期时间防止锁泄漏 Boolean lockAcquired = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "locked", Duration.ofMinutes(30)); if (Boolean.TRUE.equals(lockAcquired)) { try { branchUserService.addDailyReports(null, null); } finally { // 任务完成后释放锁 stringRedisTemplate.delete(lockKey); } } else { // 未获取到锁,跳过当前执行 log.info("Another server is generating daily reports, skip current execution"); } }
这种方法适合跨节点的并发控制,要注意处理锁过期和异常场景,避免死锁。
3. 改用集中式定时任务框架
放弃@Scheduled的本地定时,改用Quartz、XXL-Job、Elastic-Job这类分布式定时任务框架。这些框架原生支持分布式调度,能保证同一时间只有一个节点执行任务,无需自己处理并发逻辑,比如XXL-Job可直接在后台配置任务执行策略。
4. 节点选主机制
让集群自动选举出一个主节点,仅主节点执行定时任务,备节点不执行。可以通过数据库表实现简单选主:
- 创建节点心跳表,记录节点ID、心跳时间、是否为主节点
- 每个节点定时更新自己的心跳时间
- 节点启动或定时检查时,判断是否存在存活的主节点,若没有则竞选为主节点
- 定时任务仅在主节点上触发执行
内容的提问来源于stack exchange,提问作者Ravi Dobariya
相关产品推荐
相关产品推荐

