1. 程式人生 > >實現萬行級資料讀取優化

實現萬行級資料讀取優化

xl_echo編輯整理,歡迎轉載,轉載請宣告文章來源。歡迎新增echo微信(微訊號:t2421499075)交流學習。 百戰不敗,依不自稱常勝,百敗不頹,依能奮力前行。——這才是真正的堪稱強大!!


業務場景:

基於匯出的功能上,要求一次性查詢10w條資料。但是這個10w的開始值和結束值不固定(比如:startNum = 123; endNum = 100123;)

  • 難點一: dubbox時間超時規定為1s,服務呼叫圖如下: 企業微信截圖_20190731094754.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工程,當看到報錯的的行數的時候,發現指向的是下面這一行

Class.forName("oracle.jdbc.OracleDriver");

這個錯其實提示很明顯,就是找不到驅動包,我們只需要在maven中新增一個oracle的依賴即可。

<dependency>
    <groupId>com.oracle</groupId>
    <artifactId>ojdbc6</artifactId>
    <version>11.2.0.3</version>
</dependency>

當我加入依賴之後,一切都可以照常執行了。測試資料我造了10w資料,發現如果使用fetchsize和不使用,效能確實相差4倍左右的樣子。同時如果資料多一些,可能效果還會明顯一點。但是同時又出現了新的問題:測試環境和生產環境使用的是兩套資料庫,公司對生產環境保密很嚴,拿不到生產環境的密碼,如果上線會導致功能出現問題。

採坑二:

多環境開發和上線需要多次修改程式碼。這裡其實就已經卡住了,不能再次往下進行。如果可以這麼用的小夥伴可以考慮編寫多環境的介面卡。

反過來想,我們公司使用的mybatis,應該mybatis就會有對應的方法,而且只會比自己寫jdbc要好點。

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 &lt;= #{endNumber})
    where rn &gt;= #{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呼叫