SpringBatch从入门到精通-6.3 读和写处理-实战3【掘金日新计划】

413 阅读12分钟

持续创作,加速成长,6月更文活动来啦!| 掘金·日新计划

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第14天,点击查看活动详情

SpringBatch从入门到精通-1【掘金日新计划】

SpringBatch从入门到精通-2-StepScope作用域和用法【掘金日新计划】

SpringBatch从入门到精通-3-并行处理【掘金日新计划】

SpringBatch从入门到精通-3.2-并行处理-远程分区【掘金日新计划】

SpringBatch从入门到精通-3.3-并行处理-远程分区(消息聚合)【掘金日新计划】

SpringBatch从入门到精通-4 监控和指标【掘金日新计划】

SpringBatch从入门到精通-4.2 监控和指标-原理【掘金日新计划】

SpringBatch从入门到精通-5 数据源配置相关【掘金日新计划】

SpringBatch从入门到精通-5.2 数据源配置相关-原理【掘金日新计划】

SpringBatch从入门到精通-6 读和写处理【掘金日新计划】

SpringBatch从入门到精通-6.1 读和写处理-实战【掘金日新计划】

SpringBatch从入门到精通-6.2 读和写处理-实战2【掘金日新计划】

数据库相关

与大多数企业应用程序样式一样,数据库是批处理的核心存储机制。但是,由于系统必须使用的数据集的庞大规模,批处理与其他应用程序样式不同。如果一条 SQL 语句返回 100 万行,则结果集可能会将所有返回的结果保存在内存中,直到所有行都被读取。Spring Batch 为这个问题提供了两种类型的解决方案:

  • 基于光标cursor的ItemReader实现
  • 分页ItemReader实现

基于光标的ItemReader实现

使用数据库游标通常是大多数批处理开发人员的默认方法,因为它是数据库对“流式传输”关系数据问题的解决方案。Java中ResultSet类本质上是一种用于操作游标的面向对象的机制。ResultSet维护一个指向当前数据行的游标。调用next ResultSet会将此光标移动到下一行。Spring Batch 基于游标的ItemReader 实现,在初始化时打开一个游标,并在每次调用时将游标向前移动一行read,返回一个可用于处理的映射对象。然后调用该 close方法以确保释放所有资源。Spring 核心 JdbcTemplate通过使用回调模式将所有行完全映射到一个ResultSet并在将控制权返回给方法调用者之前关闭。但是,在批处理中,这必须等到步骤完成。下图显示了基于光标的ItemReader工作原理的通用图。请注意,虽然该示例使用 SQL(因为 SQL 广为人知),但任何技术都可以实现基本方法。

光标示例

图 3. 光标示例

这个例子说明了基本模式。给定一个“FOO”表,它有三列 :ID、、NAME和BAR应该是一个完全映射的Foo对象。再次调用read()会将光标移动到下一行,即FooID 为 3 的 。这些读取的结果在每个 之后写出 read,从而允许对对象进行垃圾收集(假设没有实例变量维护对它们的引用)。

JdbcCursorItemReader

JdbcCursorItemReader是基于游标技术的 JDBC 实现。它直接与 ResultSet一起工作,并且需要一条 SQL 语句来针对从 DataSource 获得的连接运行。以下数据库模式用作示例:

CREATE TABLE CUSTOMER (
   ID BIGINT IDENTITY PRIMARY KEY,
   NAME VARCHAR(45),
   CREDIT FLOAT
);

许多人喜欢为每一行使用一个域对象,因此下面的示例使用RowMapper接口的实现来映射一个CustomerCredit对象:

public class CustomerCreditRowMapper implements RowMapper<CustomerCredit> {
​
    public static final String ID_COLUMN = "id";
    public static final String NAME_COLUMN = "name";
    public static final String CREDIT_COLUMN = "credit";
​
    public CustomerCredit mapRow(ResultSet rs, int rowNum) throws SQLException {
        CustomerCredit customerCredit = new CustomerCredit();
​
        customerCredit.setId(rs.getInt(ID_COLUMN));
        customerCredit.setName(rs.getString(NAME_COLUMN));
        customerCredit.setCredit(rs.getBigDecimal(CREDIT_COLUMN));
​
        return customerCredit;
    }
}

