SpringBatch学习与实践(待完成)

1,概述

1.1,简介

​ 在许多企业级应用中都需要使用批处理(bulk processing),以便于在关键业务环境中完成任务

  • 自动、复杂地处理大量信息,无需用户交互即可最有效地处理。这些操作通常包括基于时间的事件(例如月末计算、通知或通信)。

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

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

​ Spring Batch 是一个轻量级、全面的批处理框架,旨在支持开发对企业系统的日常运营至关重要的健壮批处理应用程序。 Spring Batch 建立在人们所期望的 Spring Framework 的特性(生产力、基于 POJO 的开发方法和一般易用性)之上,同时使开发人员在必要时可以轻松访问和利用更先进的企业服务。 Spring Batch 不是调度框架。在商业和开源空间中都有许多优秀的企业调度程序(例如 Quartz、Tivoli、Control-M 等)可用。它旨在与调度程序一起工作,而不是取代调度程序。

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

1.2,背景

​ 虽然开源软件项目和相关社区更加关注基于 Web 和基于微服务的架构框架,但仍然明显缺乏关注可重用架构框架来满足基于 Java 的批处理需求,尽管持续需要处理此类在企业 IT 环境中进行处理。由于缺乏标准的、可重复使用的批处理架构,导致在客户企业 IT 功能中开发的许多一次性内部解决方案激增。——直接报我名字得了,我们这边就是这样的,每次都是一次性开发

​ 我接触的一个业务场景=> 每周(或者每天)从其它的用户管理系统中同步部门和用户信息到达我们自己的用户系统中,一遍达到用户使用场景的无缝切换、并尽可能的保证数据一致性。

image-20220121220806096

​ 如上图所示:外部系统A是其他公司的系统,他们自己有一套用户数据体系,我们的系统是C需要接入他们的数据体系,并且是单向接收同步。外部系统提供的是api接口查询数据,我们通过他们的接口去主动查询数据进行每天的数据校准!

1.3,应用场景

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

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

  • 以某种方式处理数据

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

​ Spring Batch 自动化了这个基本的批处理迭代,提供了将类似事务作为一组处理的能力,通常是在离线环境中,无需任何用户交互。批处理作业是大多数 IT 项目的一部分,Spring Batch 是唯一提供强大的企业级解决方案的开源框架。

​ 业务场景:

  • 定期提交批处理

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

  • 分阶段的企业消息驱动处理

  • 大规模并行批处理

  • 失败后手动或计划重启

  • 相关步骤的顺序处理(扩展工作流驱动的批处理)

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

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

​ 技术目标:

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

  • 基础架构、批处理执行环境和批处理应用程序之间的关注点清晰分离。

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

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

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

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

  • 提供一个简单的部署模型,架构 JAR 与应用程序完全分离,使用 Maven 构建

1.4,架构

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

Figure 1. Spring Batch Layered Architecture

​ 这种分层架构突出了三个主要的高级组件:应用程序、核心处理单元和基础架构

  • 应用程序包含开发人员使用 Spring Batch 编写的所有批处理作业自定义代码

  • Batch Core 包含启动和控制批处理作业所需的核心运行时类。它包括 JobLauncher、Job 和 Step 的实现。(应用程序核心处理单元都建立在一个通用的基础构架之上)

  • 基础架构包含常见的读取器和写入器以及服务(例如 RetryTemplate),应用程序开发人员(读取器和写入器,例如 ItemReader 和 ItemWriter)和核心框架本身(retry,它自己的库)都使用它们。

1.5,基本原则与指导

​ 在构建批处理解决方案时,应考虑以下关键原则、指南和一般注意事项:

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

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

  • 将数据的处理和存储物理上紧密地结合在一起(换句话说,将您的数据保存在您进行处理的地方)。

  • 最小化系统资源使用,尤其是 I/O。在内部存储器(内存操作!)中执行尽可能多的操作。

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

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

    • 重新读取在同一事务中较早读取数据的事务的数据。

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

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

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

  • 在批处理应用程序开始时分配足够的内存,以避免在此过程中进行耗时的重新分配(与上文中在内存中操作数据对应)。

  • 在数据完整性方面始终假设最坏的情况。插入足够的检查和记录验证以保持数据完整性。

  • 尽可能为内部验证实施校验和。例如,flat file(一种包含没有相对关系记录的文件)应该有一个预告记录,告诉文件中的记录总数和关键字段的聚合。

