欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

实现万行级数据读取优化

程序员文章站 2022-04-09 15:30:11
xl_echo编辑整理,欢迎转载,转载请声明文章来源。欢迎添加echo微信(微信号:t2421499075)交流学习。 百战不败,依不自称常胜,百败不颓,依能奋力前行。——这才是真正的堪称强大!! #### 业务场景: 基于导出的功能上,要求一次性查询10w条数据。但是这个10w的开始值和结束值不固 ......
xl_echo编辑整理,欢迎转载,转载请声明文章来源。欢迎添加echo微信(微信号:t2421499075)交流学习。 百战不败,依不自称常胜,百败不颓,依能奋力前行。——这才是真正的堪称强大!! --- #### 业务场景: 基于导出的功能上,要求一次性查询10w条数据。但是这个10w的开始值和结束值不固定(比如:startnum = 123; endnum = 100123;) * 难点一: dubbox时间超时规定为1s,服务调用图如下: ![企业微信截图_20190731094754.png](https://gper.club/server-img//mdeditors/2019/7/a09d40fba2aa42389b5a7822f40a57ca.png) * 难点二: 数据封装转换性能消耗较高,目前使用的beanutils * 难点三: 并发能力很弱,在分割查询的过程中,如果有其他的服务进入,很容易导致数据混乱 公司使用的数据库为oracle,目前我自己实现的查询功能总耗时8s。这个时间能不能再次缩短?有没有比较好的方案对数据分割查询? #### 对于jdbc batchsize 和 fetchsize的一次尝试 batch和fetch两个特性非常重要,batch相当于jdbc的写缓冲,fetch相当于读缓冲。在加入这两个特性之后,查询10w条尝试,根据描述,能个提升4倍的时间。参考文章:http://blog.sina.com.cn/s/blog_9f8ffdaf0102x3nf.html 将代码摘抄之后,修改数据库连接的账户密码。单独使用检查代码是否能够独立运行,main,报错如下: ``` 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工程,当看到报错的的行数的时候,发现指向的是下面这一行 ```java class.forname("oracle.jdbc.oracledriver"); ``` 这个错其实提示很明显,就是找不到驱动包,我们只需要在maven中添加一个oracle的依赖即可。 ```xml com.oracleojdbc611.2.0.3 ``` 当我加入依赖之后,一切都可以照常执行了。测试数据我造了10w数据,发现如果使用fetchsize和不使用,性能确实相差4倍左右的样子。同时如果数据多一些,可能效果还会明显一点。但是同时又出现了新的问题:测试环境和生产环境使用的是两套数据库,公司对生产环境保密很严,拿不到生产环境的密码,如果上线会导致功能出现问题。 ###### 采坑二: 多环境开发和上线需要多次修改代码。这里其实就已经卡住了,不能再次往下进行。如果可以这么用的小伙伴可以考虑编写多环境的适配器。 > 反过来想,我们公司使用的mybatis,应该mybatis就会有对应的方法,而且只会比自己写jdbc要好点。 #### mybatis中使用fetchsize的一次尝试 其实冲上面的应用我们可以发现fetchsize其实的作用其实就是避免一次性将数据从数据库中拿出来, 以至于导致在加载到内存的数据过多,而内存溢出,或者导致缓慢。如果fetchsize的值为1w,是指定服务器一次返回1w条数据,如果总数为10w的话服务器就要发送10次 。 在mybatis中如果使用fetchsize其实也比较简单,比如在xml文件中需要使用sql的语句上直接加上fetchsize即可。如果不想简单处理,可以自己手写resulthandler来分批处理结果集 。这里直接在xml上添加,示例如下: ```xml ``` * 实测查询结果 > 这里使用的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映射工具库。据百度介绍,这是一款转换数据的神器。如果使用的话,需要加入它对应的依赖: ```xml net.sf.dozerdozer5.5.1 ``` 如果要映射的两个对象有完全相同的属性名,那么一切都很简单。 ```java mapper mapper = new dozerbeanmapper(); destinationobject destobject = mapper.map(sourceobject, destinationobject.class); ``` 实际应用,项目需要返回vo类的数据,但你在mapper中是使用po类,返回时需要转换 ```java 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定义为单例模式。 ```java 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调用,分片请求