1

本文主要研究一下在对jdbc进行大数据量读写相关异常的防御措施

读操作

一次性select大量的数据到内存,最容易出现的是OOM的异常,这个时候可以从时间和数据大小两个维度进行限制

限制数据量

1.分页查询

对于普通的功能,分页操作是必须的,也是解决这个问题最简单的方法,在相关功能实现的时候,要对生产的数据量进行提前预估,确定好相应的分页数据量。

2.maxRows

jdbc可以设置statement的maxRows,用来限制该statment能够拉取的所有数据的最大值,超过则丢弃。不同的数据的jdbc driver实现可能不一样,比如pg的jdbc driver是会将maxRows和fetchSize做比较,取最小的值做为limit参数值来去查询。

这个参数如果要对不同的sql来做通用设置,可能不是太好设置,稍微有点野蛮和暴力,可能某些某些查询出来的数据的列数不多也占用不了太多内存。需要单独设置。但是现在实际功能实现上很少直接使用jdbc,而是使用jpa或mybatis,因此具体就需要看jpa或mybatis有没有暴露这个参数值给你设置。但是对于通用的sql服务来说,非常有必要设置下maxRows,比如不超过2w等,来进行兜底的防范。

3.fetchSize

jdbc提供fetchSize参数来设置每次查询按fetchSize分批获取。不同的数据库的jdbc driver实现不一样。

比如mysql需要url设置useCursorFetch=true,且设置了statement的fetchSize,这样才真正的批量fetch,否则是全量拉取数据。在fetch模式下,executeQuery方法不会去获取第一批数据,而是在resultSet的next方法中实现。

比如pg的话在executeQuery方法默认会拉取第一批fetchSize的数据并返回,之后resultSet的next()方法根据需要再去fetch

使用fetchSize来避免OOM的话有个限制条件,就是需要自己在遍历resultSet的过程中边遍历数据,边处理数据。如果不是边遍历边处理,还是把结果集循环添加到list中返回,在不是reactive模式的编程范式下,这个fetchSize也就失去效果了,因为最后你还是在内存中堆积所有的数据集再去处理,因此终究会有OOM的风险

限制查询时间

限制时间的话,有多个维度:

1.connection的socketTimeout

这个是jdbc中最底层的连接socket的timeout参数设定,可以用来防止数据库由于网络原因或自身问题重启导致连接阻塞,这个是非常有必要设置的,一般是在连接url中设置

比如mysql

jdbc:mysql://localhost:3306/ag_admin?useUnicode=true&characterEncoding=UTF8&connectTimeout=60000&socketTimeout=60000
比如pg,pg的单位与mysql不同,mysql是毫秒,而pg是秒
jdbc:postgresql://localhost/test?user=fred&password=secret&&connectTimeout=60&socketTimeout=60
但是现在一般使用的是数据库连接池,因此这个不设置,通过设置连接池相关参数也是可以。

2.statement的queryTimeout

这个主要是设置statement的executeQuery的执行超时时间,即从client端发出查询指令到接收到第一批数据的超时时间,通常是通过timer来实现的。

但是这个在不同的数据库的jdbc driver的实现上有所不同,比如在fetch模式下mysql的executeQuery不会获取第一批数据,而pg则会顺带拉取第一批数据再返回。这个参数只有在不是fetch模式下,即一次性查询所有数据,才相对符合语义。如果是fetch模式,该超时时间限制不了后续几批数据的拉取超时,他们只能取决于connection的socketTimeout参数。

mybatis可以通过defaultStatementTimeout参数来设置该值
jpa可以通过query hit来设置

@QueryHints(@QueryHint(name = org.hibernate.jpa.QueryHints.SPEC_HINT_TIMEOUT, value = "1000"/*ms**/))
    List<DemoUser> findAll();
jdbc template可以通过参数来设置
    @Bean(name = "pgJdbcTemplate")
    public JdbcTemplate pgJdbcTemplate(
            @Qualifier("pgDataSource") DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        jdbcTemplate.setQueryTimeout(10*1000);
        jdbcTemplate.setMaxRows(10*1000);
        return jdbcTemplate;
    }

3.transaction的timeout

在现实的编程中实现某个业务功能可能在一个事务中调用了很多个statement的查询,transaction可以以事务为单位来限制这批操作的超时间。

可以设置全局的超时时间

    @Bean
    @Qualifier("pgTransactionManager")
    PlatformTransactionManager pgTransactionManager() {
        JpaTransactionManager transactionManager = new JpaTransactionManager(pgEntityManagerFactory().getObject());
        transactionManager.setDefaultTimeout(60 /*seconds*/);
        return transactionManager;
    }
也可以在transactional注解中单独设置,比如
@Transactional(timeout=5) /**5 seconds*/
public List<DemoUser> findAll();

4.connection的占用时间

在使用连接池来进行数据库操作的时候,一般的连接池都会提供连接检测的功能,比如在borrow的时候验证下连接是否是ok的

另外还提供对连接占用的超时suspect和abandon操作,来检测连接泄露,如果上面那些操作都没有设置或(默认)设置的值太大不合理,那么这个检测就是除了socketTimeout外的兜底操作了。如果连接被借出超过指定时间未归还,则判定为连接泄露,则会强制abandon,即close掉连接,非常暴力,但也非常有用,防止线程阻塞在数据库操作最后导致服务504或502

写操作

类似fetchSize,对于大量数据的插入或更新操作,jdbc提供了batch方法,用来批量操作。因此对于大规模的数据操作时要注意内存中堆积的数据量,记得分批释放调用。比较适合使用原生的jdbc来操作,jpa的save方法还是现在内存中对接了大量对象,在flush的时候才执行批量和释放。

小结

对于jdbc的大量数据读写操作,要额外注意内存中对象的堆积,防止OOM。另外对于数据库操作的超时时间也要额外注意设置,防止服务器线程阻塞导致无法提供服务。

操作 类别 参数 备注
数量 pageSize 分页查询
数量 maxRows 限制一次或分fetch查询的所有数据量上限
数量 fetchSize 限制statement的query及result的next每次分批查询的大小
时间 connection socketTimeout 底层socket连接的读超时
时间 statement queryTimeout 限制statement的query超时
时间 transaction timeout 限制事务执行的超时时间
时间 connection remove abandon timeout 限制连接借用超时时间
数量 batch execute 分批执行

doc


codecraft
11.9k 声望2k 粉丝

当一个代码的工匠回首往事时,不因虚度年华而悔恨,也不因碌碌无为而羞愧,这样,当他老的时候,可以很自豪告诉世人,我曾经将代码注入生命去打造互联网的浪潮之巅,那是个很疯狂的时代,我在一波波的浪潮上留下...


引用和评论

0 条评论