在具有真实数据量的生产环境中尽早计划和执行压力测试。

  • 在大批量系统中,备份可能具有挑战性,尤其是当系统以 7-24小时在线运行时。数据库备份通常在联机设计中得到很好的照顾,但文件备份应该被认为同样重要。如果系统依赖于flat file,则文件备份程序不仅应到位并记录在案,而且还应定期进行测试。

1.6,批处理策略

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

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

  • 验证(Validation )应用程序(校验):验证应用程序确保所有输入/输出记录正确且一致。验证通常基于文件头和尾、校验和和验证算法以及记录级交叉检查。

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

  • 提取/更新(Extract/Update)应用程序(数据写入):从数据库或输入文件中读取记录并根据在每个输入记录中找到的数据对数据库或输出文件进行更改的应用程序。

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

  • 输出/格式化(Output/Format)应用程序(数据格式转换):读取输入文件、根据标准格式重组来自该记录的数据并生成输出文件以供打印或传输到另一个程序或系统的应用程序。

image-20220121232056218

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

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

  • 排序:读取输入文件并生成输出文件的程序,其中记录已根据记录中的排序键字段重新排序。排序通常由标准系统实用程序执行。
  • 拆分:读取单个输入文件并根据字段值将每条记录写入多个输出文件之一的程序。拆分可以由参数驱动的标准系统实用程序定制或执行。
  • 合并:从多个输入文件中读取记录并使用来自输入文件的组合数据生成一个输出文件的程序。可以通过参数驱动的标准系统实用程序来定制或执行合并。

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

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

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

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

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

​ 批处理的典型处理选项是(按实现复杂度递增的顺序):

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

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

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

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

  • 上述选项的组合。

​ 商业调度程序可能支持部分或全部这些选项

​ 以下部分将更详细地讨论这些处理细则。重要的是要注意,根据经验,批处理采用的提交和锁定策略取决于执行的处理类型,并且在线锁定策略也应该使用相同的原则。因此,在设计整体架构时,批处理架构不能简单地成为事后的想法。
​ 锁定策略可以是仅使用普通的数据库锁,也可以在架构中实现额外的自定义锁定服务。锁定服务将跟踪数据库锁定(例如,通过将必要的信息存储在专用的数据库表中)并授予或拒绝请求数据库操作的应用程序的权限。此架构还可以实现重试逻辑,以避免在锁定情况下中止批处理作业。

  1. 批处理窗口中的正常处理

    ​ 对于在单独的批处理窗口中运行的简单批处理,在线用户或其他批处理不需要正在更新的数据,并发不是问题,可以在批处理运行结束时完成单个提交。
    ​ 在大多数情况下,更稳健的方法更合适。请记住,批处理系统有随着时间的推移而增长的趋势,无论是在复杂性还是它们处理的数据量方面。如果没有锁定策略并且系统仍然依赖于单个提交点,那么修改批处理程序可能会很痛苦。因此,即使使用最简单的批处理系统,也要考虑重新启动恢复选项的提交逻辑的需求以及本节后面描述的有关更复杂情况的信息。

  2. 并发批处理或在线处理批处理

    ​ 处理在线用户可以同时更新的数据的批处理应用程序不应将在线用户可能需要的任何数据(无论是在数据库中还是在文件中)锁定超过几秒钟。此外,更新应该在每几个事务结束时提交到数据库。这最大限度地减少了其他进程不可用的数据部分以及数据不可用的经过时间。

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

  • ​ 乐观锁定假设记录争议的可能性很小。这通常意味着在批处理和在线处理同时使用的每个数据库表中插入一个时间戳列。当应用程序获取一行进行处理时,它也会获取时间戳。当应用程序尝试更新已处理的行时,更新使用 WHERE 子句中的原始时间戳。
    • 如果时间戳匹配,则更新数据和时间戳。
    • 如果时间戳不匹配,这表明另一个应用程序在获取和更新尝试之间更新了同一行。因此,无法执行更新。
  • ​ 悲观锁定是任何锁定策略,它假定记录争议的可能性很高,因此需要在检索时获得物理或逻辑锁。一种悲观逻辑锁定在数据库表中使用专用的锁定列。当应用程序检索要更新的行时,它会在锁定列中设置一个标志。使用该标志,尝试检索同一行的其他应用程序在逻辑上会失败。当设置标志的应用程序更新行时,它也会清除标志,从而使其他应用程序能够检索该行。请注意,在初始提取和标志设置之间也必须保持数据的完整性,例如通过使用数据库锁(例如 SELECT FOR UPDATE)。另请注意,此方法与物理锁定具有相同的缺点,只是它更容易管理构建一个超时机制,如果用户在记录被锁定的情况下去吃午饭时锁被释放了!

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

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

  1. 并行处理

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

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

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

  1. 分治

