# Spring 批量介绍

# Spring 批介绍

Enterprise 领域中的许多应用程序需要大容量处理,以在关键任务环境中执行业务操作。这些业务包括:

  • 对大量信息进行自动化、复杂的处理,在没有用户交互的情况下进行最有效的处理。这些操作通常包括基于时间的事件(例如月末计算、通知或通信)。

  • 在非常大的数据集中重复处理的复杂业务规则的周期性应用(例如,保险利益的确定或费率调整)。

  • 从内部和外部系统接收的信息的集成,这些信息通常需要以事务的方式进行格式化、验证和处理,并将其集成到记录系统中。批处理用于企业每天处理数十亿笔交易。

Spring 批处理是一种轻量级的、全面的批处理框架,旨在使开发对于 Enterprise 系统的日常操作至关重要的健壮的批处理应用程序成为可能。 Spring 批处理构建在人们所期望的 Spring 框架的特征(生产力、基于 POJO 的开发方法和普遍的易用性)的基础上,同时使开发人员在必要时更容易访问和利用更先进的 Enterprise 服务。 Spring 批处理不是一种调度框架。在商业和开源领域都有许多很好的 Enterprise 调度器(例如 Quartz、Tivoli、Control-M 等)。它的目的是与调度器一起工作,而不是取代调度器。

Spring 批处理提供了可重用的功能,这些功能在处理大量记录中是必不可少的,包括日志记录/跟踪、事务管理、作业处理统计、作业重新启动、跳过和资源管理。它还提供了更先进的技术服务和功能,通过优化和分区技术实现了非常大的批量和高性能的批处理作业。 Spring 批处理既可以用于简单的用例(例如将文件读入数据库或运行存储过程),也可以用于复杂的、大容量的用例(例如在数据库之间移动大容量的数据,对其进行转换,等等)。大批量批处理作业可以以高度可伸缩的方式利用框架来处理大量信息。

# 背景

虽然开放源码软件项目和相关社区更多地关注基于 Web 和基于微服务的架构框架,但明显缺乏对可重用架构框架的关注,以满足基于 Java 的批处理需求,尽管仍然需要在 EnterpriseIT 环境中处理此类处理。缺乏标准的、可重用的批处理体系结构导致了在客户 EnterpriseIT 功能中开发的许多一次性内部解决方案的激增。

SpringSource(现为 Pivotal)和埃森哲合作改变了这种状况。埃森哲在实现批处理架构方面的行业和技术经验、SpringSource 的技术经验深度以及 Spring 经过验证的编程模型,共同形成了一种自然而强大的合作关系,以创建高质量的、与市场相关的软件,旨在填补 EnterpriseJava 领域的一个重要空白。这两家公司都与许多正在通过开发基于 Spring 的批处理架构解决方案来解决类似问题的客户合作。这提供了一些有用的附加细节和现实生活中的约束,有助于确保该解决方案可以应用于客户提出的现实世界中的问题。

埃森哲为 Spring 批处理项目贡献了以前专有的批处理架构框架,以及用于驱动支持、增强和现有功能集的提交者资源。埃森哲的贡献是基于数十年来在过去几代平台上构建批处理架构的经验:COBOL/大型机、C++/UNIX,以及现在的 Java/Anywhere。

埃森哲和 SpringSource 之间的合作旨在促进软件处理方法、框架和工具的标准化,这些方法、框架和工具可以由 Enterprise 用户在创建批处理应用程序时始终如一地加以利用。希望为其 EnterpriseIT 环境提供标准的、经过验证的解决方案的公司和政府机构可以从 Spring 批处理中受益。

# 使用场景

一个典型的批处理程序通常是:

  • 从数据库、文件或队列中读取大量记录。

  • 以某种方式处理数据。

  • 以修改后的形式写回数据。

Spring 批处理自动化了这种基本的批处理迭代,提供了将类似的事务作为一个集合来处理的能力,通常是在没有任何用户交互的离线环境中。批处理作业是大多数 IT 项目的一部分, Spring 批处理是唯一提供健壮的、Enterprise 规模的解决方案的开源框架。

业务场景

  • 定期提交批处理过程

  • 并发批处理:作业的并行处理

  • 分阶段的、Enterprise 的消息驱动处理

  • 大规模并行批处理

  • 失败后手动或计划重启

  • 依赖步骤的顺序处理(扩展到工作流驱动的批处理)

  • 部分处理:跳过记录(例如,在回滚时)

  • 整批事务,用于小批量或现有存储过程/脚本的情况

