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