参考: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 batch 的 class 配置文件 BatchConfig 中,我们在下面的两个方法中,分别将事务管理器加入到 JobRepository 和 JobLauncher 中。参考代码如下:
@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 batch 的 class 配置文件 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;
即该表只有两个字段,分别为 key 和 value。而且 key 和 value 的大小分别为 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 的时候发现有异常,则跳过该数据 item 的 step 的操作。
参考代码如下:
@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/