如何检测可执行JAR启动/运行完成状态 避免cron任务执行重叠
纯Java层实现的任务串行调度方案
无需修改服务器crontab配置,不需要root权限,完全在业务代码层面实现任务互斥、执行周期判断,适配当前场景。
核心思路
废弃原来靠cron固定时间间隔避让的逻辑,把两层判断下沉到每个JAR的启动前置逻辑中:
- 当前任务今日是否符合执行规则,不符合直接退出
- 抢占全局跨进程排他锁,抢占成功才执行业务逻辑,抢占失败就轮询等待,直到前序任务释放锁
所有JAR共用同一个全局锁,从机制上保证同一时间永远只有一个维护任务在运行,彻底避免并发访问数据库的问题。
具体实现
1. 跨进程排他锁(核心防重叠逻辑)
使用Java NIO自带的FileLock实现操作系统级别的排他锁,相比自己写状态标记文件、调用系统命令查进程的方案更可靠:
- 锁由操作系统持有,任务进程异常退出、OOM、被强制kill时,锁会自动释放,不会出现死锁
- 不需要依赖任何第三方组件,JDK自带能力即可实现
- 不需要高权限,只要Java进程对锁文件路径有读写权限即可
不推荐通过jps、ps命令检测进程是否存在的方案,这类方案依赖系统命令输出,容易受用户权限、僵尸进程、进程名重复影响,判断准确率低。
2. 任务执行周期判断
把不同任务的执行频率规则直接写在代码中,不需要维护复杂的cron规则。你甚至可以把所有任务的cron触发时间都设为每日凌晨按原间隔依次触发,任务启动后自行判断当日是否需要执行,不需要执行就直接退出,大幅降低crontab的维护成本。
支持提到的所有规则:每日执行、固定间隔天数执行、指定星期几执行,规则调整只需要改代码,不需要登录服务器修改调度配置。
3. 兜底容错
- 配置锁等待超时时间:比如设置最大等待时长为2小时,超过时长还没拿到锁就打错误日志退出,避免前序任务死循环导致所有后续任务永久阻塞
- 记录全链路执行状态:把每个任务的拿锁时间、执行开始/结束时间、执行结果、耗时都写入本地状态文件,方便排查问题
- 锁资源在finally块中释放,避免业务异常导致锁未正常释放
最简实现代码
你可以把这段逻辑封装成公共的启动类,所有任务JAR都依赖这个公共类,启动时传入自身任务标识即可,原有业务逻辑不需要大改:
import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.io.RandomAccessFile; import java.time.DayOfWeek; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.Properties; import java.io.FileInputStream; import java.io.FileOutputStream; public class MaintenanceTaskBootstrap { // 所有任务共用同一个锁文件,路径所有节点保持一致即可,不需要提前创建 private static final String GLOBAL_LOCK_PATH = "/data/run/maintenance_tasks.lock"; // 任务执行状态存储文件,记录每个任务上次成功执行的日期 private static final String TASK_STATUS_PATH = "/data/log/maintenance_tasks_status.properties"; // 拿锁最大等待时长:2小时,可根据业务实际最长耗时调整 private static final long MAX_WAIT_LOCK_MS = 2 * 60 * 60 * 1000; // 拿锁重试间隔:30秒 private static final long LOCK_RETRY_INTERVAL_MS = 30 * 1000; public static void main(String[] args) throws Exception { String currentTaskName = args[0]; // 第一步:判断当前任务今日是否需要执行 if (!isTaskScheduledToday(currentTaskName)) { System.out.printf("[%s] 任务%s今日无需执行,进程退出%n", LocalDateTime.now(), currentTaskName); return; } // 第二步:循环尝试抢占全局排他锁 System.out.printf("[%s] 任务%s启动,等待获取全局执行锁...%n", LocalDateTime.now(), currentTaskName); long startWaitTime = System.currentTimeMillis(); FileLock globalLock = null; RandomAccessFile lockFile = new RandomAccessFile(GLOBAL_LOCK_PATH, "rw"); FileChannel lockChannel = lockFile.getChannel(); while (true) { globalLock = lockChannel.tryLock(); if (globalLock != null) { break; } if (System.currentTimeMillis() - startWaitTime > MAX_WAIT_LOCK_MS) { System.err.printf("[%s] 任务%s等待锁超时,退出,请检查前序任务是否异常卡死%n", LocalDateTime.now(), currentTaskName); lockChannel.close(); lockFile.close(); System.exit(1); } Thread.sleep(LOCK_RETRY_INTERVAL_MS); } System.out.printf("[%s] 任务%s获取锁成功,开始执行业务逻辑%n", LocalDateTime.now(), currentTaskName); try { // 调用对应任务的原有业务逻辑,把原main方法的逻辑迁移到对应分支即可 executeTaskBusiness(currentTaskName); // 执行成功后更新上次执行时间记录 updateTaskLastRunTime(currentTaskName); System.out.printf("[%s] 任务%s执行完成,释放锁%n", LocalDateTime.now(), currentTaskName); } finally { globalLock.release(); lockChannel.close(); lockFile.close(); } } private static boolean isTaskScheduledToday(String taskName) throws Exception { Properties statusProp = new Properties(); try (FileInputStream fis = new FileInputStream(TASK_STATUS_PATH)) { statusProp.load(fis); } LocalDate today = LocalDate.now(); switch (taskName) { case "task1": case "task3": // 每日执行 return true; case "task2": // 每两天执行一次 String lastRunDateStr = statusProp.getProperty(taskName + ".lastRun"); if (lastRunDateStr == null) return true; LocalDate lastRunDate = LocalDate.parse(lastRunDateStr); return !today.minusDays(2).isBefore(lastRunDate); case "task4": // 仅周一、周三、周五执行 DayOfWeek currentDayOfWeek = today.getDayOfWeek(); return currentDayOfWeek == DayOfWeek.MONDAY || currentDayOfWeek == DayOfWeek.WEDNESDAY || currentDayOfWeek == DayOfWeek.FRIDAY; default: return true; } } private static void updateTaskLastRunTime(String taskName) throws Exception { Properties statusProp = new Properties(); try (FileInputStream fis = new FileInputStream(TASK_STATUS_PATH)) { statusProp.load(fis); } statusProp.setProperty(taskName + ".lastRun", LocalDate.now().toString()); try (FileOutputStream fos = new FileOutputStream(TASK_STATUS_PATH)) { statusProp.store(fos, "maintenance task last run status"); } } private static void executeTaskBusiness(String taskName) { // 这里写每个任务对应的原有业务逻辑即可 } }
注意事项
- 所有任务JAR中配置的全局锁文件路径、状态文件路径必须完全一致,否则锁无法互斥
- 提前确认Java进程对两个文件所在的目录有读写权限,不需要root权限
- 如果后续需要新增任务,只需要在
isTaskScheduledToday方法中增加对应的执行规则即可,不需要调整cron配置 - 该方案仅适用于所有任务都部署在同一台服务器的场景,如果是多节点部署需要替换成基于数据库/Redis的分布式锁,单服务器cron调度的场景完全适用
内容的提问来源于stack exchange,提问作者user2526586
相关产品推荐
相关产品推荐

