Spring batch的事务简介

参考:Spring batch 的高级特性--监听,异常处理,事务

1、Spring batch 的事务处理机制

1.1 Spring batch 的事务简介

Spring batch 的事务有如下的特点:

1) step 之间事务独立
2)step 划分成多个 chunk 执行, chunk 事务彼此独立,互不影响
3)chunk 定义,例如有 chunk (N),即读取 N 条数据作为一个 chunk, chunk 开始开启一个事务,正常结束提交
4)事务提交条件: chunk 执行正常,未抛 RuntimeExecption
5)默认情况下, Reader、Processor、Writer 抛出未捕获 RuntimeException,当前 chunk 事务回滚,step 失败,job 失败
6)Spring batch 可以设置 retryLimit,即重试次数。如果重试达了指定次数,或者重试策略不满足时,step 失败,job 失败。
7)Spring batch 可以设置 skipLimit,即跳过次数。如果 Spring batch 同时设置了 retryLimit 和 skipLimit,则当 retryLimit 次数达到后,则进行 skip 操作。如果重试次数达了指定次数,到或者重试策略不满足时,step 失败,job 失败。

这些概念可以图形化为下面这几张图:

step 中的事务示意图:

监听器组件的事务示意图:

1.2 Spring batch 的事务配置

Spring 的事务配置方法一般为:

1)先配置好相关的事务管理器。
2)用注解 @EnableTransactionManagement 开启事务支持。
3)在访问数据库的 Service 方法上添加注解 @Transactional 并指定事务管理器。

而 Spring batch 的事务配置也与之相同,除此之外,还可以在 Job 仓库和 JobLauncher 配置中,直接指定好事务管理器,从而省略 2~3 的步骤。

我们可以在 spring batch 的 class 配置文件 BatchConfig 中,配置相关的事务管理器,参考代码如下:

@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

或者

@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityMngFactory) {
    return new JpaTransactionManager(entityMngFactory);
}
上面的代码分别是 两种事务管理器DataSourceTransactionManager 以及 JpaTransactionManager 。其中 DataSourceTransactionManager 针对的是 JDBC 资源的事务管理; JpaTransactionManager 针对的是 JPA 资源的事务管理。如果我们不进行配置,则 Spring batch 会查找相关配置,自动加入这两个事务管理器中的其中一个。但其实除了这两种事务管理器外,Spring 还有其他的几种事务管理器,所以最好显式配置。
还是在 spring batchclass 配置文件 BatchConfig 中,我们在下面的两个方法中,分别将事务管理器加入到 JobRepositoryJobLauncher 中。参考代码如下:
@Bean
public JobRepository jobRepository(DataSource dataSource, PlatformTransactionManager transactionManager) throws Exception{
    JobRepositoryFactoryBean jobRepositoryFactoryBean = new JobRepositoryFactoryBean();
    jobRepositoryFactoryBean.setDataSource(dataSource);
    jobRepositoryFactoryBean.setTransactionManager(transactionManager);
    jobRepositoryFactoryBean.setDatabaseType("MYSQL");
    return jobRepositoryFactoryBean.getObject();
}

@Bean
public SimpleJobLauncher jobLauncher(DataSource dataSource, PlatformTransactionManager transactionManager) throws Exception{
    SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
    jobLauncher.setJobRepository(jobRepository(dataSource, transactionManager));
    return jobLauncher;
}

1.3 Spring batch 事务的使用

上一节中,我们已经配置好 Spring batch 的事务,结合 1.1 节中我们的介绍可以知道,Step 中的 chunk,以及 chunk 中的 reader,processor,writer 都是开启了事务的。也就是说我们只要再在 spring batchclass 配置文件 BatchConfig 中,配置好相关的 step 以及 step 的内部组件,那么这些 step 的组件就会受到事务管理器的管理。
重新打开 BatchConfig 文件,写入相关的 step 组件,参考代码如下:
@Configuration
@EnableBatchProcessing
public class BatchConfig {
    @Bean
    public JobRepository jobRepository(DataSource dataSource,PlatformTransactionManager transactionManager) throws Exception{
        JobRepositoryFactoryBean jobRepositoryFactoryBean = new JobRepositoryFactoryBean();
        jobRepositoryFactoryBean.setDataSource(dataSource);
        jobRepositoryFactoryBean.setTransactionManager(transactionManager);
        jobRepositoryFactoryBean.setDatabaseType("MYSQL");
        return jobRepositoryFactoryBean.getObject();
    }

