Spring Batch 架构

Spring Batch 的设计考虑了可扩展性和各种最终用户。下图显示了支持可扩展性和易用性的分层架构,方便最终用户开发者使用。

Figure 1.1: Spring Batch Layered Architecture
图 1. Spring Batch 分层架构

此分层架构突出了三个主要的高级组件:应用程序、核心和基础设施。应用程序包含所有批处理作业和开发者使用 Spring Batch 编写的自定义代码。批处理核心包含启动和控制批处理作业所需的核心运行时类。它包括 JobLauncherJobStep 的实现。应用程序和核心都构建在通用基础设施之上。此基础设施包含常用的读取器和写入器以及服务(例如 RetryTemplate),应用程序开发者(读取器和写入器,例如 ItemReaderItemWriter)和核心框架本身(重试,它本身就是一个库)都使用这些基础设施。

通用批处理原则和指南

在构建批处理解决方案时,应考虑以下关键原则、指南和一般注意事项。

  • 请记住,批处理架构通常会影响在线架构,反之亦然。通过尽可能使用通用构建块,将两种架构和环境都考虑在内。

  • 尽可能简化,避免在单个批处理应用程序中构建复杂的逻辑结构。

  • 使数据处理和存储在物理上尽可能靠近(换句话说,将数据保存在处理发生的位置)。

  • 最大程度地减少系统资源使用,尤其是 I/O。在内部内存中执行尽可能多的操作。

  • 审查应用程序 I/O(分析 SQL 语句),以确保避免不必要的物理 I/O。特别是,需要查找以下四种常见缺陷

    • 每次事务都读取数据,而数据可以读取一次并缓存或保存在工作存储中。

    • 在同一事务中较早读取数据的同一事务中重新读取数据。

    • 导致不必要的数据表或索引扫描。

    • 在 SQL 语句的 WHERE 子句中未指定键值。

  • 不要在批处理运行中执行两次操作。例如,如果您需要数据汇总以供报告目的,则应(如果可能)在初始处理数据时递增存储的总数,以便您的报告应用程序无需重新处理相同的数据。

  • 在批处理应用程序开始时分配足够的内存,以避免在处理过程中耗时的重新分配。

  • 始终假设数据完整性方面最糟糕的情况。插入足够检查和记录验证以维护数据完整性。

  • 在可能的情况下实现校验和以进行内部验证。例如,平面文件应具有一个尾部记录,其中包含文件中记录的总数以及键字段的聚合。

  • 尽早规划并在类似生产的环境中使用实际数据量执行压力测试。

  • 在大型批处理系统中,备份可能具有挑战性,尤其是在系统以 24/7 方式与在线应用程序同时运行的情况下。数据库备份通常在在线设计中得到妥善处理,但应将文件备份视为同样重要。如果系统依赖于平面文件,则文件备份程序不仅应到位并记录在案,还应定期测试。

批处理策略

为了帮助设计和实现批处理系统,应以示例结构图和代码外壳的形式向设计人员和程序员提供基本的批处理应用程序构建块和模式。在开始设计批处理作业时,业务逻辑应分解成一系列步骤,这些步骤可以使用以下标准构建块来实现

  • 转换应用程序:对于外部系统提供或为其生成的每种类型的文件,都必须创建一个转换应用程序,以将提供的交易记录转换为处理所需的标准格式。此类批处理应用程序可以部分或全部由转换实用程序模块组成(请参阅基本批处理服务)。

  • 验证应用程序:验证应用程序确保所有输入和输出记录都是正确且一致的。验证通常基于文件头和尾部、校验和和验证算法以及记录级交叉检查。

  • 提取应用程序:提取应用程序从数据库或输入文件中读取一组记录,根据预定义规则选择记录,并将记录写入输出文件。

  • 提取/更新应用程序:提取/更新应用程序从数据库或输入文件中读取记录,并根据每个输入记录中找到的数据对数据库或输出文件进行更改。

  • 处理和更新应用程序:处理和更新应用程序对来自提取或验证应用程序的输入事务执行处理。处理通常涉及读取数据库以获取处理所需的数据,可能更新数据库并创建记录以进行输出处理。

  • 输出/格式应用程序:输出/格式应用程序读取输入文件,根据标准格式重构此记录中的数据,并生成输出文件以进行打印或传输到另一个程序或系统。

