Spring Batch 架构

本站( springdoc.cn )中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。

Spring Batch的设计考虑到了可扩展性和不同的终端用户群。下图显示了支持终端用户开发者的可扩展性和易用性的分层架构。

Figure 1.1: Spring Batch Layered Architecture
Figure 1. Spring Batch Layered Architecture

这种分层架构突出了三个主要的高层组件: 应用、核心和基础设施。应用程序包含所有批处理作业(job)和开发人员使用Spring Batch编写的自定义代码。批处理核心包含启动和控制批处理作业所需的核心运行时类。它包括 JobLauncherJobStep 的实现。应用程序和核心都是建立在一个通用的基础设施之上。这个基础架构包含了通用的 reader/writer 和服务(如 RetryTemplate),它们既被应用开发者使用(reader/writer,如 ItemReaderItemWriter),也被核心框架本身使用(retry,这是它自己的库)。

一般的批处理原则和指南

在建立一个批处理解决方案时,应考虑以下关键原则、准则和一般考虑。

  • 记住,批处理架构通常会影响到在线架构,反之亦然。在设计时要考虑到这两种架构和环境,尽可能地使用共同的构件。

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

  • 将数据的处理和存储在物理上保持在一起(换句话说,将你的数据保存在你的处理过程中)。

  • 最大限度地减少系统资源的使用,特别是I/O。尽可能多地在内部存储器中进行操作。

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

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

    • 为一个事务重读数据,而该数据在同一事务中早先已被读取。

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

    • 在SQL语句的 WHERE 子句中不指定 key 值。

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

  • 在批处理程序开始时分配足够的内存,以避免在处理过程中耗费时间的重新分配。

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

  • 在可能的情况下,为内部验证实施校验。例如,扁平文件(flat file)应该有一个尾部记录,说明文件中的记录总数和关键字段的总和。

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

  • 在大型批处理系统中,备份可能是一个挑战,特别是当系统与在线应用24小时同步运行时。在在线设计中,数据库备份通常被很好地照顾到,但文件备份应该被认为是同样重要的。如果系统依赖于扁平文件,文件备份程序不仅要到位和记录,还要定期测试。

批处理策略

为了帮助设计和实现批处理系统,应该以示例架构图和 code shell 的形式向设计者和程序员提供基本的批处理应用构件和模式。当开始设计一个批处理工作时,业务逻辑应该被分解成一系列的步骤,这些步骤可以通过使用以下标准构件来实现:

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

  • 验证应用: 验证应用程序确保所有输入和输出记录是正确和一致的。验证通常基于文件 header 和 trailer、校验和验证算法以及记录级交叉检查。

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

  • 提取/更新应用: 提取/更新应用程序从数据库或输入文件中读取记录,并在每个输入记录中发现的数据驱动下,对数据库或输出文件进行修改。

  • 处理/更新应用: 一个处理和更新应用程序对来自提取或验证应用程序的输入事务进行处理。处理过程通常涉及读取数据库以获得处理所需的数据,有可能更新数据库并为输出处理创建记录。

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

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

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

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

  • 切割: 取单个输入文件,并根据字段值将每条记录写到几个输出文件中的一个的程序。拆分可以是定制的,也可以由参数驱动的标准系统实用程序执行。

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

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

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

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

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

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

典型的批处理选项是(按实施复杂程度递增):

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

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

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

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

  • 前述选项的组合。

这些选项中的一部分或全部可能被商业调度器所支持。

本节的其余部分将更详细地讨论这些处理选项。请注意,作为一条经验法则,批处理所采用的提交和加锁策略取决于所进行的处理类型,在线加锁策略也应该采用同样的原则。因此,在设计整体架构时,不能简单地把批处理架构作为事后考虑。

加锁策略可以是只使用正常的数据库锁,或者在架构中实现一个额外的自定义锁服务。锁服务将跟踪数据库锁(例如,通过在一个专门的数据库表中存储必要的信息),并对请求数据库操作的应用程序给予或拒绝权限。重试逻辑也可以由这个架构来实现,以避免在出现锁的情况下中止批处理作业。

1. 批处理窗口中的正常处理 对于在单独的批处理窗口中运行的简单批处理来说,被更新的数据不被在线用户或其他批处理所需要,没有并发问题,在批处理运行结束时可以做一次提交。

在大多数情况下,更稳健的方法更合适。请记住,批处理系统有随着时间的推移而增长的趋势,无论是在复杂性还是在处理的数据量方面。如果没有加锁策略,系统仍然依赖单一的提交点,修改批处理程序会很痛苦。因此,即使是最简单的批处理系统,也要考虑重启-恢复选项的提交逻辑的需要,以及本节后面描述的更复杂情况的相关信息。

2. 并发批处理或在线处理 批处理程序处理的数据可以同时被在线用户更新,不应锁定任何在线用户可能需要的数据(在数据库或文件中)超过几秒钟。另外,更新应该在每几个事务结束后提交到数据库。这样做可以最大限度地减少其他进程不可用的数据部分以及数据不可用的时间。

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

  • 乐观锁假定记录竞争的可能性很低。它通常意味着在每个数据库表中插入一个时间戳列,由批处理和在线处理同时使用。当一个应用程序获取一条记录进行处理时,它也获取了时间戳。当应用程序试图更新已处理的行时,更新会使用 WHERE 子句中的原始时间戳。如果时间戳匹配,数据和时间戳就会被更新。如果时间戳不匹配,这表明另一个应用程序在获取和更新尝试之间已经更新了同一行。因此,更新不能被执行。

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

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

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

