實現萬行級數據讀取優化

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調用,分片請求
相關文章
相關標籤/搜索