​ 使用分治允许多个版本的大批量应用程序同时运行。这样做的目的是减少处理长批处理作业所需的时间。可以成功拆分的进程是可以拆分输入文件和/或对主数据库表进行分区以允许应用程序针对不同数据集运行的进程。
​ 此外,必须将已拆分的进程设计为仅处理其分配的数据集。分区架构必须与数据库设计和数据库分区策略密切相关。请注意,数据库分区并不一定意味着数据库的物理分区,尽管在大多数情况下这是可取的。下图说明了分区方法:

Figure 2. Partitioned Process

​ 该架构应该足够灵活,以允许动态配置分区数量。应同时考虑到自动和用户配置。自动配置可以基于输入文件大小和输入记录数等参数。

​ 4.1 分区方法

​ 必须根据具体情况选择分区方法。以下列表描述了一些可能的分区方法:

​ 4.1.1,固定乃至拆分记录集

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

​ 为了使用这种方法,需要预处理来拆分记录集。此拆分的结果将是一个下限和上限放置编号,可用作批处理/提取应用程序的输入,以便将其处理限制在其部分。

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

​ 4.1.2,按关键列分解

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

  • 由分区表分配给批处理实例(本节稍后介绍)。
  • 通过值的一部分(例如 0000-0999、1000 - 1999 等)分配给批处理实例。

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

​ 在选项 2 下,这可确保通过批处理作业的实例覆盖所有值。但是,一个实例处理的值的数量取决于列值的分布(0000-0999 范围内的位置可能很多,而 1000-1999 范围内的位置很少)。在此选项下,数据范围的设计应考虑到分区。
​ 在这两种选择下,都无法实现记录到批处理实例的最佳均匀分布。使用的批处理实例数量没有动态配置。

​ 4.1.3 按视图分解

​ 这种方法基本上是由一个键列分解,但在数据库级别。它涉及将记录集分解为视图。这些视图由批处理应用程序的每个实例在其处理期间使用。分解是通过对数据进行分组来完成的。

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

​ 4.1.4 增加处理指标

​ 这涉及在输入表中添加一个新列(字段),作为一个指标。作为预处理步骤,所有指标都被标记为未处理。在批处理应用程序的记录获取阶段,在该记录被标记为未处理的条件下读取记录,并且一旦它们被读取(带锁),它们被标记为正在处理。当该记录完成时,指示器会更新为完成或错误。批处理应用程序的许多实例无需更改即可启动,因为附加列可确保仅处理一次记录。

​ 使用此选项,表上的 I/O 会动态增加。在更新批处理应用程序的情况下,这种影响会降低,因为无论如何都必须进行写入。

​ 4.1.5 将表格提取到平面文件
​ 这涉及将表提取到文件中。然后可以将该文件拆分为多个段并用作批处理实例的输入。

