Spring Batch 架构

Spring Batch 在设计时考虑了可扩展性和多样化的最终用户群。下图展示了支持最终用户开发者可扩展性和易用性的分层架构。

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

这个分层架构突出了三个主要的顶层组件:应用(Application)、核心(Core)和基础设施(Infrastructure)。应用层包含所有批处理 Job 和开发者使用 Spring Batch 编写的自定义代码。批处理核心层(Batch Core)包含启动和控制批处理 Job 所需的核心运行时类。它包括 JobLauncherJobStep 的实现。应用层和核心层都构建在一个通用的基础设施之上。这个基础设施包含通用的 Reader 和 Writer 以及服务(例如 RetryTemplate),这些服务既被应用开发者使用(例如 ItemReaderItemWriter),也被核心框架本身使用(例如 Retry,它是一个独立的库)。

通用批处理原则和指南

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

  • 记住,批处理架构通常会影响在线架构,反之亦然。设计时应同时考虑两种架构和环境,尽可能使用通用构建块。

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

  • 将数据的处理和存储物理上保持靠近(换句话说,将数据保留在处理发生的地方)。

  • 最小化系统资源使用,尤其是 I/O。尽可能在内存中执行更多操作。

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

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

    • 在同一事务中已读取过的数据在后续处理中再次读取。

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

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

  • 在一次批处理运行中不要重复做同样的事情。例如,如果您需要数据汇总用于报告,您应该(如果可能)在数据首次处理时递增存储的总计,这样您的报告应用就不必重新处理相同的数据。

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

  • 对于数据完整性,始终做最坏的打算。插入足够的检查和记录验证以维护数据完整性。

  • 尽可能为内部验证实现校验和。例如,平面文件应包含一个尾部记录,说明文件中的总记录数和关键字段的聚合值。

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

  • 在大型批处理系统中,备份可能具有挑战性,尤其是当系统与在线应用同时运行且是 24x7 的时候。数据库备份通常在在线设计中得到很好的处理,但文件备份也应视为同样重要。如果系统依赖于平面文件,文件备份程序不仅应该到位和文档化,还应该定期进行测试。

批处理策略

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

  • 转换应用:对于由外部系统提供或生成给外部系统的每种文件类型,必须创建一个转换应用,将提供的事务记录转换为处理所需的标准格式。这种类型的批处理应用可以部分或完全由转换工具模块组成(参见基本批处理服务)。

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

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

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

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

  • 输出/格式化应用:输出/格式化应用读取输入文件,根据标准格式重组此记录中的数据,并生成用于打印或传输到其他程序或系统的输出文件。

此外,对于无法使用上述构建块构建的业务逻辑,应提供一个基本的应用模板。

除了主要的构建块外,每个应用还可以使用一个或多个标准工具步骤,例如:

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

  • 分割:一个程序,读取一个输入文件,并根据字段值将每条记录写入多个输出文件之一。分割可以定制,或由参数驱动的标准系统工具完成。

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

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

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

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

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

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

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

  • 在离线模式下在批处理窗口期间进行正常处理。

  • 并发批处理或在线处理。

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

  • 分区(同时处理同一个 Job 的多个实例)。

  • 上述选项的组合。

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

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

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

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

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

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

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

  • 乐观锁定假定记录冲突的可能性较低。它通常意味着在每个同时被批处理和在线处理使用的数据库表中插入一个时间戳列。当应用获取一行进行处理时,它也获取时间戳。当应用随后尝试更新已处理的行时,更新在 WHERE 子句中使用原始时间戳。如果时间戳匹配,则更新数据和时间戳。如果时间戳不匹配,这表明在获取和尝试更新之间,另一个应用已更新了同一行。因此,更新无法执行。

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

这些模式不一定适用于批处理,但可能用于并发批处理和在线处理(例如在数据库不支持行级锁定或需要跨多个表锁定逻辑相关记录的情况下)。作为一般规则,乐观锁定更适合在线应用,而悲观锁定更适合批处理应用。无论何时使用逻辑锁定,访问受逻辑锁定保护的数据实体的所有应用都必须使用相同的方案。

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

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

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

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

4. 分区 使用分区可以并行运行大型批处理应用的多个版本。这样做的目的是为了减少处理长批处理 Job 所需的经过时间。可以成功分区的过程是那些输入文件可以分割或主数据库表可以分区,以便应用能够处理不同的数据集。

此外,已分区的过程必须设计为仅处理分配给它们的数据集。分区架构必须与数据库设计和数据库分区策略紧密相关。请注意,数据库分区不一定意味着数据库的物理分区(尽管在大多数情况下建议这样做)。下图说明了分区方法:

Figure 1.2: Partitioned Process
图 2. 分区处理

架构应足够灵活,以允许动态配置分区数量。应考虑自动和用户控制的配置。自动配置可以基于输入文件大小和输入记录数等参数。

4.1 分区方法 分区方法的选择必须根据具体情况进行。以下列表描述了一些可能的分区方法:

1. 记录集的固定均匀分割

这涉及将输入记录集分成偶数个部分(例如 10 个,其中每个部分恰好占整个记录集的 1/10)。然后,每个部分由批处理/提取应用的一个实例处理。