此外,还应提供一个基本的应用程序框架,用于构建无法使用前面提到的构建块构建的业务逻辑。

除了主要的构建块之外,每个应用程序可以使用一个或多个标准实用程序步骤,例如

  • 排序:一个程序,读取输入文件并生成一个输出文件,其中记录已根据记录中的排序键字段重新排序。排序通常由标准系统实用程序执行。

  • 拆分:一个程序,读取单个输入文件并将每个记录写入多个输出文件中的一个,具体取决于字段值。拆分可以定制或由参数驱动的标准系统实用程序执行。

  • 合并:一个程序,读取来自多个输入文件的记录并生成一个输出文件,其中包含来自输入文件的数据。合并可以定制或由参数驱动的标准系统实用程序执行。

批处理应用程序还可以根据其输入源进行分类

  • 数据库驱动的应用程序由从数据库检索的行或值驱动。

  • 文件驱动的应用程序由从文件检索的记录或值驱动。

  • 消息驱动的应用程序由从消息队列检索的消息驱动。

任何批处理系统的基础都是处理策略。影响策略选择的一些因素包括:估计的批处理系统容量、与联机系统或其他批处理系统的并发性、可用的批处理窗口。(请注意,随着越来越多的企业希望能够 24x7 全天候运行,清晰的批处理窗口正在消失)。

批处理的典型处理选项(按实现复杂性递增顺序)

  • 在脱机模式下的批处理窗口期间进行正常处理。

  • 并发批处理或联机处理。

  • 同时并行处理许多不同的批处理运行或作业。

  • 分区(同时处理同一作业的许多实例)。

  • 上述选项的组合。

商业调度程序可能支持其中的一些或所有选项。

本节的其余部分将更详细地讨论这些处理选项。请注意,根据经验法则,批处理过程采用的提交和锁定策略取决于所执行的处理类型,并且联机锁定策略也应使用相同的原则。因此,在设计整体架构时,批处理架构不能仅仅是事后才考虑的。

锁定策略可以使用普通的数据库锁,也可以在架构中实现额外的自定义锁定服务。锁定服务将跟踪数据库锁定(例如,通过将必要的信息存储在专用的数据库表中)并向请求数据库操作的应用程序授予或拒绝权限。此架构还可以实现重试逻辑,以避免在出现锁定情况时中止批处理作业。

1. 批处理窗口中的正常处理 对于在单独的批处理窗口中运行的简单批处理过程,其中正在更新的数据不需要联机用户或其他批处理过程,并发不是问题,并且可以在批处理运行结束时执行一次提交。

在大多数情况下,更强大的方法更合适。请记住,批处理系统的复杂性和处理的数据量随着时间的推移而增长。如果未实施任何锁定策略,并且系统仍然依赖于单个提交点,则修改批处理程序可能会很痛苦。因此,即使对于最简单的批处理系统,也要考虑重启恢复选项的提交逻辑需求以及本节后面描述的更复杂情况的相关信息。

2. 并发批处理或联机处理 处理可以由联机用户同时更新的数据的批处理应用程序不应锁定联机用户可能需要超过几秒钟的任何数据(数据库或文件中)。此外,应在每隔几笔事务后将更新提交到数据库。这样做可以最大程度地减少其他进程无法访问的数据部分以及数据不可用经过的时间。