由于JdbcCursorItemReader与 共享关键接口JdbcTemplate,因此查看如何使用 读取此数据的示例JdbcTemplate以将其与 进行对比很有用ItemReader。出于本示例的目的,假设CUSTOMER数据库中有 1,000 行。第一个示例使用JdbcTemplate:

//For simplicity sake, assume a dataSource has already been obtained
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
List customerCredits = jdbcTemplate.query("SELECT ID, NAME, CREDIT from CUSTOMER",
                                          new CustomerCreditRowMapper());

运行上述代码片段后,customerCredits列表包含 1,000 个 CustomerCredit对象。在查询方法中,从 中获取连接 DataSource,针对它运行提供的 SQL,并mapRow为 中的每一行调用该方法ResultSet。将此与 的方法进行对比 JdbcCursorItemReader,如下例所示:

JdbcCursorItemReader itemReader = new JdbcCursorItemReader();
itemReader.setDataSource(dataSource);
itemReader.setSql("SELECT ID, NAME, CREDIT from CUSTOMER");
itemReader.setRowMapper(new CustomerCreditRowMapper());
int counter = 0;
ExecutionContext executionContext = new ExecutionContext();
itemReader.open(executionContext);
Object customerCredit = new Object();
while(customerCredit != null){
    customerCredit = itemReader.read();
    counter++;
}
itemReader.close();

运行上述代码片段后,计数器等于 1,000。如果上面的代码将返回的值放入一个列表中,结果将与示例customerCredit完全相同。JdbcTemplate但是,它的最大优点ItemReader 是它允许“流式传输”项目。该read方法可以调用一次,可以用 写出项目ItemWriter,然后可以用 获取下一个项目 read。这允许项目的读取和写入在“块”中完成并定期提交,这是高性能批处理的本质。此外,它很容易配置为注入 Spring Batch Step。

以下示例展示了如何在 Java 中将ItemReader 注入到 Step中:

Java 配置

@Bean
public JdbcCursorItemReader<CustomerCredit> itemReader() {
    return new JdbcCursorItemReaderBuilder<CustomerCredit>()
            .dataSource(this.dataSource)
            .name("creditReader")
            .sql("select ID, NAME, CREDIT from CUSTOMER")
            .rowMapper(new CustomerCreditRowMapper())
            .build();
​
}
附加属性

因为在 Java 中打开游标有很多不同的选项,所以JdbcCursorItemReader可以设置许多属性,如下表所述:

ignoreWarnings确定是否记录 SQLWarnings 或导致异常。默认值为true(意味着记录警告)。
fetchSize当. ResultSet_ ItemReader默认情况下,不给出任何提示。
maxRows设置底层证券ResultSet在任何时候可以容纳的最大行数限制。
queryTimeoutStatement设置驱动程序等待对象运行的秒数。如果超出限制,则抛出DataAccessException 。(有关详细信息,请参阅您的驱动程序供应商文档)。
verifyCursorPosition因为由ResultSet持有的相同ItemReader被传递给RowMapper,所以用户可能会调用ResultSet.next()自己,这可能会导致阅读器的内部计数出现问题。 如果光标位置在调用后与之前不同,则将此值设置为true会引发异常。RowMapper
saveState指示是否应将阅读器的状态保存在 ExecutionContext提供的 中ItemStream#update(ExecutionContext)。默认值为 true.
driverSupportsAbsolute指示 JDBC 驱动程序是否支持在ResultSet. true 对于支持 的 JDBC 驱动程序,建议将其设置为ResultSet.absolute(),因为它可以提高性能,尤其是在处理大型数据集时步骤失败的情况下。默认为false.
setUseSharedExtendedConnection指示用于游标的连接是否应由所有其他处理使用,从而共享同一事务。如果将其设置为false,则游标将使用其自己的连接打开,并且不参与为其余步骤处理启动的任何事务。如果将此标志设置为,true则必须将 DataSource 包装在 an 中 ExtendedConnectionDataSourceProxy,以防止在每次提交后关闭和释放连接。当您将此选项设置为true,用于打开游标的语句是使用“READ_ONLY”和“HOLD_CURSORS_OVER_COMMIT”选项创建的。这允许在步骤处理中执行的事务开始和提交上保持光标打开。要使用此功能,您需要一个支持此功能的数据库和一个支持 JDBC 3.0 或更高版本的 JDBC 驱动程序。默认为false.
HibernateCursorItemReader

