多实例Java应用Cron定时MongoDB清理任务冲突解决求助
单实例执行MongoDB清理任务的解决方案
方案1:基于MongoDB分布式锁实现
利用MongoDB的原子操作实现分布式锁,只有成功获取锁的实例才执行清理任务,避免并行冲突。
实现步骤:
- 在MongoDB中创建锁集合
task_locks,文档包含task_name(任务唯一标识,比如mongodb_cleanup)、expire_at(锁过期时间,防止实例挂死导致锁无法释放)、instance_id(持有锁的实例标识)。 - 每个实例生成唯一
instance_id(可用主机名+进程ID拼接)。 - 任务触发时,用
findAndModify原子操作尝试获取锁:仅当锁不存在或已过期时,才更新锁的持有实例和过期时间。 - 若操作返回的锁文档匹配当前实例ID,就执行清理;否则直接跳过。
- 在MongoDB中创建锁集合
核心代码示例:
// 生成实例唯一标识 String instanceId = InetAddress.getLocalHost().getHostName() + "-" + ProcessHandle.current().pid(); // 设置锁过期时间(根据任务实际耗时调整,这里设1小时) Date expireAt = new Date(System.currentTimeMillis() + 3600 * 1000); MongoCollection<Document> lockCollection = mongoClient.getDatabase("admin").getCollection("task_locks"); // 查询条件:锁不存在,或锁已过期 Document query = new Document("task_name", "mongodb_cleanup") .append("$or", Arrays.asList( new Document("expire_at", new Document("$lt", new Date())), new Document("expire_at", null) )); // 更新操作:设置当前实例为锁持有者,更新过期时间 Document update = new Document("$set", new Document("instance_id", instanceId).append("expire_at", expireAt)); FindOneAndUpdateOptions options = new FindOneAndUpdateOptions().upsert(true).returnDocument(ReturnDocument.AFTER); Document result = lockCollection.findOneAndUpdate(query, update, options); // 判断是否成功获取锁 if (result != null && instanceId.equals(result.getString("instance_id"))) { executeCleanup(); // 执行清理逻辑 } else { log.info("已有实例在执行清理任务,本次跳过"); }
方案2:配置中心指定执行实例
通过配置中心维护一个“专属执行实例”配置项,只有匹配该配置的实例才执行任务,简单直接。
实现步骤:
- 在配置中心(如Nacos、Spring Cloud Config)添加配置
task.cleanup.instance.id,值设为某个实例的唯一标识(比如主机名)。 - 每个实例启动时读取该配置,同时获取自身的唯一标识。
- Cron任务触发时,对比自身标识与配置值,一致则执行清理,否则跳过。
- 若执行实例故障,手动修改配置切换到其他实例即可。
- 在配置中心(如Nacos、Spring Cloud Config)添加配置
核心代码示例:
@Value("${task.cleanup.instance.id}") private String designatedInstanceId; // 获取当前实例标识 private String getCurrentInstanceId() { try { return InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException e) { return UUID.randomUUID().toString(); } } @Scheduled(cron = "${task.cleanup.cron}") public void cleanupTask() { if (designatedInstanceId.equals(getCurrentInstanceId())) { executeCleanup(); } else { log.info("非指定执行实例,跳过清理任务"); } }
方案3:基于服务注册中心的领导选举
如果应用已接入服务注册中心(如Eureka、Nacos),可以用领导选举机制,让当选的Leader实例单独执行任务。
实现步骤:
- 借助Curator(ZooKeeper)或注册中心自带的选举组件,每个实例启动后参与Leader选举。
- 只有当选Leader的实例才启用Cron清理任务,其他实例自动禁用任务。
- 当Leader实例故障时,注册中心会自动重新选举新Leader,保证任务持续可用。
核心代码示例(Curator LeaderLatch):
private LeaderLatch leaderLatch; @PostConstruct public void initLeaderLatch() throws Exception { // 初始化Curator客户端连接ZooKeeper CuratorFramework client = CuratorFrameworkFactory.newClient("zk-host:2181", new ExponentialBackoffRetry(1000, 3)); client.start(); // 创建LeaderLatch,指定选举节点路径 leaderLatch = new LeaderLatch(client, "/mongodb-cleanup-leader"); // 监听Leader状态变化 leaderLatch.addListener(() -> { if (leaderLatch.hasLeadership()) { enableCleanupTask(); // 当选Leader,启用任务 } else { disableCleanupTask(); // 失去Leader身份,禁用任务 } }); leaderLatch.start(); }
内容的提问来源于stack exchange,提问作者J Harish
相关产品推荐
相关产品推荐