    @Bean
    public SimpleJobLauncher jobLauncher(DataSource dataSource, PlatformTransactionManager transactionManager) throws Exception{
        SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
        jobLauncher.setJobRepository(jobRepository(dataSource, transactionManager));
        System.out.println(">>>>>>>>>>" + transactionManager.getClass());
        return jobLauncher;
    }

    ......

    //读数据
    @Bean
    @StepScope
    public ListItemReader<String> stepForTranscationReader() throws UnexpectedInputException, ParseException, NonTransientResourceException, Exception {
        System.out.println("------tx step Reader--------");

        List<String> indexVals = new ArrayList<String>();

        indexVals.add("001");
        indexVals.add("002");
        indexVals.add("003");

        indexVals.add("004");
        indexVals.add("005");
        indexVals.add("006");

        indexVals.add("007");
        indexVals.add("008008008008");
        indexVals.add("009");

        indexVals.add("010");
        indexVals.add("011");
        indexVals.add("012");

        ListItemReader reader = new ListItemReader(indexVals);
        return reader;
    }

    .......

    //处理数据    
    @Bean
    @StepScope
    public ItemProcessor stepForTranscationProcessor() throws JsonParseException, JsonMappingException, IOException {
        System.out.println("------tx step Processor--------");
        return new StringToStringDoNotingProcessor();
    }

    ......

    //写数据
    @Bean
    @StepScope
    public ItemWriter<String> stepForTranscationWriter(JdbcTemplate jdbcTemplate) throws JsonParseException, JsonMappingException, IOException {
        System.out.println("------tx step writer--------");

        TestTableWriter writer = new TestTableWriter();
        writer.setJdbcTemplate(jdbcTemplate);
        return writer;   
    }

    //---------------job & step----------------
    ......

    @Bean
    public Job testBatchTranscation(JobBuilderFactory jobs, @Qualifier("step1")Step firstStep, @Qualifier("step2")Step secondStep, @Qualifier("stepForTranscation")Step stepForTranscation, JobExecutionListener listener) {  
        return jobs.get("testBatchTranscation")
                .incrementer(new RunIdIncrementer())
                .listener(listener)
                .start(stepForTranscation)
                .build();
    }

    @Bean
    public Step stepForTranscation(StepBuilderFactory stepBuilderFactory, @Qualifier("stepForTranscationReader")ListItemReader<String> reader,
             @Qualifier("stepForTranscationProcessor")ItemProcessor<String, String> processor, @Qualifier("stepForTranscationWriter")ItemWriter<String> writer) {
        return stepBuilderFactory.get("stepForTranscation")
                .<String, String> chunk(3)
                .reader(reader)
                .processor(processor)
                .writer(writer)
                .build();
    }

    ......
    //@Bean
    //public PlatformTransactionManager transactionManager(DataSource dataSource) {
        //return new DataSourceTransactionManager(dataSource);
    //}

    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityMngFactory) {
        return new JpaTransactionManager(entityMngFactory);
    }

    // end::jobstep[]
    @Bean
    public static JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

上面的这段代码中,其中有两个被引用的类的参考代码:

StringToStringDoNotingProcessor 类:

public class StringToStringDoNotingProcessor implements ItemProcessor<String, String> {

    @Override
    public String process(String item) throws Exception {
        // TODO Auto-generated method stub
        return item;
    }
}
这个是一个模拟 Processor 的代码。一般的,Processor 是对 reader 中的数据 item 进行处理,例如进行校验,格式转换或者运算等等,然后再把处理好的新数据 item 给到 writer。但我们例子中为了化简了这一过程,直接不做任何事。
所以也将其起名为 StringToStringDoNotingProcessor。

另一个 TestTableWriter 类:

public class TestTableWriter implements ItemWriter<String> {

    private JdbcTemplate jdbcTemplate;  

    public JdbcTemplate getJdbcTemplate() {
        return jdbcTemplate;
    }

    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public  void write(List<? extends String> indexVals) throws Exception {
        System.out.println("-------tx step writer--write()-------");

        for(String tmpIndex : indexVals){
            String key = "key_" + tmpIndex;
            String value = "value_" + tmpIndex;

            jdbcTemplate.update("insert into test_tbl values('" + key + "','" + value + "')");
        }   

    }
}
这里自定义实现 wrtier,其中有一个 write 方法,进行对数据库的写处理。一般设置 chunk 的数值,例本例设置为 3,Spring batch 会按这样的机制处理:每当累计有 3 条数据 item 到达 writer 后,会进行一次 write () 方法的调用,即写一次数据库。(若最后一次不足 3 条数据的时候,会进行最后一次写的操作把剩余数据 item 写入)