就像普通的 Spring 用户对是否使用 ORM 解决方案做出重要决定一样,这会影响他们是否使用 JdbcTemplate或 HibernateTemplate,Spring Batch 用户也有相同的选择。 HibernateCursorItemReader是游标技术的 Hibernate 实现。Hibernate 在批处理中的使用颇具争议。这主要是因为 Hibernate 最初是为了支持在线应用程序样式而开发的。但是,这并不意味着它不能用于批处理。解决这个问题最简单的方法是使用StatelessSession而不是标准会话。这消除了 Hibernate 使用的所有缓存和脏检查,这可能会导致批处理场景中的问题。有关无状态和正常休眠会话之间差异的更多信息,请参阅特定休眠版本的文档。HibernateCursorItemReader允许您声明 HQL 语句并传入 a ,这将在 每次 SessionFactory调用时传回一个项目,以与JdbcCursorItemReader. 以下示例配置使用与 JDBC 阅读器相同的“客户信用”示例:

HibernateCursorItemReader itemReader = new HibernateCursorItemReader();
itemReader.setQueryString("from CustomerCredit");
//为简单起见,假设已经获得sessionFactory。
itemReader.setSessionFactory(sessionFactory);
itemReader.setUseStatelessSession(true);
int counter = 0;
ExecutionContext executionContext = new ExecutionContext();
itemReader.open(executionContext);
Object customerCredit = new Object();
while(customerCredit != null){
    customerCredit = itemReader.read();
    counter++;
}
itemReader.close();

此配置以与 描述的完全相同的方式ItemReader返回对象,假设已为表正确创建了休眠映射文件。'useStatelessSession' 属性默认为 true,但已在此处添加以提醒人们注意打开或关闭它的能力。还值得注意的是,可以使用该 属性设置底层游标的获取大小。与 一样,配置很简单。CustomerCreditJdbcCursorItemReaderCustomersetFetchSizeJdbcCursorItemReader

以下示例显示了ItemReader如何在 Java 中注入 Hibernate:

Java 配置

@Bean
public HibernateCursorItemReader itemReader(SessionFactory sessionFactory) {
    return new HibernateCursorItemReaderBuilder<CustomerCredit>()
            .name("creditReader")
            .sessionFactory(sessionFactory)
            .queryString("from CustomerCredit")
            .build();
}

分页ItemReader实现

使用数据库游标的另一种方法是运行多个查询,其中每个查询获取部分结果。我们将此部分称为页面。每个查询都必须指定起始行号和我们希望在页面中返回的行数。

JdbcPagingItemReader

分页的一种实现ItemReader是JdbcPagingItemReader. JdbcPagingItemReader需要一个PagingQueryProvider负责提供用于检索组成页面的行的 SQL 查询。 由于每个数据库都有自己的策略来提供分页支持,因此我们需要PagingQueryProvider 为每种支持的数据库类型使用不同的策略。还有SqlPagingQueryProviderFactoryBean 一个自动检测正在使用的数据库并确定适当的 PagingQueryProvider实现。这简化了配置,是推荐的最佳实践。

SqlPagingQueryProviderFactoryBean要求您指定一个子句select和一个 from子句。您还可以提供一个可选where子句。这些子句和 requiredsortKey用于构建 SQL 语句。

重要的是对 有一个唯一的键约束,sortKey以保证在执行之间不会丢失任何数据。

read打开阅读器后,它会以与任何其他相同的基本方式将每次调用传回一个项目ItemReader。当需要额外的行时,分页发生在幕后。

以下 Java 示例配置使用与ItemReaders前面显示的基于光标的类似“客户信用”示例:

Java 配置

@Bean
public JdbcPagingItemReader itemReader(DataSource dataSource, PagingQueryProvider queryProvider) {
    Map<String, Object> parameterValues = new HashMap<>();
    parameterValues.put("status", "NEW");
​
    return new JdbcPagingItemReaderBuilder<CustomerCredit>()
                        .name("creditReader")
                        .dataSource(dataSource)
                        .queryProvider(queryProvider)
                        .parameterValues(parameterValues)
                        .rowMapper(customerCreditMapper())
                        .pageSize(1000)
                        .build();
}
​
@Bean
public SqlPagingQueryProviderFactoryBean queryProvider() {
    SqlPagingQueryProviderFactoryBean provider = new SqlPagingQueryProviderFactoryBean();
​
    provider.setSelectClause("select id, name, credit");
    provider.setFromClause("from customer");
    provider.setWhereClause("where status=:status");
    provider.setSortKey("id");
​
    return provider;
}