技术目标

  • 批处理开发人员使用 Spring 编程模型:专注于业务逻辑,让框架处理基础架构。

  • 在基础结构、批处理执行环境和批处理应用程序之间明确分离关注点。

  • 提供公共的、核心的执行服务,作为所有项目都可以实现的接口。

  • 提供可以“开箱即用”地使用的核心执行接口的简单和默认实现。

  • 通过在所有层中利用 Spring 框架,易于配置、自定义和扩展服务。

  • 所有现有的核心服务都应该易于替换或扩展,而不会对基础设施层产生任何影响。

  • 提供一个简单的部署模型,其体系结构 JAR 与应用程序完全分开,使用 Maven 构建。

# Spring 批处理体系结构

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

Figure 1.1: Spring Batch Layered Architecture

图 1. Spring 批处理分层架构

这个分层架构突出了三个主要的高级组件:应用程序、核心和基础架构。该应用程序包含由开发人员使用 Spring 批处理编写的所有批处理作业和自定义代码。批处理核心包含启动和控制批处理作业所必需的核心运行时类。它包括JobLauncherJobStep的实现。应用程序和核心都是建立在一个共同的基础架构之上的。这个基础结构包含常见的读取器、编写器和服务(例如RetryTemplate),应用程序开发人员(读取器和编写器,例如ItemReaderItemWriter)和核心框架本身(Retry,这是它自己的库)都使用它们。

# 一般批处理原则和准则

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

  • 请记住,批处理架构通常会影响在线架构,反之亦然。在设计时,尽可能使用通用的构建模块,同时考虑到体系结构和环境。

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

  • 保持数据的处理和存储在物理上紧密地联系在一起(换句话说,将数据保存在发生处理的位置)。

  • 尽量减少系统资源的使用,特别是 I/O。在内存中执行尽可能多的操作。

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

    • 当数据可以读取一次并缓存或保存在工作存储器中时,为每个事务读取数据。

    • 重读事务的数据,而该事务的数据是在同一事务中较早读取的。

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

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

  • 不要在一次批处理中做两次事情。例如,如果出于报告目的需要进行数据汇总,则应该(如果可能的话)在最初处理数据时增加存储总量,这样你的报告应用程序就不必重新处理相同的数据。

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

  • 对于数据 Integrity,总是假设最坏的情况。插入足够的检查和记录验证,以维护数据的 Integrity。

  • 在可能的情况下,为内部验证实现校验和。例如,平面文件应该有一个预告片记录,它告诉文件中记录的总数和关键字段的汇总。

  • 在具有实际数据量的类似于生产的环境中,尽早地计划和执行压力测试。

  • 在大批量系统中,备份可能是具有挑战性的,特别是如果系统在 24-7 的基础上与在线并发运行。数据库备份通常在联机设计中得到很好的处理,但是文件备份也应该被认为是同样重要的。如果系统依赖于平面文件,那么文件备份过程不仅应该到位并记录在案,还应该定期进行测试。

# 批处理策略

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

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

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

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

  • *提取/更新应用程序:*一种应用程序,它从数据库或输入文件中读取记录,并由每个输入记录中的数据驱动对数据库或输出文件进行更改。

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

  • *输出/格式应用程序:*读取输入文件、根据标准格式从该记录重组数据并产生输出文件以用于打印或传输到另一个程序或系统的应用程序。

此外,应该为不能使用前面提到的构建块构建的业务逻辑提供一个基本的应用程序 shell。

除了主要的构建块之外,每个应用程序可以使用一个或多个标准实用程序步骤,例如:

  • 排序:读取输入文件并产生输出文件的程序,其中记录已根据记录中的排序键域重新排序。排序通常由标准的系统实用程序执行。

  • 分割:一种程序,它读取单个输入文件,并根据字段的值将每条记录写入多个输出文件中的一个。分割可以由参数驱动的标准系统实用程序进行裁剪或执行。

  • 合并:一种程序,从多个输入文件中读取记录,并用输入文件的合并数据生成一个输出文件。合并可以由参数驱动的标准系统实用程序进行裁剪或执行。

批处理应用程序还可以按其输入源进行分类:

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

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

  • 消息驱动的应用程序是由从消息队列中检索到的消息驱动的。

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

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

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

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

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

  • 分区(在同一时间处理同一作业的多个实例)。

  • 上述选项的组合。

这些选项中的一部分或全部可能由商业调度程序支持。