其他代码讲解:

@Bean
public Step stepForTranscation(StepBuilderFactory stepBuilderFactory, @Qualifier("stepForTranscationReader")ListItemReader<String> reader,
         @Qualifier("stepForTranscationProcessor")ItemProcessor<String, String> processor, @Qualifier("stepForTranscationWriter")ItemWriter<String> writer) {  

    return stepBuilderFactory.get("stepForTranscation")
            .<String, String> chunk(3)
            .reader(reader)
            .processor(processor)
            .writer(writer)
            .build();
}
stepForTranscationReader 中,设置了总共读取 12 条数据 item,而 step 中设置的 chunk 条数是 3,所以,实质上这 12 条数据 item 是分了 4 个 chunk,每个 chunk 读和处理 3 条,然后将 3 条数据 item 一次写入数据库,这里的每个 chunk 都是一个独立的事务,如果在该 step 过程中出错了,则单独对当前出错的 chunk 进行回滚操作。

test_tbl 的建表参考代码如下:

CREATE TABLE `test_tbl` (
  `key` varchar(10) DEFAULT NULL,
  `value` varchar(15) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
即该表只有两个字段,分别为 keyvalue。而且 keyvalue 的大小分别为 10 和 15 个字符。
回到 stepForTranscationReader 中,我们设置的 12 条数据 item 中,第 8 条数据是超过了数据库表的限制的:
List<String> indexVals = new ArrayList<String>();

indexVals.add("001");
indexVals.add("002");
indexVals.add("003");

indexVals.add("004");
indexVals.add("005");
indexVals.add("006");

indexVals.add("007");
indexVals.add("008008008008");
indexVals.add("009");

indexVals.add("010");
indexVals.add("011");
indexVals.add("012"); 
这就是说,当我们使用 Spring batch 处理到第 8 条数据的时候,会报数据库异常。那我们运行一下程序,看看 Spring batch 的事务机制是如何处理的。

启动 Spring boot 以及 Spring batch参考代码如下:

@SpringBootApplication(scanBasePackages={"com.ljp.spring.batchtest"})
@EnableConfigurationProperties
@EnableTransactionManagement 
public class Starter {
    public static void main(String[] args) throws JobExecutionAlreadyRunningException, org.springframework.batch.core.repository.JobRestartException, JobInstanceAlreadyCompleteException, JobParametersInvalidException, SchedulerException, BeansException, JsonProcessingException, ParseException, InterruptedException{
        ApplicationContext context = SpringApplication.run(Starter.class, args); 

        JobLauncher jobLauncher = (JobLauncher)context.getBean("jobLauncher");
        SimpleJob testBatchJob = (SimpleJob) context.getBean("testBatchTranscation");
        JobExecution execution = null;
        try {           
            execution = jobLauncher.run(testBatchJob, new JobParametersBuilder().toJobParameters());
        } catch (JobExecutionAlreadyRunningException | JobRestartException | JobInstanceAlreadyCompleteException
                | JobParametersInvalidException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (org.springframework.batch.core.repository.JobRestartException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }

}

结果如下:

共有 6 条数据写入了 test_tbl 表,java 后台运行 console 信息:

2017-05-08 17:50:21.973 INFO 324136 --- [ main] o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=testBatchTranscation]] launched with the following parameters: [{}]
2017-05-08 17:50:22.108 INFO 324136 --- [ main] o.s.batch.core.job.SimpleStepHandler : Executing step: [stepForTranscation]
------tx step Reader--------
------tx step Processor--------
------tx step writer--------
-------tx step writer--write()-------
-------tx step writer--write()-------
-------tx step writer--write()-------
2017-05-08 17:50:22.591 INFO 324136 --- [ main] o.s.b.f.xml.XmlBeanDefinitionReader : Loading XML bean definitions from class path resource [org/springframework/jdbc/support/sql-error-codes.xml]
2017-05-08 17:50:23.177 INFO 324136 --- [ main] o.s.jdbc.support.SQLErrorCodesFactory : SQLErrorCodes loaded: [DB2, Derby, H2, HSQL, Informix, MS-SQL, MySQL, Oracle, PostgreSQL, Sybase, Hana]
2017-05-08 17:50:23.286 ERROR 324136 --- [ main] o.s.batch.core.step.AbstractStep : Encountered an error executing step stepForTranscation in job testBatchTranscation
org.springframework.dao.DataIntegrityViolationException: StatementCallback; SQL [insert into test_tbl values('key_008008008008','value_008008008008')]; Data truncation: Data too long for column 'key' at row 1; 
nested exception is com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column 'key' at row 1
at org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:102) ~[spring-jdbc-4.3.4.RELEASE.jar:4.3.4.RELEASE]
......
......
2017-05-08 17:50:23.695 INFO 324136 --- [ main] o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=testBatchTranscation]] completed with the following parameters: [{}] and the following status: [FAILED]