​ 使用此选项,将表提取到文件中并将其拆分的额外开销可能会抵消多分区的影响。可以通过更改文件拆分脚本来实现动态配置。

​ 4.1.6 哈希列的使用
​ 该方案涉及向用于检索驱动程序记录的数据库表添加哈希列(键/索引)。此散列列有一个指示符,用于确定批处理应用程序的哪个实例处理此特定行。例如,如果要启动三个批处理实例,则指示符“A”标记行由实例 1 处理,指示符“B”标记行由实例 2 处理,指示符“C” ' 标记要由实例 3 处理的行。

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

​ 将使用一个简单的批处理应用程序来更新指标,例如在不同实例之间重新分配负载。当添加了足够多的新行时,可以运行此批处理(任何时候,除了在批处理窗口中)以将新行重新分配给其他实例。
批处理应用程序的其他实例只需要运行前面段落中描述的批处理应用程序来重新分配指标以使用新数量的实例。

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

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

​ 存储在分区表中的信息是静态的,通常应由 DBA 维护。该表应包含多分区应用程序的每个分区的一行信息。该表应包含 Program ID Code、Partition Number(分区的逻辑 ID)、此分区的 db key 列的 Low Value 和此分区的 db key 列的 High Value 的列。

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

  • 添加到输出文件/数据库更新,以使合并过程正常工作。
  • 向批处理日志报告正常处理,向架构错误处理程序报告任何错误。

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

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

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

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

​ 4.4 参数传递和验证
​ 分区架构对应用程序开发人员应该是相对透明的。架构应该执行与以分区模式运行应用程序相关的所有任务,包括:

  • 在应用程序启动之前检索分区参数。
  • 在应用程序启动之前验证分区参数。
  • 在启动时将参数传递给应用程序。

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

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

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

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

  • 在进入下一个工作步骤之前必须完成所有分区吗?
  • 如果其中一个分区中止会发生什么?
  1. 运行或作业并行运行,以最大限度地减少批处理运行的总时间。只要作业不共享相同的文件、数据库表或索引空间,这不是问题。如果他们这样做,则应使用分区数据来实现此服务。另一种选择是构建一个架构模块,通过使用控制表来维护相互依赖关系。控制表应该包含每个共享资源的一行,以及它是否正在被应用程序使用。然后,批处理架构或并行作业中的应用程序将从该表中检索信息,以确定它是否可以访问所需的资源。

​ 如果数据访问没有问题,可以通过使用额外的线程并行处理来实现并行处理。在大型机环境中,传统上使用并行作业类,以确保所有进程都有足够的 CPU 时间。无论如何,解决方案必须足够健壮,以确保所有运行进程的时间片。
并行处理中的其他关键问题包括负载平衡和通用系统资源(如文件、数据库缓冲池等)的可用性。另请注意,控制表本身很容易成为关键资源。

2,概念

​ The Domain Language of Batch

​ 对于任何有经验的批处理架构师来说,Spring Batch 中使用的批处理的整体概念应该熟悉和舒适的。有“作业”和“步骤”以及开发人员提供的称为 ItemReader 和 ItemWriter 的处理单元。然而,由于 Spring 模式、操作、模板、回调和惯用语,有以下可能:

  • 在遵守明确的关注点分离方面有显着改善。
  • 清晰描述的架构层和作为接口提供的服务。
  • 简单和默认的实现,允许快速采用和开箱即用的易用性。
  • 可扩展性显着增强。

​ 下图是已经使用了几十年的批处理参考架构的简化版本。它概述了构成批处理领域语言的组件。这个架构框架是一个蓝图,已经在过去几代平台(COBOL/Mainframe、C/Unix,以及现在的 Java/anywhere)上经过数十年的实施证明。 JCL 和 COBOL 开发人员可能与 C、C# 和 Java 开发人员一样熟悉这些概念。 Spring Batch 提供了在健壮、可维护的系统中常见的层、组件和技术服务的物理实现,这些系统用于解决从简单到复杂的批处理应用程序的创建,以及用于解决非常复杂的处理需求的基础设施和扩展。

