Spring Batch 架构
Spring Batch 的设计兼顾了可扩展性和多样化的终端用户群体。下图展示了支持终端用户开发者实现可扩展性和易用性的分层架构。

图 1. Spring Batch 分层架构
这种分层架构突出了三个主要的高级组件:应用层、核心层和基础设施层。应用层包含所有由开发者使用 Spring Batch 编写的批处理作业和自定义代码。批处理核心层包含启动和控制批处理作业所需的核心运行时类,其中包括 JobOperator、Job 和 Step 的实现。应用层和核心层都构建在一个通用的基础设施层之上。该基础设施层包含通用的读取器、写入器和服务(例如 RetryTemplate),这些组件既被应用开发者(读取器和写入器,如 ItemReader 和 ItemWriter)使用,也被核心框架本身(重试机制,作为其独立的库)所使用。
通用批处理原则与指南
构建批处理解决方案时,应考虑以下关键原则、指导方针和一般注意事项。
-
请记住,批处理架构通常会影响在线架构,反之亦然。设计时应同时考虑两种架构和环境,尽可能使用通用的构建模块。
-
尽可能简化,避免在单个批处理应用中构建复杂的逻辑结构。
-
保持数据处理和存储的物理位置尽量接近(换句话说,让数据靠近处理发生的地方)。
-
最小化系统资源使用,尤其是 I/O。尽可能在内存中执行操作。
-
审查应用程序的 I/O(分析 SQL 语句),以确保避免不必要的物理 I/O。特别需要检查以下四个常见缺陷:
-
在数据可以读取一次并缓存或保存在工作存储中的情况下,却为每个事务都读取数据。
-
在同一事务中,对之前已读取过的数据再次进行读取。
-
导致不必要的表或索引扫描。
-
在 SQL 语句的
WHERE子句中没有指定键值。
-
-
不要在批处理运行中重复做同一件事。例如,如果出于报告目的需要数据汇总,你应该(如果可能的话)在初始处理数据时递增存储的总计,这样你的报告应用就不必重新处理相同的数据。
-
在批处理应用开始时分配足够的内存,以避免在处理过程中进行耗时的重新分配。
-
在数据完整性方面,始终做最坏的打算。插入足够的检查和记录验证以维护数据完整性。
-
尽可能为内部验证实现校验和。例如,平面文件应该有一个尾部记录,说明文件中的记录总数以及关键字段的聚合值。
-
尽早在一个具有真实数据量的类生产环境中规划和执行压力测试。
-
在大型批处理系统中,备份可能具有挑战性,尤其是在系统与在线应用 24-7 全天候并发运行的情况下。数据库备份通常在在线设计中得到了很好的处理,但文件备份也应被视为同等重要。如果系统依赖于平面文件,文件备份程序不仅应该到位并记录在案,还应定期测试。
批处理策略
为帮助设计和实现批处理系统,应以示例结构图和代码框架的形式,为设计者和程序员提供基础的批处理应用程序构建模块和模式。在开始设计批处理作业时,应将业务逻辑分解为一系列步骤,这些步骤可通过使用以下标准构建模块来实现:
-
转换应用程序: 对于外部系统提供或为其生成的每种文件类型,都必须创建一个转换应用程序,以将提供的交易记录转换为处理所需的标准格式。这类批处理应用程序可以部分或全部由翻译实用程序模块组成(参见基本批处理服务)。
-
验证应用程序: 验证应用程序确保所有输入和输出记录正确且一致。验证通常基于文件头和尾、校验和与验证算法,以及记录级别的交叉检查。
-
提取应用程序: 提取应用程序从数据库或输入文件中读取一组记录,根据预定义的规则选择记录,并将这些记录写入输出文件。
-
提取/更新应用程序: 提取/更新应用程序从数据库或输入文件中读取记录,并根据在每个输入记录中找到的数据,对数据库或输出文件进行更改。
-
处理和更新应用程序: 处理和更新应用程序对来自提取或验证应用程序的输入交易进行处理。该处理通常涉及读取数据库以获取处理所需的数据,可能更新数据库并创建记录以供输出处理。
-
输出/格式化应用程序: 输出/格式化应用程序读取输入文件,根据标准格式重构此记录中的数据,并生成用于打印或传输到另一个程序或系统的输出文件。
此外,应为无法通过前述构建模块实现的业务逻辑提供一个基础应用框架。
除了主要的构建模块外,每个应用程序还可以使用一个或多个标准实用步骤,例如:
-
排序 (Sort): 一种读取输入文件并生成输出文件的程序,其中记录已根据记录中的排序键字段重新排序。排序通常由标准系统实用程序执行。
-
拆分 (Split): 一种读取单个输入文件并根据字段值将每条记录写入多个输出文件之一的程序。拆分可以定制,也可以通过参数驱动的标准系统实用程序执行。
-
合并 (Merge): 一种从多个输入文件读取记录并生成一个包含输入文件组合数据的输出文件的程序。合并可以定制,也可以通过参数驱动的标准系统实用程序执行。
批处理应用程序还可以根据其输入源进行分类:
-
数据库驱动型应用程序由从数据库检索的行或值驱动。
-
文件驱动型应用程序由从文件检索的记录或值驱动。
-
消息驱动型应用程序由从消息队列检索的消息驱动。
任何批处理系统的基础都是处理策略。影响策略选择的因素包括:预估的批处理系统容量、与在线系统或其他批处理系统的并发性、可用的批处理窗口。(值得注意的是,随着越来越多的企业希望实现7x24小时不间断运行,明确的批处理窗口正在逐渐消失)。
批处理的典型处理选项包括(按实现复杂度递增排序):
-
在离线模式下,批处理窗口期间的正常处理。
-
并发批处理或在线处理。
-
同时并行处理多个不同的批处理运行或作业。
-
分区处理(同时处理同一作业的多个实例)。
-
上述选项的组合。
商业调度器可能支持部分或全部这些选项。
本节剩余部分将更详细地讨论这些处理选项。请注意,根据经验法则,批处理采用的提交和锁定策略取决于所执行的处理类型,而在线锁定策略也应遵循相同的原则。因此,在设计整体架构时,批处理架构不能简单地作为事后考虑。
锁定策略可以采用仅使用常规数据库锁,或在架构中实现额外的自定义锁定服务。该锁定服务将跟踪数据库锁定(例如,通过将必要信息存储在专用数据库表中),并向请求数据库操作的应用程序授予或拒绝权限。此架构还可实现重试逻辑,以避免在发生锁定情况时中止批处理作业。
1. 批处理窗口中的正常处理 对于在独立批处理窗口中运行的简单批处理过程,如果在线用户或其他批处理过程不需要正在更新的数据,则并发性不是问题,可以在批处理运行结束时执行一次提交。
在大多数情况下,采用更稳健的方法更为合适。请记住,随着时间的推移,批处理系统往往会在复杂性和处理的数据量方面不断增长。如果没有适当的锁定策略,并且系统仍然依赖于单一的提交点,修改批处理程序可能会变得非常困难。因此,即使对于最简单的批处理系统,也应考虑是否需要为重启恢复选项设计提交逻辑,并参考本节后面描述的更复杂情况的相关信息。
2. 并发批处理或在线处理
对于处理可能被在线用户同时更新的数据的批处理应用程序,不应锁定任何在线用户可能需要的数据(无论是在数据库中还是在文件中)超过几秒钟。此外,每完成几笔交易后,应将更新提交到数据库。这样做可以最大限度地减少其他进程无法使用的数据部分,并缩短数据不可用的持续时间。
另一种最小化物理锁定的选择是通过乐观锁定模式或悲观锁定模式实现逻辑行级锁定。
-
乐观锁假设记录争用的可能性较低。通常意味着在每个数据库表中插入一个时间戳列,该表同时被批处理和在线处理使用。当应用程序获取一行进行处理时,它也会获取时间戳。然后,当应用程序尝试更新已处理的行时,更新操作会在
WHERE子句中使用原始时间戳。如果时间戳匹配,则更新数据和时间戳。如果时间戳不匹配,则表明在获取和尝试更新之间,另一个应用程序已更新了同一行。因此,无法执行更新。 -
悲观锁是任何假设记录争用可能性很高的锁定策略,因此需要在检索时获取物理锁或逻辑锁。一种悲观逻辑锁在数据库表中使用专用的锁列。当应用程序检索某行进行更新时,它会在锁列中设置一个标志。设置此标志后,其他尝试检索同一行的应用程序在逻辑上会失败。当设置标志的应用程序更新该行时,它也会清除标志,从而使其他应用程序能够检索该行。请注意,在初始获取和设置标志之间也必须保持数据的完整性——例如,通过使用数据库锁(如
SELECT FOR UPDATE)。还要注意,这种方法与物理锁存在相同的缺点,只是构建超时机制稍微容易一些,如果用户在记录被锁定时离开(例如去吃午餐),该机制可以释放锁。
这些模式并不一定适用于批处理,但它们可能用于并发批处理和在线处理(例如在数据库不支持行级锁定的情况下)。通常来说,乐观锁更适合在线应用,而悲观锁更适合批处理应用。无论何时使用逻辑锁,所有访问受逻辑锁保护的数据实体的应用程序都必须采用相同的方案。
请注意,这两种解决方案都只涉及锁定单个记录。通常,我们可能需要锁定逻辑上相关的一组记录。使用物理锁时,必须非常小心地管理这些锁,以避免潜在的死锁。使用逻辑锁时,通常最好构建一个逻辑锁管理器,该管理器能够理解你想要保护的逻辑记录组,并确保锁的一致性和无死锁性。这个逻辑锁管理器通常使用自己的表来进行锁管理、争用报告、超时机制以及其他相关操作。
3. 并行处理 并行处理允许多个批处理运行或作业并行执行,以最小化批处理的总耗时。只要作业不共享相同的文件、数据库表或索引空间,这就不是问题。如果它们共享资源,则应通过使用分区数据来实现此服务。另一种选择是构建一个架构模块,通过使用控制表来维护相互依赖性。控制表应为每个共享资源包含一行,并指示其是否正在被某个应用程序使用。然后,并行作业中的批处理架构或应用程序将从该表中检索信息,以确定其是否可以访问所需的资源。
如果数据访问不成问题,可以通过使用额外的线程来实现并行处理。在大型机环境中,传统上会使用并行作业类来确保所有进程获得足够的CPU时间。无论如何,解决方案必须足够稳健,以确保所有运行进程都能获得时间片。
并行处理中的其他关键问题包括负载均衡和通用系统资源的可用性,例如文件、数据库缓冲池等。此外,请注意控制表本身很容易成为关键资源。
4. 分区 使用分区可以让大型批处理应用程序的多个版本并发运行。这样做的目的是减少处理长时间批处理作业所需的耗时。能够成功进行分区的进程是指那些输入文件可以被拆分,或者主数据库表可以被分区,从而让应用程序针对不同的数据集运行。
此外,被分区的进程必须设计为仅处理其分配的数据集。分区架构必须与数据库设计及数据库分区策略紧密关联。请注意,数据库分区并不一定意味着数据库的物理分区(尽管在大多数情况下建议这样做)。下图展示了分区方法:

图 2. 分区进程
架构应具备足够的灵活性,以支持动态配置分区数量。您需要同时考虑自动配置和用户控制配置两种方式。自动配置可基于输入文件大小和输入记录数量等参数进行。
4.1 分区方法 选择分区方法必须根据具体情况逐一进行。以下列表描述了一些可能的分区方法:
1. 固定且均匀的记录集拆分
这涉及将输入记录集分割为偶数个部分(例如,10个部分,每个部分恰好包含整个记录集的1/10)。然后,每个部分由批处理/提取应用程序的一个实例进行处理。
要使用这种方法,需要进行预处理以分割记录集。分割的结果是一个下限和上限放置编号,您可以将此作为批处理/提取应用程序的输入,以将其处理限制在仅属于其部分的范围。
预处理可能带来较大的开销,因为它需要计算并确定记录集每个部分的范围。
2. 按关键列拆分
这涉及到根据一个关键列(例如位置代码)来拆分输入记录集,并将每个关键值的数据分配给一个批次实例。为实现此目的,列值可以:
-
通过分区表分配给批处理实例(本节稍后描述)。
-
通过值的部分范围分配给批处理实例(例如 0000-0999、1000-1999 等)。
在选项1下,新增数值意味着需要手动重新配置批处理或提取过程,以确保新数值被添加到特定实例中。
在选项2下,这确保了所有值都能被批处理作业的实例覆盖。然而,一个实例处理的数据量取决于列值的分布情况(例如,0000-0999范围内可能有大量位置,而1000-1999范围内可能很少)。在此选项下,数据范围的设计应充分考虑分区策略。
在这两种方案下,都无法实现记录在批处理实例间的理想均匀分配。同时,批处理实例的数量也无法进行动态配置。
3. 按视图拆分
这种方法基本上是通过一个关键列在数据库层面进行拆分。它涉及将记录集分解为多个视图。这些视图由批处理应用程序的每个实例在处理过程中使用。拆分是通过对数据进行分组来完成的。
启用此选项后,每个批处理应用程序实例都需要配置为访问特定视图(而非主表)。此外,当新增数据值时,必须将新数据组纳入视图中。由于实例数量的变化会导致视图的变更,因此不具备动态配置能力。
4. 添加处理指示器
这涉及到向输入表中添加一个新列,该列用作指示器。作为预处理步骤,所有指示器都被标记为未处理。在批处理应用程序的记录获取阶段,读取记录的条件是单个记录被标记为未处理,并且一旦读取(带锁),它就被标记为正在处理中。当该记录完成时,指示器将更新为完成或错误。您可以启动批处理应用程序的多个实例而无需更改,因为额外的列确保每条记录仅被处理一次。
启用此选项后,表上的 I/O 操作会动态增加。对于更新批处理应用程序而言,这种影响会有所减轻,因为无论如何都需要执行写入操作。
5. 将表提取为平面文件
该方法涉及将表提取为平面文件。随后,该文件可被分割为多个片段,并用作批处理实例的输入。
启用此选项后,将表提取到文件并进行分割的额外开销可能会抵消多分区带来的优势。通过修改文件分割脚本,可以实现动态配置。
6. 哈希列的使用
该方案涉及向用于检索驱动记录的数据表中添加一个哈希列(键或索引)。此哈希列包含一个指示符,用于确定哪个批处理应用程序实例处理该特定行。例如,如果有三个批处理实例需要启动,指示符 'A' 标记由实例 1 处理的行,指示符 'B' 标记由实例 2 处理的行,指示符 'C' 标记由实例 3 处理的行。
随后,用于检索记录的过程将添加一个额外的 WHERE 子句,以选择由特定指示符标记的所有行。在此表中插入数据时,将涉及添加标记字段,该字段将默认设置为其中一个实例(例如 'A')。
一个简单的批处理应用程序将用于更新指标,例如在不同实例之间重新分配负载。当添加了足够数量的新行后,可以运行此批处理(除批处理窗口外的任何时间)以将新行重新分配到其他实例。
批处理应用程序的额外实例仅需运行批处理应用程序(如前文所述),即可将指标重新分配以支持新的实例数量。
4.2 数据库与应用程序设计原则
支持多分区应用程序的架构,若其运行于分区数据库表并采用键列方法,则应包含一个用于存储分区参数的中央分区存储库。这提供了灵活性并确保了可维护性。该存储库通常由一个单一的表组成,称为分区表。
分区表中存储的信息是静态的,通常应由数据库管理员(DBA)进行维护。该表应为每个多分区应用程序的每个分区包含一行信息。表中应包含以下列:程序ID代码、分区编号(分区的逻辑ID)、该分区数据库键列的低值以及该分区数据库键列的高值。
在程序启动时,程序 id 和分区号应由架构(具体来说,由控制处理任务)传递给应用程序。如果采用键列方法,这些变量将用于读取分区表,以确定应用程序要处理的数据范围。此外,在整个处理过程中必须使用分区号来:
-
添加到输出文件或数据库更新中,以确保合并过程正常工作。
-
将正常处理报告给批处理日志,并将任何错误报告给架构错误处理程序。
4.3 最小化死锁
当应用程序并行运行或分区时,可能会出现数据库资源争用和死锁。作为数据库设计的一部分,数据库设计团队必须尽可能消除潜在的争用情况,这一点至关重要。
此外,开发人员必须确保数据库索引表的设计考虑到死锁预防和性能。
死锁或热点问题通常出现在管理表或架构表中,例如日志表、控制表和锁表。这些影响也应予以考虑。真实的压力测试对于识别架构中可能存在的瓶颈至关重要。
为了最小化冲突对数据的影响,架构应在连接数据库或遇到死锁时提供服务(例如等待重试间隔)。这意味着需要内置一种机制来响应特定的数据库返回码,而不是立即发出错误,而是等待预定时间后重试数据库操作。
4.4 参数传递与验证
分区架构对应用开发者而言应相对透明。该架构应执行所有与在分区模式下运行应用相关的任务,包括:
-
在应用程序启动前检索分区参数。
-
在应用程序启动前验证分区参数。
-
在启动时将参数传递给应用程序。
验证应包括检查以确保:
-
应用程序拥有足够的分区来覆盖整个数据范围。
-
分区之间没有间隙。
如果数据库进行了分区,可能需要额外的验证来确保单个分区不会跨越数据库分区。
此外,架构设计还需考虑分区合并的问题。关键问题包括:
-
是否必须所有分区都完成后才能进入下一个作业步骤?
-
如果其中一个分区中止了会怎样?