解释:

数据库一共写入了 6 条记录,分别是 Spring batch step 中的前两个 chunk 所为(每 3 个数据 item 为一个 chunk)。
然后到处理第 3 个 chunk 的时候, 007,008008008008,009 这 3 个数据 item 中,第 2 个数据 item 超出了数据库的限制长度,所以 Java 后台 console 显示会报出:“ Data too long for column 'key' at row 1;” 的提示。
由于每一个 chunk 我们都设置了事务,所以,这个 chunk 中,哪怕 007 数据是可以写入数据库的,但由于 008008008008 这条数据报错,所以导致整一个 chunk 进行回滚,而 007 数据也进行了回滚。
另外由于每一个 chunk 的事务独立,所以第 3 个 chunk 回滚的事件不会影响到前两个 chunk,所以 001~006 的 6 条数据 item 都能成功写入数据库。

Spring batch 持久化表数据:

数据库表数据查看,发生了回滚: Spring batch 总共 commit 了 2 次事务,分别有 6 条数据写入,对应了 2 个 chunk。而总共读取了 9 条数据,即第 3 个 chunk,但就在这时有数据错误,进行了回滚操作,整个 step 状态为 FAILED

1.4 Spring batch 的容错机制

Spring batch 的容错机制是一种与事务机制相结合的机制,它主要包括有 3 种操作:

1)restart
2)retry
3)skip
其中, restart 是针对 job 来使用retry skip 是针对 step 以及其内部组件来使用
restart 是重启 job 的一个操作。一般的,只有 job 是失败的情况下,才能 restart。前面也说了,相同的作业只能成功运行一次,如果需要再次运行,则需要改变 JobParameters。
retry 是对 job 的某一 step 而言,处理一条数据 item 的时候发现有异常,则重试一次该数据 item 的 step 的操作。
skip 是对 job 的某一个 step 而言,处理一条数据 item 的时候发现有异常,则跳过该数据 itemstep 的操作。

参考代码如下:

@Bean
public Step stepForTranscation(StepBuilderFactory stepBuilderFactory, @Qualifier("stepForTranscationReader")ListItemReader<String> reader,
         @Qualifier("stepForTranscationProcessor")ItemProcessor<String, String> processor, @Qualifier("stepForTranscationWriter")ItemWriter<String> writer) {
    return stepBuilderFactory.get("stepForTranscation")
            .<String, String> chunk(3)
            .reader(reader)
            .processor(processor)
            .writer(writer).faultTolerant().retryLimit(3).retry(DataIntegrityViolationException.class).skipLimit(1).skip(DataIntegrityViolationException.class).startLimit(3)
            .build();
}

新的 step 配置中,比之前多了一些配置项,如下:

.faultTolerant()  
  .retryLimit(3)  
  .retry(DataIntegrityViolationException.class)  
  .skipLimit(1)  
  .skip(DataIntegrityViolationException.class)  
  .startLimit(3)  
即 retry,skip,restart 的配置。
这里设置了允许重试的次数为 3 次,允许跳过的数据最多为 1 条,如果 job 失败了,运行重跑次数最多为 3 次。

重新运行程序,得到新的结果:

如上图所示: 12 条数据中总共有 11 条数据进入到数据库,而过长的 008008008008 数据,则因为设置了 skip,所以容错机制允许它不进入数据库,这次的 Spring batch 最终没有因为回滚而中断。

查阅 Spring batch 的持久化数据表:

可以看出,的确是有一条数据被跳过了,但因为是我们允许它跳过的,所以整个 job 顺利完成,即 COMPLETED

2、参考文档

1,网文《全面解析 spring batch 大数据批处理框架》: http://mt.sohu.com/20161116/n473372684.shtml

2,网文《Spring batch 的事务处理》: http://blog.csdn.net/karott/article/details/44154501

3,Spring batch 官网信息 : http://projects.spring.io/spring-batch/

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值