Figure 1. Batch Stereotypes

​ 上图突出了构成 Spring Batch 领域语言的关键概念。一个 Job 有一对多的步骤,每个步骤正好有一个 ItemReader、一个 ItemProcessor 和一个 ItemWriter。需要启动一个作业(使用 JobLauncher),并且需要存储有关当前运行进程的元数据(在 JobRepository 中)。

2.1,作业(Job)

​ 本节描述与批处理作业概念相关的构造型。 Job 是封装整个批处理过程的实体。与其他 Spring 项目一样,Job 与 XML 配置文件或基于 Java 的配置连接在一起。这种配置可以称为“作业配置”。但是,Job 只是整个层次结构的顶部,如下图所示:

结合java的视角进行描述就是:

​ Job就是一个类,JobInstance就是一个实例,JobExecution就是一次实例的执行。

​ 在 Spring Batch 中,Job 只是 Step 实例的容器。它将逻辑上属于流中的多个步骤组合在一起,并允许为所有步骤配置全局属性,例如可重新启动性。作业配置包含:

  • 作业的简单名称(例如:上图中的EndOfDay作业)。
  • Step 实例的定义和排序。
  • 作业是否可重新启动。

​ 对于那些使用 Java 配置的人,Spring Batch 以 SimpleJob 类的形式提供了 Job 接口的默认实现,它在 Job 之上创建了一些标准功能。使用基于 java 的配置时,构建器集合可用于作业的实例化,如以下示例所示:

@Bean
public Job footballJob() {
    return this.jobBuilderFactory.get("footballJob")
                     .start(playerLoad())
                     .next(gameLoad())
                     .next(playerSummarization())
                     .build();
}

2.1.1,作业实例(JobInstance )

​ JobInstance 是指逻辑作业运行的概念。考虑一个应该在一天结束时运行一次的批处理作业,例如上图中的“EndOfDay”作业。有一个“EndOfDay”作业,但必须单独跟踪作业的每个单独运行。对于此作业,每天有一个逻辑 JobInstance。例如,有 1 月 1 日运行、1 月 2 日运行等。如果 1 月 1 日运行第一次失败并在第二天再次运行,则仍然是 1 月 1 日运行。 (通常,这也与它正在处理的数据相对应,这意味着 1 月 1 日的运行会处理 1 月 1 日的数据)。因此,每个 JobInstance 可以有多个执行(JobExecution 将在本章后面更详细地讨论),并且只有一个对应于特定 Job 并标识 JobParameters 的 JobInstance 可以在给定时间运行。

​ JobInstance 的定义与要加载的数据完全无关。完全由 ItemReader 实现来确定如何加载数据。例如,在 EndOfDay 场景中,数据上可能有一列指示数据所属的“生效日期”或“计划日期”。因此,1 月 1 日的运行将仅加载第 1 次的数据,而 1 月 2 日的运行将仅使用第 2 次的数据。因为这个决定很可能是一个商业决策,所以由 ItemReader 来决定。但是,使用相同的 JobInstance 会决定是否使用先前执行的“状态”(即本章稍后讨论的 ExecutionContext)。使用新的 JobInstance 意味着“从头开始”,使用现有实例通常意味着“从你离开的地方开始”。

2.1.2,作业参数(JobParameters)

​ 在讨论了 JobInstance 以及它与 Job 的区别之后,自然要问的问题是:“一个 JobInstance 与另一个 JobInstance 有何区别?”答案是:JobParameters。 JobParameters 对象包含一组用于启动批处理作业的参数。它们可用于识别,甚至可在运行期间用作参考数据,如下图所示:

​ 在前面的示例中,有两个实例,一个用于 1 月 1 日,另一个用于 1 月 2 日,实际上只有一个 Job,但它有两个 JobParameter 对象:一个以 01-01-2017 的作业参数启动另一个以 01-02-2017 的参数开始。因此,合约可以定义为:JobInstance = Job + 标识 JobParameters。这允许开发人员有效地控制 JobInstance 的定义方式,因为他们控制传入的参数。

