实现万行级数据读取优化

xl_echo编辑整理,欢迎转载,转载请声明文章来源。欢迎添加echo微信(微信号:t2421499075)交流学习。 百战不败,依不自称常胜,百败不颓,依能奋力前行。——这才是真正的堪称强大!!html

业务场景:

基于导出的功能上,要求一次性查询10w条数据。可是这个10w的开始值和结束值不固定(好比:startNum = 123; endNum = 100123;)前端

  • 难点一:
    dubbox时间超时规定为1s,服务调用图以下:
    企业微信截图_20190731094754.png
  • 难点二:
    数据封装转换性能消耗较高,目前使用的BeanUtils
  • 难点三:
    并发能力很弱,在分割查询的过程当中,若是有其余的服务进入,很容易致使数据混乱

公司使用的数据库为oracle,目前我本身实现的查询功能总耗时8s。这个时间能不能再次缩短?有没有比较好的方案对数据分割查询?java

对于JDBC batchsize 和 fetchsize的一次尝试

Batch和Fetch两个特性很是重要,Batch至关于JDBC的写缓冲,Fetch至关于读缓冲。在加入这两个特性以后,查询10w条尝试,根据描述,能个提高4倍的时间。参考文章:http://blog.sina.com.cn/s/blog_9f8ffdaf0102x3nf.htmlspring

将代码摘抄以后,修改数据库链接的帐户密码。单独使用检查代码是否可以独立运行,main,报错以下:sql

Exception in thread "main" java.lang.ClassNotFoundException: oracle.jdbc.OracleDriver
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:264)
    at com.example.mybatisplusdemo.temp.Test.fetchRead(Test.java:74)
    at com.example.mybatisplusdemo.temp.Test.main(Test.java:22)复制代码

采坑一:

测试的功能为一个完整的springboot工程,当看到报错的的行数的时候,发现指向的是下面这一行数据库

Class.forName("oracle.jdbc.OracleDriver");复制代码

这个错其实提示很明显,就是找不到驱动包,咱们只须要在maven中添加一个oracle的依赖便可。springboot

<dependency>
    <groupId>com.oracle</groupId>
    <artifactId>ojdbc6</artifactId>
    <version>11.2.0.3</version>
</dependency>复制代码

当我加入依赖以后,一切均可以照常执行了。测试数据我造了10w数据,发现若是使用fetchsize和不使用,性能确实相差4倍左右的样子。同时若是数据多一些,可能效果还会明显一点。可是同时又出现了新的问题:测试环境和生产环境使用的是两套数据库,公司对生产环境保密很严,拿不到生产环境的密码,若是上线会致使功能出现问题。服务器

采坑二:

多环境开发和上线须要屡次修改代码。这里其实就已经卡住了,不能再次往下进行。若是能够这么用的小伙伴能够考虑编写多环境的适配器。微信

反过来想,咱们公司使用的mybatis,应该mybatis就会有对应的方法,并且只会比本身写jdbc要好点。mybatis

mybatis中使用fetchsize的一次尝试

其实冲上面的应用咱们能够发现fetchsize其实的做用其实就是避免一次性将数据从数据库中拿出来, 以致于致使在加载到内存的数据过多,而内存溢出,或者致使缓慢。若是fetchsize的值为1w,是指定服务器一次返回1w条数据,若是总数为10w的话服务器就要发送10次。在mybatis中若是使用fetchsize其实也比较简单,好比在xml文件中须要使用sql的语句上直接加上fetchsize便可。若是不想简单处理,能够本身手写ResultHandler来分批处理结果集。这里直接在xml上添加,示例以下:

