You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何确保Spring Batch作业实例在多节点并发启动时仅执行一次

问题描述

我们在两个节点部署了两个Spring Boot后端副本,两个后端会并发启动同一个作业实例。该作业实例通过参数execution-date标识,此参数为向下取整到分钟起始时刻的Java日期(秒、毫秒设为0),执行代码如下:

try {
    var executionDate = DateTimeUtils.roundDownToBeginOfMinute(new Date());
    var params = new HashMap<String, JobParameter>();
    params.put("execution-date", new JobParameter(executionDate, true));

    var jobExecution = jobLauncher.run(job, new JobParameters(params));
    ...

} catch (JobInstanceAlreadyCompleteException | JobExecutionAlreadyRunningException e) {
    log.info("Job already executed on other node");
} catch (Exception e) {
    log.error("Unexpected exception", e);
}

遇到的问题

  • 副本1抛出预期的org.springframework.batch.core.repository.JobExecutionAlreadyRunningException
  • 副本2在327毫秒后抛出:org.springframework.dao.DuplicateKeyException: PreparedStatementCallback; SQL [INSERT into BATCH_JOB_INSTANCE(JOB_INSTANCE_ID, JOB_NAME, JOB_KEY, VERSION) values (?, ?, ?, ?)]; ORA-00001: unique constraint (APP.JOB_INST_UN) violated
  • 所有副本均未执行作业

环境版本

  • Spring Boot: 2.7.15
  • spring-core: 5.3.29
  • spring-batch-core: 4.3.9
  • DB: Oracle Database 19c Enterprise Edition Release 19.0.0.0.0 - Production Version 19.19.0.0.0, Case sensitivity: plain=upper, delimited=exact

已尝试的方案及新问题

设置spring.batch.jdbc.isolation-level-for-create=SERIALIZABLE后,出现新异常行为:

  • 多次出现两个副本启动作业时均抛出CannotSerializeTransactionException,且均未执行作业
  • 出现一次同一作业实例被执行两次的情况,作业仓库表(BATCH_JOB_EXECUTION、BATCH_JOB_EXECUTION_PARAMS)中存在两条相同作业参数的记录

我们已实现一个基于数据库表(含execution-date唯一约束)的可行方案,但希望借助Spring Batch作业仓库实现并发作业执行的锁机制,请问该如何处理?


解决方案

1. 理解并发冲突根源

Spring Batch的作业实例由JOB_NAME + JOB_KEY(作业参数生成)唯一标识。并发场景下,两个节点同时尝试创建同一作业实例时,会触发BATCH_JOB_INSTANCE表的唯一约束冲突。而SERIALIZABLE隔离级别在Oracle中易导致双方事务回滚,甚至因间隙锁逻辑漏洞出现重复执行的极端情况。

2. 配置作业启动器的专属事务管理器

不要全局设置序列化隔离级别,而是为作业启动器单独配置REPEATABLE_READ隔离级别的事务管理器(Oracle默认是READ_COMMITTED,REPEATABLE_READ更适合避免幻读):

@Bean
public PlatformTransactionManager batchTransactionManager(DataSource dataSource) {
    DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
    transactionManager.setDefaultTransactionIsolationLevel(Connection.TRANSACTION_REPEATABLE_READ);
    return transactionManager;
}

@Bean
public JobLauncher jobLauncher(JobRepository jobRepository, PlatformTransactionManager batchTransactionManager) {
    SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
    jobLauncher.setJobRepository(jobRepository);
    jobLauncher.setTransactionManager(batchTransactionManager);
    return jobLauncher;
}

3. 扩展异常处理逻辑

补充捕获DuplicateKeyException,将其作为作业实例已被其他节点创建的判定依据,确保冲突时只有一个节点执行作业:

try {
    var executionDate = DateTimeUtils.roundDownToBeginOfMinute(new Date());
    var params = new HashMap<String, JobParameter>();
    params.put("execution-date", new JobParameter(executionDate, true));

    var jobExecution = jobLauncher.run(job, new JobParameters(params));
    ...

} catch (JobInstanceAlreadyCompleteException | JobExecutionAlreadyRunningException e) {
    log.info("Job already executed on other node");
} catch (DuplicateKeyException e) {
    // 捕获唯一约束冲突,判定为其他节点已创建作业实例
    log.info("Job instance already created by other node");
} catch (Exception e) {
    log.error("Unexpected exception", e);
}

4. 确保作业参数的一致性

确认DateTimeUtils.roundDownToBeginOfMinute方法在所有节点的时间一致性,避免时钟偏差导致execution-date参数不一致。推荐改用数据库统一生成时间参数:

// 调用Oracle数据库的TRUNC函数获取分钟起始时间
String sql = "SELECT TRUNC(SYSDATE, 'MI') FROM DUAL";
Date executionDate = jdbcTemplate.queryForObject(sql, Date.class);

5. 启用Spring Batch分布式锁(可选)

Spring Batch 4.3+支持基于数据库的分布式锁,可通过配置JobRepository启用:

@Bean
public JobRepository jobRepository(DataSource dataSource, PlatformTransactionManager transactionManager) throws Exception {
    JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
    factory.setDataSource(dataSource);
    factory.setTransactionManager(transactionManager);
    factory.setIsolationLevelForCreate("ISOLATION_REPEATABLE_READ");
    // 启用数据库分布式锁
    factory.setLockProvider(new DefaultLockProvider(dataSource));
    return factory.getObject();
}

关键注意事项

  • 避免使用SERIALIZABLE隔离级别,Oracle该级别下的事务逻辑会大幅降低可用性;
  • DuplicateKeyException是并发创建作业实例冲突的核心信号,必须纳入异常处理;
  • 时间参数的全局一致性是避免重复作业的基础,建议使用NTP同步节点时间或统一依赖数据库时间源。

内容的提问来源于stack exchange,提问作者bahnson

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 19:50:31