2.1.3,作业执行(JobExecution )

​ JobExecution 是指一次尝试运行 Job 的技术概念。执行可能以失败或成功结束,但除非执行成功完成,否则对应于给定执行的 JobInstance 不被视为完成。以前面描述的 EndOfDay 作业为例,考虑 2017 年 1 月 1 日的 JobInstance,它在第一次运行时失败。如果使用与第一次运行 (01-01-2017) 相同的识别作业参数再次运行它,则会创建一个新的 JobExecution。但是,仍然只有一个 JobInstance。

​ Job 定义了作业是什么以及如何执行,JobInstance 是一个纯粹的组织对象,用于将执行分组在一起,主要是为了启用正确的重启语义。然而,JobExecution 是运行期间实际发生的事情的主要存储机制,并且包含许多必须控制和持久化的属性,如下表所示:

2.2,步骤(step)

​ Step 是一个域对象,它封装了批处理作业的独立、顺序阶段。因此,每个 Job 完全由一个或多个步骤组成。步骤包含定义和控制实际批处理所需的所有信息。这必然是一个模糊的描述,因为任何给定 Step 的内容都由编写 Job 的开发人员自行决定。步骤可以根据开发人员的需要简单或复杂。一个简单的步骤可以将文件中的数据加载到数据库中,需要很少或不需要代码(取决于使用的实现)。更复杂的步骤可能具有作为处理的一部分应用的复杂业务规则。与 Job 一样,Step 具有与唯一 JobExecution 相关联的单独 StepExecution,如下图所示:

步骤执行(StepExecution )

​ StepExecution 表示执行 Step 的单次尝试。每次运行 Step 时都会创建一个新的 StepExecution,类似于 JobExecution。但是,如果一个步骤因为之前的步骤失败而无法执行,则不会为它持久执行。 StepExecution 仅在其 Step 实际启动时创建。

2.3,执行上下文(ExecutionContext )

​ ExecutionContext 表示由框架持久化和控制的键/值对的集合,以允许开发人员存储范围为 StepExecution 对象或 JobExecution 对象的持久状态(存储、记录)。对于熟悉 Quartz 的人来说,它与 JobDataMap 非常相似。最好的使用例子是方便重启。以平面文件输入为例,在处理单个行时,框架会在提交点定期保存 ExecutionContext。这样做可以让 ItemReader 存储其状态,以防在运行期间发生致命错误或即使断电。所需要的只是将当前读取的行数放入上下文中,如下例所示,其余的由框架完成:

executionContext.putLong(getKey(LINES_READ_COUNT), reader.getPosition());

​ 以 Job Stereotypes 部分中的 EndOfDay 示例为例,假设有一个步骤“loadData”将文件加载到数据库中。在第一次运行失败后,元数据表将类似于以下示例:

JOB_INST_ID JOB_NAME
1 EndOfDayJob
JOB_INST_ID TYPE_CD KEY_NAME DATE_VAL
1 DATE schedule.Date 2017-01-01
STEP_EXEC_ID JOB_EXEC_ID STEP_NAME START_TIME END_TIME STATUS
1 1 loadData 2017-01-01 21:00 2017-01-01 21:30 FAILED
STEP_EXEC_ID SHORT_CONTEXT
1

2.4,作业存储库(JobRepository)

​ JobRepository 是上述所有 Stereotypes 的持久化机制。它为 JobLauncher、Job 和 Step 实现提供 CRUD 操作。首次启动 Job 时,会从存储库中获取 JobExecution,并且在执行过程中,通过将 StepExecution 和 JobExecution 实现传递到存储库来持久化它们。

2.5,作业启动器(JobLauncher)

​ JobLauncher 表示一个简单的接口,用于使用给定的一组 JobParameters 启动 Job,如以下示例所示:

public interface JobLauncher {

public JobExecution run(Job job, JobParameters jobParameters)
            throws JobExecutionAlreadyRunningException, JobRestartException,
                   JobInstanceAlreadyCompleteException, JobParametersInvalidException;
}

​ 期望实现从 JobRepository 获取有效的 JobExecution 并执行 Job。

2.6,项目阅读器(ItemReader)

​ ItemReader 是一种抽象,表示检索 Step 的输入,一次一个项目。当 ItemReader 用尽了它可以提供的项目时,它通过返回 null 来表明这一点。有关 ItemReader 接口及其各种实现的更多详细信息,请参阅 Readers 和 Writers。

2.7,项目写入器(ItemWriter)

​ ItemWriter 是一个抽象,它表示一个步骤的输出,一次是一批或一大块项目。通常,ItemWriter 不知道它接下来应该接收的输入,并且只知道在其当前调用中传递的项目。有关 ItemWriter 接口及其各种实现的更多详细信息可以在 Readers 和 Writers 中找到。

2.8,项目执行器(ItemProcessor)

​ ItemProcessor 是一个抽象,表示项目的业务处理(可选的)。当 ItemReader 读取一个项目,而 ItemWriter 写入它们时,ItemProcessor 提供了一个访问点来转换或应用其他业务处理。如果在处理该项目时确定该项目无效,则返回 null 表示不应写出该项目。关于 ItemProcessor 接口的更多细节可以在 Readers 和 Writers 中找到。

3,执行操作

3.1,作业(Job)

​ 一个作业(Job)中,step从一个到另外一个的直接过程!

// 主函数
@SpringBootApplication
@EnableBatchProcessing
public class BatchApplication {
    public static void main(String[] args) {
        SpringApplication.run(BatchApplication.class, args);
    }
}

// jobflow类
@Configuration
public class JobFlowConfiguration {
    @Autowired
    private JobBuilderFactory jobBuilderFactory;
    @Autowired
    private StepBuilderFactory stepBuilderFactory;

    @Bean
    public Job helloWorld() {
        return jobBuilderFactory.get("jobFlowDemo1")
          			// 普通的串行执行
/*                .start(step1())
                .next(step2())
                .next(step3())*/

                // 类似状态机转换!
                .start(step1()).on("COMPLETED").to(step2())
                .from(step2()).on("COMPLETED").to(step3())
                .from(step3()).end()
                .build();
    }

    private Step step1() {
        return stepBuilderFactory.get("step1")
                .tasklet((stepContribution, chunkContext)->{
                    System.out.println("step1");
                    return RepeatStatus.FINISHED;
                }).build();
    }
    private Step step2() {
        return stepBuilderFactory.get("step1")
                .tasklet((stepContribution, chunkContext)->{
                    System.out.println("step2");
                    return RepeatStatus.FINISHED;
                }).build();
    }
    private Step step3() {
        return stepBuilderFactory.get("step1")
                .tasklet((stepContribution, chunkContext)->{
                    System.out.println("step3");
                    return RepeatStatus.FINISHED;
                }).build();
    }
}

3.2,流(flow)

​ flow是多个具有相互转换关系步骤(step)它们的集合,其主要目的是为了复用(在一个作业job中,或者从一个作业到另外一个作业时),可以使用FlowBuilder创建一个流(与创建一个流基本相似)。

image-20220122154759019

​ 就是层级关系一样:一个job中可以包括多个flow、step,一个flow中可以包括多个step(类似于代码块分割)

// flow样例
@Configuration
public class JobFlowTwoConfiguration {

    @Autowired
    private JobBuilderFactory jobBuilderFactory;

    @Autowired
    private StepBuilderFactory stepBuilderFactory;

    @Bean
    public Flow jobFlowDemoFlow1() {
        return new FlowBuilder<Flow>("jobFlowDemoFlow1")
                .start(step2())
                .next(step3())
                .build();
    }

    @Bean
    public Job jobFlowDemoJob1() {
        return jobBuilderFactory.get("jobFlowDemoJob1")
                .start(jobFlowDemoFlow1())
                .next(step1()).end()
                .build();
    }