最小化物理锁定的另一种选择是使用乐观锁定模式或悲观锁定模式实现逻辑行级锁定。

  • 乐观锁定假设记录争用可能性较低。它通常意味着在每个数据库表中插入一个时间戳列,该列由批处理和联机处理同时使用。当应用程序获取一行以进行处理时,它还会获取时间戳。然后,当应用程序尝试更新已处理的行时,更新会在WHERE子句中使用原始时间戳。如果时间戳匹配,则更新数据和时间戳。如果时间戳不匹配,则表示另一个应用程序在获取和更新尝试之间更新了同一行。因此,无法执行更新。

  • 悲观锁定是任何假设记录争用可能性很高并因此需要在检索时获取物理锁或逻辑锁的锁定策略。一种类型的悲观逻辑锁定在数据库表中使用专用的锁定列。当应用程序检索要更新的行时,它会在锁定列中设置一个标志。设置此标志后,尝试检索同一行的其他应用程序将逻辑上失败。当设置标志的应用程序更新行时,它还会清除标志,从而使其他应用程序能够检索该行。请注意,还必须在初始获取和设置标志之间维护数据的完整性,例如,通过使用数据库锁(例如SELECT FOR UPDATE)。还要注意,此方法与物理锁定具有相同的缺点,只是构建超时机制以在用户在记录被锁定期间去吃午饭时释放锁相对容易一些。

这些模式不一定适合批处理,但可以用于并发批处理和联机处理(例如,在数据库不支持行级锁定的情况下)。一般来说,乐观锁定更适合联机应用程序,而悲观锁定更适合批处理应用程序。无论何时使用逻辑锁定,都必须对访问受逻辑锁保护的数据实体的所有应用程序使用相同的方案。

请注意,这两种解决方案都只解决了锁定单个记录的问题。通常,我们可能需要锁定逻辑相关的记录组。对于物理锁,您必须非常小心地管理它们以避免潜在的死锁。对于逻辑锁,通常最好构建一个逻辑锁管理器,该管理器了解您想要保护的逻辑记录组,并且可以确保锁是一致且无死锁的。此逻辑锁管理器通常使用自己的表进行锁管理、争用报告、超时机制和其他问题。

3. 并行处理 并行处理允许多个批处理运行或作业并行运行,以最大程度地减少总的批处理时间。只要作业不共享相同的文件、数据库表或索引空间,这就不成问题。如果它们共享,则应通过使用分区数据来实现此服务。另一种选择是构建一个架构模块,用于使用控制表维护依赖关系。控制表应包含每个共享资源的行以及它是否正在被应用程序使用。然后,并行作业中的批处理架构或应用程序将从该表中检索信息以确定它是否可以访问其所需的资源。

如果数据访问不是问题,则可以通过使用附加线程并行处理来实现并行处理。在大型机环境中,传统上使用并行作业类来确保所有进程都有足够的 CPU 时间。无论如何,解决方案必须足够健壮,以确保所有正在运行的进程都有时间片。

并行处理的其他关键问题包括负载平衡和通用系统资源(如文件、数据库缓冲池等)的可用性。此外,请注意,控制表本身很容易成为关键资源。

4. 分区 使用分区允许大型批处理应用程序的多个版本并发运行。这样做的目的是减少处理长时间批处理作业所需的时间。可以成功分区的进程是那些可以拆分输入文件或对主数据库表进行分区以使应用程序针对不同的数据集运行的进程。

此外,分区的进程必须设计为仅处理其分配的数据集。分区架构必须与数据库设计和数据库分区策略紧密相关。请注意,数据库分区并不一定意味着数据库的物理分区(尽管在大多数情况下,这是可取的)。下图说明了分区方法

Figure 1.2: Partitioned Process
图 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 参数传递和验证

分区架构对于应用程序开发人员来说应该相对透明。架构应执行与以分区模式运行应用程序相关的所有任务,包括

  • 在应用程序启动之前检索分区参数。

  • 在应用程序启动之前验证分区参数。

  • 在启动时将参数传递给应用程序。

验证应包括检查以确保

  • 应用程序有足够的分区来覆盖整个数据范围。

  • 分区之间没有间隙。

如果数据库已分区,则可能需要进行一些额外的验证以确保单个分区不跨越数据库分区。

此外,架构应考虑分区的合并。关键问题包括

  • 在进入下一作业步骤之前,是否必须完成所有分区?

  • 如果其中一个分区中止会发生什么?