Spring Batch为批处理提供了一个轻量化的解决方案,它根据批处理的需要迭代处理各种记录,提供事物功能。但是Spring Batch仅仅适用于"脱机"场景,在处理的过程中不能和外部进行任何交互,也不允许有任何输入。
如上图,通常情况下一个独立的JVM程序就是仅仅用于处理批处理,而不要和其他功能重叠。 在最后一层基础设置(Infrastructure)部分主要分为3个部分。JobLauncher
、Job
以及Step
。每一个Step
又细分为ItemReader
、ItemProcessor
、ItemWirte
。使用Spring Batch主要就是知道每一个基础设置负责的内容,然后在对应的设施中实现对应的业务。
当我们构建一个批处理的过程时,必须注意以下原则:
和软件开发的设计模式一样,批处理也有各种各样的现成模式可供参考。当一个开发(设计)人员开始执行批处理任务时,应该将业务逻辑拆分为一下的步骤或者板块分批执行:
以上五个步骤是一个标准的数据批处理过程,Spring batch框架为业务实现提供了以上几个功能入口。
某些情况需要实现对数据进行额外处理,在进入批处理之前通过其他方式将数据进行处理。主要内容有:
批处理的数据源通常包括:
在执行2,3点批处理时需要注意事物隔离等级。
下图是批处理的核心流程图。
(图片来源于网络)
Spring Batch同样按照批处理的标准实现了各个层级的组件。并且在框架级别保证数据的完整性和事物性。
如图所示,在一个标准的批处理任务中组要涵盖的核心概念有JobLauncher
、Job
、Step
,一个Job
可以涵盖多个Step
,一个Job
对应一个启动的JobLauncher
。一个Step
中分为ItemReader
、ItemProcessor
、ItemWriter
,根据字面意思它们分别对应数据提取、数据处理和数据写入。此外JobLauncher
、Job
、Step
也称之为批处理的元数据(Metadata),它们会被存储到JobRepository
中。
简单的说Job
是封装一个批处理过程的实体,与其他的Spring项目类似,Job
可以通过XML或Java类配置,称职为”Job Configuration“.如下图Job
是单个批处理的最顶层。
为了便于理解,可以建立的理解为Job
就是每一步(Step
)实例的容器。他结合了多个Step
,为它们提供统一的服务同时也为Step
提供个性化的服务,比如步骤重启。通常情况下Job的配置包含以下内容
:
Step
执行实例。Step
是否可以重启。Spring Batch为Job接口提供了默认的实现——SimpleJob
类,在类中实现了一些标准的批处理方法。下面的代码展示了如可申明一个Job
。
@Bean
public Job footballJob() {
return this.jobBuilderFactory.get("footballJob") //get中命名了Job的名称
.start(playerLoad()) //playerLoad、gameLoad、playerSummarization都是Step
.next(gameLoad())
.next(playerSummarization())
.end()
.build();
}
JobInstance
是指批处理作业运行的实例。例如一个批处理必须在每天执行一次,系统在2019年5月1日执行了一次我们称之为2019-05-01的实例,类似的还会有2019-05-02、2019-05-03实例。在特定的运行实践中,一个Job
只有一个JobInstance
以及对应的JobParameters
,但是可以有多个JobExecution
。(JobParameters
、JobExecution
见后文)。同一个JobInstance
具有相同的上下文(ExecutionContext
内容见后文)。
前面讨论了JobInstance
与Job
的区别,但是具体的区别内容都是通过JobParameters
体现的。一个JobParameters
对象中包含了一系列Job运行相关的参数,这些参数可以用于参考或者用于实际的业务使用。对应的关系如下图:
当我们执行2个不同的JobInstance
时JobParameters
中的属性都会有差异。可以简单的认为一个JobInstance
的标识就是Job
+JobParameters
。
JobExecution
可以理解为单次运行Job
的容器。一次JobInstance
执行的结果可能是成功、也可能是失败。但是对于Spring Batch框架而言,只有返回运行成功才会视为完成一次批处理。例如2019-05-01执行了一次JobInstance
,但是执行的过程失败,因此第二次还会有一个“相同的”的JobInstance
被执行。
Job
可以定义批处理如何执行,JobInstance
纯粹的就是一个处理对象,把所有的内容、对象组织在一起,主要是为了当面临问题时定义正确的重启参数。而JobExecution
是运行时的“容器”,记录动态运行时的各种属性和上线文,主要有一下内容:
属性 | 说明 |
---|---|
status | 状态类名为BatchStatus,它指示了执行的状态。在执行的过程中状态为BatchStatus#STARTED,失败:BatchStatus#FAILED,完成:BatchStatus#COMPLETED |
startTime | java.util.Date对象,标记批处理任务启动的系统时间,批处理任务未启动数据为空 |
endTime | java.util.Date对象,结束时间无论是否成功都包含该数据,如未处理完为空 |
exitStatus | ExitStatus类,记录运行结果。 |
createTime | java.util.Date,JobExecution的创建时间,某些使用execution已经创建但是并未开始运行。 |
lastUpdate | java.util.Date,最后一次更新时间 |
executionContext | 批处理任务执行的所有用户数据 |
failureExceptions | 记录在执行Job时的异常,对于排查问题非常有用 |
对应的每次执行的结果会在元数据库中体现为:
BATCH_JOB_INSTANCE:
JOB_INST_ID | JOB_NAME |
---|---|
1 | EndOfDayJob |
BATCH_JOB_EXECUTION_PARAMS:
JOB_EXECUTION_ID | TYPE_CD | KEY_NAME | DATE_VAL | IDENTIFYING |
---|---|---|---|---|
1 | DATE | schedule.Date | 2019-01-01 | TRUE |
BATCH_JOB_EXECUTION:
JOB_EXEC_ID | JOB_INST_ID | START_TIME | END_TIME | STATUS |
---|---|---|---|---|
1 | 1 | 2019-01-01 21:00 | 2017-01-01 21:30 | FAILED |
当某个Job
批处理任务失败之后会在对应的数据库表中路对应的状态。假设1月1号执行的任务失败,技术团队花费了大量的时间解决这个问题到了第二天21才继续执行这个任务。
BATCH_JOB_INSTANCE:
JOB_INST_ID | JOB_NAME |
---|---|
1 | EndOfDayJob |
2 | EndOfDayJob |
BATCH_JOB_EXECUTION_PARAMS:
JOB_EXECUTION_ID | TYPE_CD | KEY_NAME | DATE_VAL | IDENTIFYING |
---|---|---|---|---|
1 | DATE | schedule.Date | 2019-01-01 | TRUE |
2 | DATE | schedule.Date | 2019-01-01 | TRUE |
3 | DATE | schedule.Date | 2019-01-02 | TRUE |
BATCH_JOB_EXECUTION:
JOB_EXEC_ID | JOB_INST_ID | START_TIME | END_TIME | STATUS |
---|---|---|---|---|
1 | 1 | 2019-01-01 21:00 | 2017-01-01 21:30 | FAILED |
2 | 1 | 2019-01-02 21:00 | 2017-01-02 21:30 | COMPLETED |
3 | 2 | 2019-01-02 21:31 | 2017-01-02 22:29 | COMPLETED |
从数据上看好似JobInstance
是一个接一个顺序执行的,但是对于Spring Batch并没有进行任何控制。不同的JobInstance
很有可能是同时在运行(相同的JobInstance
同时运行会抛出JobExecutionAlreadyRunningException
异常)。
Step
是批处理重复运行的最小单元,它按照顺序定义了一次执行的必要过程。因此每个Job
可以视作由一个或多个多个Step
组成。一个Step
包含了所有所有进行批处理的必要信息,这些信息的内容是由开发人员决定的并没有统一的标准。一个Step
可以很简单,也可以很复杂。他可以是复杂业务的组合,也有可能仅仅用于迁移数据。与JobExecution
的概念类似,Step
也有特定的StepExecution
,关系结构如下:
StepExecution
表示单次执行Step的容器,每次Step
执行时都会有一个新的StepExecution
被创建。与JobExecution
不同的是,当某个Step
执行失败后并不会再次尝试重新执行该Step
。StepExecution
包含以下属性:
属性 | 说明 |
---|---|
status | 状态类名为BatchStatus,它指示了执行的状态。在执行的过程中状态为BatchStatus#STARTED,失败:BatchStatus#FAILED,完成:BatchStatus#COMPLETED |
startTime | java.util.Date对象,标记StepExecution启动的系统时间,未启动数据为空 |
endTime | java.util.Date对象,结束时间,无论是否成功都包含该数据,如未处理完为空 |
exitStatus | ExitStatus类,记录运行结果。 |
createTime | java.util.Date,JobExecution的创建时间,某些使用execution已经创建但是并未开始运行。 |
lastUpdate | java.util.Date,最后一次更新时间 |
executionContext | 批处理任务执行的所有用户数据 |
readCount | 成功读取数据的次数 |
wirteCount | 成功写入数据的次数 |
commitCount | 成功提交数据的次数 |
rollbackCount | 回归数据的次数,有业务代码触发 |
readSkipCount | 当读数据发生错误时跳过处理的次数 |
processSkipCount | 当处理过程发生错误,跳过处理的次数 |
filterCount | 被过滤规则拦截未处理的次数 |
writeSkipCount | 写数据失败,跳过处理的次数 |
前文已经多次提到ExecutionContext
。可以简单的认为ExecutionContext
提供了一个Key/Value机制,在StepExecution
和JobExecution
对象的任何位置都可以获取到ExecutionContext
中的任何数据。最有价值的作用是记录数据的执行位置,以便发生重启时候从对应的位置继续执行:
executionContext.putLong(getKey(LINES_READ_COUNT), reader.getPosition())
比如在任务中有一个名为“loadData”的Step
,他的作用是从文件中读取数据写入到数据库,当第一次执行失败后,数据库中有如下数据:
BATCH_JOB_INSTANCE:
JOB_INST_ID | JOB_NAME |
---|---|
1 | EndOfDayJob |
BATCH_JOB_EXECUTION_PARAMS:
JOB_INST_ID | TYPE_CD | KEY_NAME | DATE_VAL |
---|---|---|---|
1 | DATE | schedule.Date | 2019-01-01 |
BATCH_JOB_EXECUTION:
JOB_EXEC_ID | JOB_INST_ID | START_TIME | END_TIME | STATUS |
---|---|---|---|---|
1 | 1 | 2017-01-01 21:00 | 2017-01-01 21:30 | FAILED |
BATCH_STEP_EXECUTION:
STEP_EXEC_ID | JOB_EXEC_ID | STEP_NAME | START_TIME | END_TIME | STATUS |
---|---|---|---|---|---|
1 | 1 | loadData | 2017-01-01 21:00 | 2017-01-01 21:30 | FAILED |
BATCH_STEP_EXECUTION_CONTEXT: |STEP_EXEC_ID|SHORT_CONTEXT| |---|---| |1|{piece.count=40321}|
在上面的例子中,Step
运行30分钟处理了40321个“pieces”,我们姑且认为“pieces”表示行间的行数(实际就是每个Step完成循环处理的个数)。这个值会在每个commit
之前被更新记录在ExecutionContext
中(更新需要用到StepListener
后文会详细说明)。当我们再次重启这个Job
时并记录在BATCH_STEP_EXECUTION_CONTEXT中的数据会加载到ExecutionContext
中,这样当我们继续执行批处理任务时可以从上一次中断的位置继续处理。例如下面的代码在ItemReader
中检查上次执行的结果,并从中断的位置继续执行:
if (executionContext.containsKey(getKey(LINES_READ_COUNT))) {
log.debug("Initializing for restart. Restart data is: " + executionContext);
long lineCount = executionContext.getLong(getKey(LINES_READ_COUNT));
LineReader reader = getReader();
Object record = "";
while (reader.getPosition() < lineCount && record != null) {
record = readLine();
}
}
ExecutionContext
是根据JobInstance
进行管理的,因此只要是相同的实例都会具备相同的ExecutionContext(无论是否停止)。此外通过以下方法都可以获得一个ExecutionContext
:
ExecutionContext ecStep = stepExecution.getExecutionContext();
ExecutionContext ecJob = jobExecution.getExecutionContext();
但是这2个ExecutionContext
并不相同,前者是在一个Step
中每次Commit
数据之间共享,后者是在Step
与Step
之间共享。
JobRepository
是所有前面介绍的对象实例的持久化机制。他为JobLauncher
、Job
、Step
的实现提供了CRUD操作。当一个Job
第一次被启动时,一个JobExecution
会从数据源中获取到,同时在执行的过程中StepExecution
、JobExecution
的实现都会记录到数据源中。挡在程序启动时使用@EnableBatchProcessing
注解,JobRepository
会进行自动化配置。
JobLauncher
为Job
的启动运行提供了一个边界的入口,在启动Job
的同时还可以定制JobParameters
:
public interface JobLauncher {
public JobExecution run(Job job, JobParameters jobParameters)
throws JobExecutionAlreadyRunningException, JobRestartException,
JobInstanceAlreadyCompleteException, JobParametersInvalidException;
}
(adsbygoogle = window.adsbygoogle || []).push({});