下一节将更详细地讨论这些处理选项。重要的是要注意,根据经验,批处理过程采用的提交和锁定策略取决于所执行的处理的类型,并且在线锁定策略也应该使用相同的原则。因此,在设计整体架构时,批处理架构不能仅仅是一个事后的想法。

锁定策略可以是仅使用普通的数据库锁,或者在体系结构中实现额外的自定义锁定服务。锁定服务将跟踪数据库锁定(例如,通过将必要的信息存储在专用的 DB-table 中),并向请求 DB 操作的应用程序授予或拒绝权限。该体系结构还可以实现重试逻辑,以避免在锁定情况下中止批处理作业。

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

在大多数情况下,更稳健的方法更合适。请记住,批处理系统在复杂性和它们处理的数据量方面都有随着时间推移而增长的趋势。如果没有锁定策略,并且系统仍然依赖于一个提交点,那么修改批处理程序可能会很痛苦。因此,即使对于最简单的批处理系统,也要考虑重新启动-恢复选项的提交逻辑的需求,以及与本节后面描述的更复杂情况有关的信息。

2.并发批处理或在线处理处理可由联机用户同时更新的数据的批处理应用程序不应锁定联机用户可能需要超过几秒钟的任何数据(数据库或文件中的数据)。此外,在每几个事务结束时,都应该将更新提交给数据库。这将最小化其他进程不可用的数据部分和数据不可用的时间。

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

  • 乐观锁定假设记录争用的可能性较低。它通常意味着在每个数据库表中插入一个时间戳列,该数据库表由批处理和在线处理并发使用。当应用程序获取要处理的行时,它也会获取时间戳。当应用程序尝试更新已处理的行时,更新将使用 WHERE 子句中的原始时间戳。如果时间戳匹配,则数据和时间戳将被更新。如果时间戳不匹配,则表示另一个应用程序在获取和更新尝试之间更新了相同的行。因此,无法执行更新。

  • 悲观锁定是任何一种锁定策略,该策略假定存在记录争用的高可能性,因此需要在检索时获得物理或逻辑锁定。一种悲观逻辑锁使用数据库表中的专用锁列。当应用程序检索要更新的行时,它会在 Lock 列中设置一个标志。有了标志后,试图从逻辑上检索同一行的其他应用程序将失败。当设置标记的应用程序更新该行时,它也会清除标记,从而使其他应用程序能够检索该行。请注意,在初始获取和标志设置之间也必须保持数据的 Integrity,例如通过使用 DB 锁(例如SELECT FOR UPDATE)。还需要注意的是,这种方法与物理锁定有相同的缺点,只是在用户去吃午饭而记录被锁定的情况下,构建一个超时机制来释放锁,会更容易管理。

这些模式不一定适合批处理,但它们可能用于并发批处理和在线处理(例如在数据库不支持行级锁定的情况下)。作为一般规则,乐观锁定更适合于在线应用程序,而悲观锁定更适合批处理应用程序。每当使用逻辑锁时,必须对所有访问由逻辑锁保护的数据实体的应用程序使用相同的方案。

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

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

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

并行处理中的其他关键问题包括负载平衡和一般系统资源(如文件、数据库缓冲池等)的可用性。还要注意,控制表本身很容易成为关键资源。

4.划分使用分区允许多个版本的大批量应用程序同时运行。这样做的目的是减少处理长批处理作业所需的时间。可以成功分区的进程是那些可以分割输入文件和/或分区主数据库表以允许应用程序在不同的数据集上运行的进程。

此外,被分区的进程必须被设计为仅处理其分配的数据集。分区体系结构必须与数据库设计和数据库分区策略紧密联系在一起。请注意,数据库分区并不一定意味着数据库的物理分区,尽管在大多数情况下这是可取的。下图展示了分区方法:

图 1.2:分区过程

图 2.分区过程

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

4.1 划分方法选择一种分区方法必须在逐案的基础上进行。下面的列表描述了一些可能的分区方法:

1.固定甚至打破记录

这涉及将设置的输入记录分解为偶数个部分(例如,10,其中每个部分正好占整个记录集的 1/10)。然后,每个部分由批处理/提取应用程序的一个实例进行处理。

为了使用这种方法,需要进行预处理以分割设置的记录。这种分割的结果将是一个下界和上界的位置编号,它可以用作批处理/提取应用程序的输入,以便将其处理限制为仅限于其部分。

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

2.按键列分开