要使用此方法,需要进行预处理以分割记录集。分割的结果是可以使用较低和较高位置边界作为批处理/提取应用的输入,以将其处理限制在其自己的部分。

预处理可能产生很大的开销,因为它必须计算和确定记录集每个部分的边界。

2. 按键列分割

这涉及按键列(如位置代码)分割输入记录集,并将每个键的数据分配给一个批处理实例。为此,可以将列值:

  • 通过分区表分配给批处理实例(本节后面描述)。

  • 通过值的一部分分配给批处理实例(如 0000-0999、1000-1999 等)。

在选项 1 下,添加新值意味着手动重新配置批处理或提取,以确保新值添加到特定实例中。

在选项 2 下,这确保了所有值都被批处理 Job 的一个实例覆盖。但是,一个实例处理的值数量取决于列值的分布(可能在 0000-0999 范围内有大量位置,而在 1000-1999 范围内很少)。在此选项下,应在设计时考虑分区来设计数据范围。

在这两种选项下,无法实现记录对批处理实例的最佳均匀分布。无法动态配置使用的批处理实例数量。

3. 按视图分割

此方法基本上是按键列分割,但在数据库级别进行。它涉及将记录集分割成视图。这些视图由批处理应用在处理期间的每个实例使用。分割是通过对数据进行分组来完成的。

使用此选项,批处理应用的每个实例都必须配置为访问特定视图(而不是主表)。此外,随着新数据值的添加,必须将这组新数据包含到视图中。由于实例数量的改变会导致视图的改变,因此没有动态配置能力。

4. 添加处理指示器

这涉及在输入表中添加一个新列,该列充当指示器。作为预处理步骤,所有指示器都被标记为未处理。在批处理应用的记录获取阶段,在单个记录被标记为未处理的条件下读取记录,一旦读取(带有锁),就将其标记为正在处理。当该记录完成时,指示器会更新为完成或错误。您可以启动批处理应用的许多实例而无需更改,因为附加列确保记录只处理一次。

使用此选项,表上的 I/O 会动态增加。对于更新批处理应用,由于无论如何都会发生写入,因此这种影响会减小。

5. 将表提取到平面文件

此方法涉及将表提取到平面文件中。然后可以将此文件分割成多个段,并用作批处理实例的输入。

使用此选项,将表提取到文件并分割它的额外开销可能会抵消多分区的效果。通过更改文件分割脚本可以实现动态配置。

6. 使用哈希列

此方案涉及在用于检索驱动记录的数据库表中添加一个哈希列(键或索引)。此哈希列带有一个指示器,用于确定批处理应用的哪个实例处理该特定行。例如,如果要启动三个批处理实例,标记“A”表示行由实例 1 处理,标记“B”表示行由实例 2 处理,标记“C”表示行由实例 3 处理。

用于检索记录的过程将包含一个额外的 WHERE 子句,以选择由特定指示器标记的所有行。在此表中插入操作将涉及添加标记字段,该字段将默认设置为其中一个实例(例如“A”)。

一个简单的批处理应用将用于更新指示器,例如在不同实例之间重新分配负载。当添加了足够多的新行时,可以运行此批处理(随时运行,批处理窗口除外)以将新行重新分配给其他实例。

额外的批处理应用实例仅需要运行批处理应用(如前几段所述)即可重新分配指示器以适应新的实例数量。

4.2 数据库和应用设计原则

支持针对已分区数据库表运行并使用键列方法的多分区应用的架构应包含一个中央分区仓库,用于存储分区参数。这提供了灵活性并确保了可维护性。该仓库通常由一个表组成,称为分区表。

分区表中存储的信息是静态的,通常应由 DBA 维护。该表应包含多分区应用的每个分区的一行信息。该表应包含程序 ID 代码、分区编号(分区的逻辑 ID)、此分区的数据库键列的低值以及此分区的数据库键列的高值的列。

在程序启动时,程序 id 和分区编号应从架构传递给应用(特别是从控制处理 tasklet)。如果使用键列方法,这些变量用于读取分区表以确定应用要处理的数据范围。此外,在整个处理过程中必须使用分区编号:

  • 添加到输出文件或数据库更新中,以便合并过程正常工作。

  • 向批处理日志报告正常处理情况,向架构错误处理器报告任何错误。

4.3 最大程度减少死锁

当应用并行运行或进行分区时,可能会发生数据库资源争用和死锁。作为数据库设计的一部分,数据库设计团队必须最大程度地消除潜在的争用情况,这一点至关重要。

此外,开发人员必须确保数据库索引表的设计考虑了死锁预防和性能。

死锁或热点通常发生在管理或架构表中,如日志表、控制表和锁表。这些影响也应考虑在内。现实的压力测试对于识别架构中可能的瓶颈至关重要。

为了最大程度地减少冲突对数据的影响,架构应在连接数据库或遇到死锁时提供服务(例如等待和重试间隔)。这意味着内置一种机制来对某些数据库返回代码作出反应,并且不立即发出错误,而是等待预定时间并重试数据库操作。

4.4 参数传递和验证

分区架构应相对透明于应用开发者。架构应执行与在分区模式下运行应用相关的所有任务,包括:

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

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

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

验证应包括检查以确保:

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

  • 分区之间没有间隔。

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

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

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

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