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