此配置使用必须指定的ItemReader返回CustomerCredit对象。RowMapper“pageSize”属性确定每次查询运行时从数据库读取的实体数。

'parameterValues' 属性可用于指定Map查询的参数值。如果在where子句中使用命名参数,则每个条目的键应与命名参数的名称匹配。如果您使用传统的“?” placeholder,那么每个条目的 key 应该是占位符的编号,从 1 开始。

JpaPagingItemReader

分页的另一种实现ItemReader是JpaPagingItemReader. JPA 没有类似于 Hibernate 的概念StatelessSession,所以我们不得不使用 JPA 规范提供的其他特性。由于 JPA 支持分页,因此在使用 JPA 进行批处理时这是一个自然的选择。读取每个页面后,实体将分离并清除持久性上下文,以便在处理页面后对实体进行垃圾收集。

JpaPagingItemReader允许您声明 JPQL 语句并 EntityManagerFactory传入. 然后,它在每次调用时传回一个项目,以与任何其他项目相同的基本方式进行读取ItemReader。当需要其他实体时,分页发生在幕后。

Java 配置

@Bean
public JpaPagingItemReader itemReader() {
    return new JpaPagingItemReaderBuilder<CustomerCredit>()
                        .name("creditReader")
                        .entityManagerFactory(entityManagerFactory())
                        .queryString("select c from CustomerCredit c")
                        .pageSize(1000)
                        .build();
}

假设对象具有正确的 JPA 注释或 ORM 映射文件,此配置以与上述完全相同的方式ItemReader返回对象。“pageSize”属性确定每次查询执行时从数据库读取的实体数。CustomerCreditJdbcPagingItemReaderCustomerCredit

数据库 ItemWriters

虽然FlatFile和 XML 文件都有特定的ItemWriter实例,但在数据库世界中并没有完全相同的实例。这是因为事务提供了所有需要的功能。 ItemWriter文件的实现是必要的,因为它们必须像事务性的那样工作,跟踪写入的项目并在适当的时间刷新或清除。数据库不需要此功能,因为写入已包含在事务中。用户可以创建自己的 DAO 来实现ItemWriter接口或使用自定义的 DAOItemWriter这是为通用处理问题而编写的。无论哪种方式,它们都应该毫无问题地工作。需要注意的一件事是通过批处理输出提供的性能和错误处理能力。这在使用 hibernate 时最常见,ItemWriter但在使用 JDBC 批处理模式时可能会遇到相同的问题。批处理数据库输出没有任何固有缺陷,假设我们小心刷新并且数据没有错误。但是,写入时的任何错误都可能导致混淆,因为无法知道是哪个单个项目导致了异常,或者即使任何单个项目负责,如下图所示:

刷新错误

图 4. 刷新错误

如果项目在写入之前被缓冲,则在提交之前刷新缓冲区之前不会引发任何错误。例如,假设每个块写入 20 个项目,第 15 个项目抛出一个DataIntegrityViolationException. 就目前Step 而言,所有 20 项都已成功写入,因为在实际写入之前无法知道是否发生了错误。一旦Session#flush()被调用,缓冲区就会被清空并触发异常。在这一点上,没有什么Step 可以做。事务必须回滚。通常,此异常可能会导致项目被跳过(取决于跳过/重试策略),然后不再写入。但是,在批处理场景中,无法知道是哪个项目导致了问题。发生故障时正在写入整个缓冲区。解决此问题的唯一方法是在每个项目之后刷新,如下图所示:

写入错误

图 5. 写入错误

这是一个常见的用例,尤其是在使用 Hibernate 时,实现的简单准则ItemWriter是在每次调用write(). 这样做可以可靠地跳过项目,Spring Batch 在内部负责处理ItemWriter错误后调用的粒度。

代码位置: github.com/jackssybin/…