这涉及分解由键列(例如位置代码)设置的输入记录,并将每个键的数据分配给批处理实例。为了实现这一点,列值可以是:

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

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

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

在选项 2 中,这确保通过批处理作业的实例覆盖所有值。然而,一个实例处理的值的数量取决于列值的分布(在 0000-0999 范围内可能有大量的位置,而在 1000-1999 范围内可能很少)。在此选项下,数据范围的设计应该考虑分区。

在这两种选择下,都不能实现记录到批处理实例的最优均匀分布。没有动态配置所使用的批处理实例的数量。

3.按视图划分的分手

这种方法基本上是在数据库级别上按键列分解。这涉及到将已有的记录分解成不同的观点。这些视图由批处理应用程序的每个实例在其处理过程中使用。分解是通过对数据进行分组来完成的。

有了这个选项,一个批处理应用程序的每个实例都必须被配置为命中一个特定的视图(而不是主表)。此外,在添加了新的数据值之后,必须将这组新的数据包含到视图中。没有动态配置功能,因为实例数量的变化会导致视图的变化。

4.增加一个处理指示器

这涉及到在输入表中添加一个新列,该列充当指示器。作为预处理步骤,所有指标都被标记为未处理。在批处理应用程序的记录获取阶段,记录被读取,条件是该记录被标记为未处理,并且一旦它们被读取(使用锁定),它们就被标记为正在处理中。当该记录完成时,指示器将更新为“完成”或“错误”。许多批处理应用程序的实例可以在不进行更改的情况下启动,因为附加的列确保只处理一次记录。

有了这个选项,表上的 I/O 会动态增加。在更新批处理应用程序的情况下,这种影响会减少,因为无论如何都必须进行写操作。

5.将表格解压缩为平面文件

这涉及到将表提取到一个文件中。然后可以将该文件拆分成多个段,并将其用作批处理实例的输入。

有了这个选项,将表提取到一个文件中并对其进行分割的额外开销可能会抵消多个分区的影响。动态配置可以通过更改文件分割脚本来实现。

6.哈希列的使用

此方案涉及在用于检索驱动程序记录的数据库表中添加一个散列列(key/index)。这个散列有一个指示器,用于确定批处理应用程序的哪个实例处理这个特定的行。例如,如果要启动三个批处理实例,那么“A”的指示器将标记一个由实例 1 处理的行,“B”的指示器将标记一个由实例 2 处理的行,而“C”的指示器将标记一个由实例 3 处理的行。

然后,用于检索记录的过程将具有一个附加的WHERE子句,以选择由特定指示器标记的所有行。此表中的插入将涉及添加标记字段,这将默认为其中一个实例(例如“a”)。

一个简单的批处理应用程序将用于更新指标,例如在不同实例之间重新分配负载。当添加了足够多的新行时,可以运行这个批处理(除了批处理窗口中的任何时候),以便将新行重新分发到其他实例。

批处理应用程序的其他实例只需要运行前几段所述的批处理应用程序,就可以重新分配指示器,以使用新数量的实例。

4.2 数据库和应用程序设计原则

一个支持使用键列方法在分区数据库表上运行的多分区应用程序的体系结构应该包括一个用于存储分区参数的中心分区存储库。这提供了灵活性并确保了可维护性。存储库通常由一个表组成,称为分区表。

存储在分区表中的信息是静态的,并且通常应该由 DBA 来维护。表应该由多分区应用程序的每个分区的一行信息组成。表中应该有用于程序 ID 代码的列、分区号(分区的逻辑 ID)、此分区的 DB 键列的低值和此分区的 DB 键列的高值。

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

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

  • 将正常处理报告给批处理日志,并将任何错误报告给架构错误处理程序。

4.3 最大限度地减少死锁

当应用程序并行运行或被分区时,数据库资源和死锁中可能会发生争用。作为数据库设计的一部分,数据库设计团队尽可能地消除潜在的争用情况是至关重要的。

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

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

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

4.4 参数传递和验证

分区架构应该对应用程序开发人员相对透明。体系结构应该执行与以分区模式运行应用程序相关的所有任务,包括:

  • 在应用程序启动之前检索分区参数.

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

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

验证应包括检查,以确保:

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

  • 分区之间没有空隙。

如果数据库是分区的,则可能需要进行一些额外的验证,以确保单个分区不会跨越数据库分区。

此外,体系结构应该考虑到分区的合并。关键问题包括:

  • 在进入下一个作业步骤之前,必须完成所有的分区吗?

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