在日常开发过程中,我们或多或少都会涉及到数据报表、统计分析、定时任务之类的应用场景。针对这些场景,我们可以采用 Hadoop 生态圈中的相关技术。
但是 Hadoop 是一种重量级的实现方案,实际应用过程中存在入门门槛过高、学习周期过长、开发和维护困难等问题,对于某些体量并不是特别大的应用场景而言并不建议使用。相反,我们希望找到一种轻量级实现方案来支持日常批处理功能,这就是今天我们要讨论的话题。

图 1 批处理需求和实现方案
那么,如何实现轻量级的批处理呢?让我们先从相关设计理念开始讲起。
轻量级批处理基本架构
在考虑批处理架构之前,我们站在最高的抽象度上,可以把批处理过程看作是一个流程,包括读数据、处理数据和写数据,而这些数据背后是各种数据存储媒介。

图 2 批处理流程的抽象
批处理架构的抽象过程
和普通应用程序一样,对于如何实现上述流程,我们第一个需要考虑的设计问题是如何确定所需要实现组件之间的职责和功能,这就需要引入分层思想。
分层结构上,批处理架构可以抽象为三个主要层次,基础架构层、核心处理层和应用开发层。
基础架构层提供了通用的读、写、处理服务,是对各种数据媒介的操作封装;核心处理层关注于批处理的执行过程,包括对批处理任务和流程的抽象以及如何启动、控制这些任务与流程;应用开发层则包含应用程序需要实现的业务代码。

图 3 批处理技术的分层架构
有了分层架构之后,我们接下来对批处理的处理对象进行建模,从而引出任务(Job)的概念。Job 就是批处理的基本对象,每个 Job 可以包含一个或多个步骤(Step),每个 Step 负责与具体的外部媒介交互,并产生计算结果。
我们知道,对于批处理应用而言,处理的对象并不是一条数据,而是一批数据的集合(Batch)。因此,在读取数据阶段,读取器(Reader)可以单条执行读取操作,并交由数据处理器(Processor)进行转换或过滤处理,但在写数据的过程中,数据写入器(Writer)往往会以一批数据为基本操作单元。

图 4 批处理时序图
批处理的健壮性
在上面这个时序图中,每一步都可能出现问题。因此,我们需要在出现问题时仍然能够确保批处理流程执行完毕,这就需要引入健壮性(Robustness)的概念。我们可以把批处理的健壮性简单理解为是一种智能化机制,即在长时间不需要开发人员或业务人员干预的情况下仍然可以自动处理各种异常情况。
那么,如何实现健壮性呢?结合批处理的处理特性,我们可以梳理健壮性的不同实现策略,常见的保护三种,即忽略、重试和重启。

图 5 批处理健壮性的三种策略
忽略的含义在于,对于那些并不影响批处理执行流程的异常情况,我们没有必要停止整个任务,而是可以选择性地忽略这些异常。场景可以忽略的异常包括数字格式错误等。
有时候,导致任务执行出现异常的原因并不是数据或代码有问题,而是那些瞬态异常,常见的包括网络访问失败或数据库锁等。针对这些瞬态异常,我们可以采取带有重试次数限制的重试策略。
与前面两种情况不同,有时候因为业务处理异常同样会导致批处理执行失败。显然这时候采用忽略或重试策略是解决不了问题的。我们需要暂停任务,然后修复代码问题之后再重新执行任务,这就是重启策略。
在一个成熟的批处理基本架构中,开发人员可以综合使用这三种健壮性处理策略。而这三种策略的采用时机也是可以动态调整的,典型的例子包含:刚开始出现异常情况时,我们可以采用重试机制,但当重试出现三次之后如果仍然抛出异常,那么我们就需要转为采用重启策略了。
轻量级批处理框架:Spring Batch
介绍完轻量级批处理的基本架构之后,我们来讨论它的实现工具。业界有许多轻量级批处理框架,今天我主要给你介绍的是,在 Spring 家族中专门针对轻量级批处理技术提供的相应解决方案,这就是 Spring Batch。
Spring Batch 基于 Spring 和 Java,实现了批处理的基本架构,并支持批处理健壮性。Spring Batch 内置包括文件、数据库、消息中间件、外部服务在内的多种数据读取和写入机制,也对数据处理过程做了转换和过滤抽象。
针对使用场景,Spring Batch 也给提供了系统化的支持。使用 Spring Batch 可以应用于定期提交批处理任务、按顺序处理依赖的任务、部分处理、批处理事务支持以及消息传递等基础设施集成等场景。
Spring Batch 的设计理念之一在于以接口形式暴露通用核心的服务并提供了完整的默认实现。Spring Batch 的核心接口如下,分别对应批处理的三个主要步骤。可以看到读和处理操作的对象是一个 Item,而写操作则使用 Item 列表。这点与我们前面的分析完全一致。代码 1。
csharp
public interface ItemReader<T> {
T read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException;
}
public interface ItemProcessor<I, O> {
O process(I item) throws Exception;
}
public interface ItemWriter<T> {
void write(List<? extends T> items) throws Exception;
}
其中,ItemReader 和 ItemWriter 分别实现数据读取和数据写入,对象可以包括文本文件、XML 文件、数据库、服务和 JMS 等多种形式。
然后,ItemProcessor 代表处理器模型,Spring Batch 中的数据处理有转换(Transformation)和过滤(Filtering)两种主要的场景。转换的形式有多种,基本的数据状态和数据结构转换比较常见。而过滤的目的是决定是否进行 Writer 操作。无论是转换还是过滤,Spring Batch 都为开发人员提供了扩展接口,我们可以基于业务逻辑实现自定义的复杂机制。
针对批处理的健壮性,Spring Batch 也同时支持 Skip、Retry 和 Restart 这三种策略。为了实现这三种策略,Spring Batch 对 Job 进行了进一步抽象。对于任何一个 Job,运行过程中都存在一种一对多的关系,即 Job 的定义应该只有一份,但可以有多次执行。因此,Spring Batch 中针对每个 Job 会生成一个 Job Instance,然后每次 Job 执行对应一个 Job Execution。这样,健壮性策略在过程中会根据 Job Instance 生成一个新的 Job Execution 并放在 Job Repository 中。

图 6 Spring Batch 中的 Job Instance 和 Execution
上图中的 Job Repository 保存批处理运行时详细信息,Spring Batch 支持 In-memory 和 JDBC 两种持久化实现策略。
总结来说,站在最高的抽象层次上,所有批处理的过程都包括读数据、处理数据和写数据三大部分。虽然,普通的数据处理技术也可以实现这三个步骤,但一些关键特性使得批处理与这些数据数据技术有本质性区别。批处理面向海量数据,要求在实现自动化的前提下还需要保证处理过程的健壮性、可靠性和性能。Spring Batch 作为一款优秀的批处理开源框架,为开发人员提供了轻量级批处理的一整套解决方案。
总结
我们系统分析了轻量级批处理技术的方方面面。我们看到作为一个常见的技术体系,想要实现批处理,开发人员需要考虑的维度非常多。
我们首先站在架构设计的角度,对批处理执行过程进行了抽象,并给出了分层架构。在分层架构的基础上,我们就引出了批处理的健壮性需求,并同样阐述了三种实现方案。基于这些讨论的设计思想,业界也存在一些轻量级批处理框架,比如 EasyBatch。今天的内容我们关注 Spring 家族的 Spring Batch 框架,可以看到该框架的实现过程和我们的设计思想是高度一致的。