    private Step step1() {
        return stepBuilderFactory.get("step1")
                .tasklet((stepContribution, chunkContext)->{
                    System.out.println("step1");
                    return RepeatStatus.FINISHED;
                }).build();
    }
    private Step step2() {
        return stepBuilderFactory.get("step1")
                .tasklet((stepContribution, chunkContext)->{
                    System.out.println("step2");
                    return RepeatStatus.FINISHED;
                }).build();
    }
    private Step step3() {
        return stepBuilderFactory.get("step1")
                .tasklet((stepContribution, chunkContext)->{
                    System.out.println("step3");
                    return RepeatStatus.FINISHED;
                }).build();
    }
}

3.3,分割流

​ 到目前为止描述的每个场景都涉及一个以线性方式一次执行其步骤的作业。除了这种典型的风格,Spring Batch 还允许使用并行流配置作业。

​ 基于 Java 的配置允许您通过提供的builders配置拆分。如以下示例所示,“split”元素包含一个或多个“流”元素,其中可以定义整个单独的流。 'split' 元素还可以包含任何先前讨论的转换元素,例如'next' 属性或'next'、'end' 或'fail' 元素。

@Configuration
public class JobSplitDemoConfiguration {

    @Autowired
    private JobBuilderFactory jobBuilderFactory;

    @Autowired
    private StepBuilderFactory stepBuilderFactory;

    @Bean
    public Job jobSplitJob1() {
        return jobBuilderFactory.get("jobSplitJob01")
                .start(jobSplitFlow1())
                .split(new SimpleAsyncTaskExecutor())
                .add(jobSplitFlow2())
                .end()
                .build();
    }

    @Bean
    public Flow jobSplitFlow1() {
        return new FlowBuilder<Flow>("jobSplitFlow01")
                .start(stepBuilderFactory
                        .get("jobSplitStep1").
                        tasklet(taskletPre())
                        .build())
                .build();
    }

    @Bean
    public Flow jobSplitFlow2() {
        return new FlowBuilder<Flow>("jobSplitFlow02")
                .start(stepBuilderFactory
                        .get("jobSplitStep2")
                        .tasklet(taskletPre())
                        .build())
                .next(stepBuilderFactory
                        .get("jobSplitStep3")
                        .tasklet(taskletPre())
                        .build())
                .build();
    }

    private Tasklet taskletPre() {
        return new PrintTasklet();
    }

    private class PrintTasklet implements Tasklet {
        @Override
        public RepeatStatus execute(StepContribution stepContribution,
                                    ChunkContext chunkContext) {
            System.out.println(String.format("%s has been executed on thread %s",
                    chunkContext.getStepContext().getStepName(),
                    Thread.currentThread().getName()));
            return RepeatStatus.FINISHED;
        }
    }
}
/* 执行结果
jobSplitStep2 has been executed on thread SimpleAsyncTaskExecutor-1
jobSplitStep1 has been executed on thread SimpleAsyncTaskExecutor-2jobSplitStep3 has been executed on thread SimpleAsyncTaskExecutor-1
*/

3.4,程序流决策器

​ 在某些情况下,可能需要比退出状态更多的信息来决定下一步执行哪个步骤。在这种情况下,可以使用 JobExecutionDecider 来协助决策,如下例所示:

public class MyDecider implements JobExecutionDecider {
    public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) {
        String status;
        if (someCondition()) {
            status = "FAILED";
        }
        else {
            status = "COMPLETED";
        }
        return new FlowExecutionStatus(status);
    }
}

参考链接

1,Spring Batch官网

2,spring batch使用reader读数据的内存容量问题

3,批处理框架spring batch基础知识介绍

4,什么是平面文件(Flat-File Database)?

5,Readers And Writers

6,Spring Batch 批处理基础视频教程

posted on 2022-01-22 16:35  周健康  阅读(536)  评论(0编辑  收藏  举报

导航