3. 并行处理 并行处理让多个批处理或作业并行运行,以尽量减少总的批处理时间。只要作业不共享相同的文件、数据库表或索引空间,这就不是一个问题。如果他们这样做,这项服务应该通过使用分区数据来实现。另一个选择是建立一个架构模块,通过使用控制表来维护相互依赖关系。一个控制表应该包含每一个共享资源的一行,以及它是否被一个应用程序使用。然后,批处理架构或并行作业中的应用程序将从该表中检索信息,以确定它是否可以获得它所需要的资源的访问。

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

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

4. 分区 使用分区可以让多个版本的大型批处理程序同时运行。这样做的目的是为了减少处理长批处理作业所需的时间。可以成功分区的程序是那些输入文件可以被分割或主数据库表被分割的程序,以使应用程序针对不同的数据集运行。

此外,被分区的进程必须被设计为只处理其指定的数据集。分区架构必须与数据库设计和数据库分区策略紧密联系在一起。请注意,数据库分区并不一定意味着数据库的物理分区(尽管在大多数情况下,这是可取的)。下面的图片说明了分区的方法:

Figure 1.2: Partitioned Process
Figure 2. 分区的过程

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

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

1. 记录集的固定和均匀分解

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

要使用这种方法,需要进行预处理,以分割记录集。这种分割的结果是一个下限和上限的安置数,你可以把它作为批处理/提取应用程序的输入,以限制其处理只限于其部分。

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

2. 按key列分解

这涉及到通过一个key列(如 location code)来分割输入记录集,并将每个关键的数据分配给一个批处理实例。为了实现这一点,列值可以是:

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

  • 按部分数值分配给一个批处理实例(如0000-0999,1000-1999,等等)。

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

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

在这两个选项下,无法实现记录到批处理实例的最佳均匀分布。没有对使用的批处理实例的数量进行动态配置。

3. 按视图(View)分解

这种方法基本上是在数据库层面上通过一个 key 列进行分解。它涉及到将记录集分解成视图。这些视图在处理过程中被批处理程序的每个实例所使用。分解是通过对数据分组来完成的。

有了这个选项,批处理程序的每个实例都必须被配置为命中一个特定的视图(而不是主表)。另外,随着新数据值的增加,这组新的数据必须被纳入一个视图中。没有动态配置能力,因为实例数量的变化会导致视图的变化。

4. 增加一个处理指标

这涉及到在输入表中增加一个新的列,作为一个指标。作为一个预处理步骤,所有指标都被标记为未处理。在批处理程序的记录获取阶段,记录被读取的条件是个别记录被标记为未处理,一旦被读取(带锁),它就被标记为正在处理中。当该记录完成后,该指标被更新为完成或错误。你可以在不改变的情况下启动一个批处理程序的许多实例,因为附加列确保一条记录只被处理一次。

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

5. 提取表到一个扁平文件

这种方法涉及将表提取到一个平面文件中。然后这个文件可以被分割成多个片段,并作为批处理实例的输入。

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

6. 使用哈希列

这个方案涉及到在用于检索驱动程序记录的数据库表中增加一个哈希列(键或索引)。这个哈希列有一个指标,以确定批处理程序的哪个实例处理这个特定行。例如,如果有三个批处理实例要启动,一个指标 "A" 标志着由实例1处理的行,一个指标 "B" 标志着由实例2处理的行,而一个指标 "C" 标志着由实例3处理的行。

用于检索记录的过程将有一个额外的 WHERE 子句来选择所有被特定指标标记的记录。在这个表中的插入将涉及到标记字段的添加,它将被默认为其中一个实例(如 'A')。

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

批处理应用的额外实例只需要运行批处理应用(如前几段所述)来重新分配指标,以便与新数量的实例一起工作。

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

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

存储在分区表中的信息是静态的,一般来说,应该由DBA来维护。该表应该由一个多分区应用程序的每个分区的一条信息组成。该表应该有程序 ID Code、分区编号(分区的逻辑ID)、该分区的数据库 key 列的低值和该分区的数据库 key 列的高值等列。

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

  • 添加到输出文件或数据库更新中,以使合并过程正常工作。

  • 向批处理日志报告正常处理,向架构 error handler 报告任何错误。

4.3 尽量减少死锁

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

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

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

为了尽量减少冲突对数据的影响,架构应该在附加到数据库或遇到死锁时提供服务(如等待和重试间隔)。这意味着要有一个内置的机制来对某些数据库的返回代码做出反应,而不是立即发出错误,而是等待一个预定的时间并重试数据库的操作。

4.4 参数传递和验证

分区架构对应用程序开发人员来说应该是相对透明的。该架构应该执行与在分区模式下运行应用程序相关的所有任务,包括:

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

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

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

验证应包括检查以确保:

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

  • 分区之间没有空隙。

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

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

  • 在进入下一个 job step 之前,是否必须完成所有的分区?

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