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