Spring Batch 架构
Spring Batch 的设计考虑了可扩展性和多样化的最终用户群体。下图展示了支持最终用户开发人员的可扩展性和易用性的分层架构。
这种分层架构突出了三个主要的高级组件:应用、核心和基础设施。应用包含所有批处理作业和开发人员使用 Spring Batch 编写的自定义代码。批处理核心包含启动和控制批处理作业所需的核心运行时类。它包括 JobOperator、Job 和 Step 的实现。应用和核心都建立在公共基础设施之上。该基础设施包含通用的读写器和服务(例如 RetryTemplate),这些都被应用开发人员(读写器,例如 ItemReader 和 ItemWriter)和核心框架本身(重试,它是一个独立的库)使用。
通用批处理原则和指南
在构建批处理解决方案时,应考虑以下关键原则、指南和一般注意事项。
-
请记住,批处理架构通常会影响在线架构,反之亦然。在设计时,要同时考虑两种架构和环境,并尽可能使用通用的构建块。
-
尽可能简化,避免在单个批处理应用程序中构建复杂的逻辑结构。
-
保持数据处理和存储在物理上彼此靠近(换句话说,将数据保存在处理发生的地方)。
-
最小化系统资源使用,特别是 I/O。在内部内存中执行尽可能多的操作。
-
审查应用程序 I/O(分析 SQL 语句),以确保避免不必要的物理 I/O。特别是,需要查找以下四个常见缺陷:
-
每次事务都读取数据,而数据可以一次性读取并缓存或保存在工作存储中。
-
在同一事务中,数据已被读取,但又为该事务重新读取数据。
-
导致不必要的表或索引扫描。
-
在 SQL 语句的
WHERE子句中未指定键值。
-
-
在批处理运行中不要重复做事情。例如,如果出于报告目的需要数据汇总,您应该(如果可能)在数据初次处理时增加存储的总计,这样您的报告应用程序就不必重新处理相同的数据。
-
在批处理应用程序开始时分配足够的内存,以避免在处理过程中耗时的重新分配。
-
始终假设数据完整性最差的情况。插入足够的检查和记录验证以维护数据完整性。
-
尽可能实施校验和进行内部验证。例如,平面文件应包含一个尾记录,说明文件中的总记录数和关键字段的聚合值。
-
尽早在一个类似于生产的环境中使用真实数据量进行压力测试的规划和执行。
-
在大型批处理系统中,备份可能具有挑战性,特别是如果系统在 24/7 的基础上与在线应用程序并发运行。数据库备份通常在在线设计中得到很好的处理,但文件备份也应被视为同样重要。如果系统依赖于平面文件,文件备份程序不仅应到位并有文档记录,还应定期进行测试。
批处理策略
为了帮助设计和实现批处理系统,应以样本结构图和代码模板的形式向设计人员和程序员提供基本的批处理应用程序构建块和模式。在开始设计批处理作业时,业务逻辑应分解为一系列步骤,这些步骤可以使用以下标准构建块来实现:
-
转换应用程序: 对于外部系统提供或生成的每种类型的文件,都必须创建一个转换应用程序,将提供的事务记录转换为处理所需的标准格式。这种类型的批处理应用程序可以部分或完全由转换实用程序模块组成(参见基本批处理服务)。
-
验证应用程序: 验证应用程序确保所有输入和输出记录都是正确且一致的。验证通常基于文件头和尾、校验和验证算法以及记录级交叉检查。
-
提取应用程序: 提取应用程序从数据库或输入文件读取一组记录,根据预定义规则选择记录,并将记录写入输出文件。
-
提取/更新应用程序: 提取/更新应用程序从数据库或输入文件读取记录,并根据每个输入记录中找到的数据对数据库或输出文件进行更改。
-
处理和更新应用程序: 处理和更新应用程序对来自提取或验证应用程序的输入事务执行处理。处理通常涉及读取数据库以获取处理所需的数据,可能更新数据库并创建用于输出处理的记录。
-
输出/格式应用程序: 输出/格式应用程序读取输入文件,根据标准格式重构此记录中的数据,并生成用于打印或传输到另一个程序或系统的输出文件。
此外,对于无法使用前面提到的构建块构建的业务逻辑,应提供一个基本的应用程序外壳。
除了主要的构建块,每个应用程序还可以使用一个或多个标准实用程序步骤,例如:
-
排序:一个程序,读取输入文件并生成一个输出文件,其中记录已根据记录中的排序键字段重新排序。排序通常由标准系统实用程序执行。
-
拆分:一个程序,读取单个输入文件并根据字段值将每条记录写入多个输出文件之一。拆分可以定制或由参数驱动的标准系统实用程序执行。
-
合并:一个程序,从多个输入文件读取记录,并生成一个包含输入文件组合数据的输出文件。合并可以定制或由参数驱动的标准系统实用程序执行。
批处理应用程序还可以根据其输入源进行分类:
-
数据库驱动的应用程序由从数据库检索到的行或值驱动。
-
文件驱动的应用程序由从文件检索到的记录或值驱动。
-
消息驱动的应用程序由从消息队列检索到的消息驱动。
任何批处理系统的基础都是处理策略。影响策略选择的因素包括:估计的批处理系统吞吐量、与在线系统或其他批处理系统的并发性、可用的批处理窗口。(请注意,随着越来越多的企业希望 24x7 运行,清晰的批处理窗口正在消失)。
批处理的典型处理选项(按实现复杂性递增的顺序)有:
-
在批处理窗口期间以离线模式进行正常处理。
-
并发批处理或在线处理。
-
同时并行处理许多不同的批处理运行或作业。
-
分区(同时处理同一作业的多个实例)。
-
前述选项的组合。
其中一些或所有选项可能由商业调度器支持。
本节的其余部分将更详细地讨论这些处理选项。请注意,根据经验法则,批处理过程采用的提交和锁定策略取决于所执行的处理类型,并且在线锁定策略也应使用相同的原则。因此,在设计整体架构时,批处理架构不能仅仅是事后考虑。
锁定策略可以是仅使用正常的数据库锁,也可以在架构中实现一个附加的自定义锁定服务。该锁定服务将跟踪数据库锁定(例如,通过将必要的信息存储在专用的数据库表中),并授予或拒绝请求数据库操作的应用程序程序的权限。此架构还可以实现重试逻辑,以避免在发生锁定情况时中止批处理作业。
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)、此分区的数据库键列的低值和此分区的数据库键列的高值的列。
程序启动时,程序 id 和分区号应从架构(特别是从控制处理任务)传递给应用程序。如果使用键列方法,这些变量用于读取分区表以确定应用程序要处理的数据范围。此外,分区号必须在整个处理过程中使用,以:
-
添加到输出文件或数据库更新中,以便合并过程正常工作。
-
向批处理日志报告正常处理,向架构错误处理程序报告任何错误。
4.3 最小化死锁
当应用程序并行运行或分区时,可能会发生数据库资源争用和死锁。至关重要的是,数据库设计团队应尽可能消除潜在的争用情况,作为数据库设计的一部分。
此外,开发人员必须确保数据库索引表的设计考虑了死锁预防和性能。
死锁或热点经常出现在管理或架构表中,例如日志表、控制表和锁定表。这些问题的影响也应考虑在内。真实的压力测试对于识别架构中可能的瓶颈至关重要。
为了最大限度地减少冲突对数据的影响,架构应在连接到数据库或遇到死锁时提供服务(例如等待和重试间隔)。这意味着内置机制可以对某些数据库返回代码做出反应,并且不是立即发出错误,而是等待预定的时间量并重试数据库操作。
4.4 参数传递与验证
分区架构对应用程序开发人员来说应该相对透明。架构应执行与分区模式下运行应用程序相关的所有任务,包括:
-
在应用程序启动前检索分区参数。
-
在应用程序启动前验证分区参数。
-
在启动时将参数传递给应用程序。
验证应包括检查以确保:
-
应用程序有足够的分区来覆盖整个数据范围。
-
分区之间没有间隙。
如果数据库已分区,则可能需要进行一些额外的验证,以确保单个分区不会跨越数据库分区。
此外,架构还应考虑分区的整合。关键问题包括:
-
是否所有分区都必须在进入下一个作业步骤之前完成?
-
如果其中一个分区中止,会发生什么?