扔掉笨重的XXL-JOB;基于Nacos的优雅调度方案

Nacos 负责发现节点,Spring Scheduler 负责触发,节点之间自己选一个人干活

order-service
├── 10.20.4.17:8080
├── 10.20.4.21:8080
└── 10.20.4.33:8080

三台机器都能收到定时触发,但真正执行之前,先去 Nacos 看一下当前有哪些健康节点。

排序。

排第一的执行。

其他节点直接返回。

 

@Component
@RequiredArgsConstructor
public class JobOwner {

    private final NamingService namingService;
    private final Environment env;

    public boolean mine(String serviceName) {
        try {
            List<Instance> nodes =
                    namingService.selectInstances(serviceName, true);

            if (nodes.isEmpty()) {
                return false;
            }

            String winner = nodes.stream()
                    .map(this::nodeKey)
                    .sorted()
                    .findFirst()
                    .orElse("");

            return winner.equals(localNode());
        } catch (Exception e) {
            return false;
        }
    }

    private String nodeKey(Instance node) {
        return node.getIp() + ":" + node.getPort();
    }

    private String localNode() {
        String ip = env.getProperty("spring.cloud.client.ip-address");
        String port = env.getProperty("server.port", "8080");
        return ip + ":" + port;
    }
}

 

@Component
@RequiredArgsConstructor
public class OrderRepairJob {

    private final JobOwner jobOwner;
    private final OrderRepairService repairService;

    @Scheduled(cron = "0 */5 * * * ?")
    public void repair() {
        if (!jobOwner.mine("order-service")) {
            return;
        }

        repairService.fixPendingOrders();
    }
}

 

到这里,其实已经能解决大部分“多实例重复跑任务”的问题。

比如 10.20.4.17 当前排序第一,那它执行。

17 挂了以后,Nacos 健康实例列表里没它了,下一轮自然变成 21 执行。

不需要再维护一个调度中心,也不用专门配置 executor

 

Nacos 节点选主,不等于严格的分布式锁。

比如节点刚好发生上下线、网络抖动,两个实例获取到的健康节点列表存在短暂差异,就可能出现同一个时间窗口执行两次。

这种场景我一般再补一道数据库幂等。

建一张很小的执行记录表:

create table job_fire_guard (
    task_key varchar(80) not null,
    fire_slot varchar(32) not null,
    created_at datetime not null,
    primary key (task_key, fire_slot)
);

真正跑任务之前,抢一下当前时间窗口

@Transactionalpublic boolean acquire(String task, String slot) {    try {

        jdbcTemplate.update("
insert into job_fire_guard(task_key, fire_slot, created_at)
            values (?, ?, now())
            ", task, slot);

        return true;
    } catch (DuplicateKeyException e) {
        return false;
    }
}


@Scheduled(cron = "0 */5 * * * ?")
public void repair() {
    if (!jobOwner.mine("order-service")) {
        return;
    }

    String slot = LocalDateTime.now()
            .format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"));

    if (!fireGuard.acquire("order-repair", slot)) {
        return;
    }

    repairService.fixPendingOrders();
}
posted @ 2026-09-28 23:34  KLAPT  阅读(2)  评论(0)    收藏  举报