<select id="selectAll" resultMap="BaseResultMap" parameterType="java.util.Map" fetchSize="10000">
    select * from
        (select c.*, ROWNUM rn FROM TABLE c where rownum <= #{endNumber})
    where rn >= #{startNumber}
</select>复制代码

  • 实测查询结果

这里使用的fetchsize设置值为1w,若是值越大估计性能提高会不断降低(这里属于博主猜想,没有验证哈,依据就是当不设置的时候就是一次性所有加载,那就至关于无限大,无限大的时候和1w比较值以下表格)

是否使用fetchsize 查询第1次时间 查询第2次时间 查询第3次时间 查询第4次时间 查询第5次时间
1010ms 1269ms 1091ms 1147ms 1028ms
4813ms 4736ms 4800ms 4417ms 4580ms

将fetchsize使用了以后,返回时间为1s左右,可是也只是优化了查询,还能够优化数据的封装。优化到这以后咱们能够看到dubbox超时时间须要放宽。

数据封装优化的思路Dozer --解决难点二

这里并无实际去使用,由于需求中使用的时候发现BeanUtils去转换这一环节能够直接去掉。由于数据在传递的终点不是直接发送到前端,而是发送到controller,这种件若是去掉转换消耗会更小,后面在controller直接使用值会更快。

固然,后期可能会须要使用,由于业务确定还会迭代这一块。只是目前能够省略。

Dozer是一个JavaBean映射工具库。据百度介绍,这是一款转换数据的神器。若是使用的话,须要加入它对应的依赖:

<dependency>
    <groupId>net.sf.dozer</groupId>
    <artifactId>dozer</artifactId>
    <version>5.5.1</version>
</dependency>复制代码

若是要映射的两个对象有彻底相同的属性名,那么一切都很简单。

Mapper mapper = new DozerBeanMapper();
DestinationObject destObject = mapper.map(sourceObject, DestinationObject.class);复制代码

实际应用,项目须要返回VO类的数据,但你在mapper中是使用PO类,返回时须要转换

Mapper announcementDozerMapper =new DozerBeanMapper();
/**
 * @param announcementPo 原PO类的announcement类型
 * @return 返回VO类的announcement类型
 * @description 将announcement的PO类转化为VO类
 **/
private AnnouncementVo doToVo(AnnouncementPo announcementPo){
    if(announcementPo == null) {
        return null;
    }
    AnnouncementVo vo = announcementDozerMapper.map(announcementPo, AnnouncementVo.class);
    return vo;
}复制代码

  • 注意:这里最好不要每次映射对象时都建立一个Mapper实例来工做,这样会产生没必要要的开销。若是你不使用IoC容器(如:spring)来管理你的项目,那么,最好将Mapper定义为单例模式。
public class DozerMapperConstant {
    public static final Mapper dozerMapper = new org.dozer.DozerBeanMapper();
}复制代码

当作完上面这些的时候发现,已经实现了已被的增速。从耗时8s到如今耗时只须要4s,这个是一个进步。固然这里,并不能知足,还须要进一步优化。

当优化完成以后,咱们再一次去整合代码,数据库的查询从原来的分片查询直接更改成一次查询,响应时间为2s,可是这个时候,数据没有分片查询,致使在controller中调用接口的时候的接受数据是一次性接受的。因此10w数据的接受直接就走了rpc,最后报了以下错误:

com.alibaba.dubbo.remoting.TimeoutException: Waiting server-side response timeout.
java.lang.NullPointerException: null复制代码

仔细看是超时,这个时候发现其实又回到了原点,全部的优化在传输的时候又暂用回来了。最后去查看的时候才发现,10w的数据并不能直接经过rpc传输。这样会致使调用失败,并非上面的超时。

  • dubbox传输数据最大值为8M,10w条数据确定大于8M,因此这个时候查询的优化完成以后要再一次优化dubbox之间的传输。

这里遵循不添加中间件,只修改代码哈。这里就不展现代码了,最终采用的办法就是分片请求。

总结:

  • fetchsize解决一次性查询时间慢的问题,性能提高4倍
  • 减除转换,直接传递。了解Dozer,为后期的转化作准备
  • dubbox调用,分片请求
相关文章
相关标签/搜索