1. 程式人生 > >JDBC postgresql大資料量流式讀取

JDBC postgresql大資料量流式讀取

前言:

最近做資料同步,需要從PostgreSql獲取資料,發現一旦資料比較多,那麼讀取的速度非常慢,並且記憶體佔用特別多&GC不掉。

程式碼樣例:

為了方便講解,下面寫了事例程式碼,從b2c_order獲取資料,這個資料表6G左右。

複製程式碼
package com.synchro;

import java.sql.*;

/**
 * Created by qiu.li on 2015/10/16.
 */
public class Test {

    public static void main(String[] args) {
        Connection conn = null;
        try {
            Class.forName("org.postgresql.Driver");
            conn = DriverManager.getConnection("jdbc:postgresql://***.qunar.com:5432/database", "username", "password");
            String sql = "select * from mirror.b2c_order";
            PreparedStatement ps = conn.prepareStatement(sql);
            ResultSet rs = ps.executeQuery();
            int i = 0;
            while (rs.next()) {
                i++;
                if (i % 100 == 0) {
                    System.out.println(i);
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

現象:

在Idea執行程式碼,發現卡死,並且佔用大量的記憶體


解決方案:

然後我決定開始逐步除錯,跟蹤程式碼:

第一步、我發現是在執行executeQuery方法的時候卡住的

第二步、是在執行AbstractJdbc2Statement.executeWithFlags方法卡住的

第三步、繼續跟蹤,並在網路上檢視可能引起的原因是和設定fetchSize引數相關,所以我設定了fetchSize,奇葩的是沒有生效

第四步、sendQuery,sendOneQuery方法,在這裡發現了問題,好在程式碼不太多,我就都貼出來了:

複製程式碼
        boolean usePortal = (flags & 8) != 0 && !noResults && !noMeta && fetchSize > 0 && !describeOnly;
        boolean oneShot = (flags & 1) != 0 && !usePortal;
        int rows;
        if(noResults) {
            rows = 1;
        } else if(!usePortal) {
            rows = maxRows;
        } else if(maxRows != 0 && fetchSize > maxRows) {
            rows = maxRows;
        } else {
            rows = fetchSize;
        }
複製程式碼

可見是usePortal是true,那麼fetchSize才會生效。

boolean usePortal = (flags & 8) != 0 && !noResults && !noMeta && fetchSize > 0 && !describeOnly;

那麼咱們逐一看一下這些條件:

  • !noResults表示這個SQL不需要返回任何結果,這個肯定等於true,因為所有的select都會要求返回結果
  • !noMeta表示這個SQL不需要返回元資料,這個肯定等於true,因為select都要求返回元資料,供後續的resultSet.get使用
  • !fetchSize大於0,這個不說了,自然是true
  • !describeOnly,這個只有在desc table這樣的語句的時候,才會是false,對於select,也是true

那麼,試下的唯一的可能導致usePortal為true的原因就是 flags & 8這個值是true。。(我想說這種寫法很別緻,tmd,設定flags的時候肯定是flags=flag|8,後來發現新的驅動修改了這種寫法)

繼續往上翻,看看什麼時候才會執行flags = flags | 8 這個程式碼了,因為只有這個程式碼被執行過,才會導致上面這個條件為true

        if(this.fetchSize > 0 && !this.wantsScrollableResultSet() && !this.connection.getAutoCommit() && !this.wantsHoldableResultSet()) {
            flags |= 8;
        }

其中:wantsHoldableResultSet()程式碼直接返回的false,所以,不考慮這個。

那麼,wantsScrollableResultSet()返回false,並且connection.getAutoCommit()返回false,才會導致fetchSize生效。wantsScrollableResultSet()這個方法的程式碼為:

protected boolean wantsScrollableResultSet() {
        return resultsettype != 1003; //老程式碼,看到這裡我真想死,1003是啥?好在偶然的機會看見了新的Postgresql驅動,使用ResultSet.TYPE_FORWARD_ONLY表示1003
}

至此,問題終於被定位:

1、如果connection不是自動提交事務的,那麼,fetchSize將生效(非預設)

2、如果statement是TYPE_FORWARD_ONLY的,那麼,fetchSize也將生效(預設)

結論

如果想fetchSize生效,必須保證connection是autocommit = false的,並且,statement為1003(forward_only)的:

conn.setAutoCommit(false);
final Statement statement = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.FETCH_FORWARD);

另外,不帶引數的conn.createStatement(),其預設就是TYPE_FORWARD_ONLY。所以,一般情況下,如果想fetchsize生效,只須設定autocommit為flase,也就是需要手工去管理事務。預設的原始碼如下:

    public Statement createStatement() throws SQLException {
        return this.createStatement(1003, 1007); //有興趣的同學可以繼續跟蹤看看,1003就是resultsettype
    }

程式碼:

那麼修改程式碼如下:

複製程式碼
package com.synchro;

import java.sql.*;

/**
 * Created by qiu.li on 2015/10/16.
 */
public class Test {

    public static void main(String[] args) {
        Connection conn = null;
        try {
            Class.forName("org.postgresql.Driver");
            conn = DriverManager.getConnection("jdbc:postgresql://***.qunar.com:5432/datasource", "username", "password");
            conn.setAutoCommit(false); //並不是所有資料庫都適用,比如hive就不支援,orcle不需要
            String sql = "select * from mirror.b2c_order";
            PreparedStatement ps = conn.prepareStatement(sql);
            ps.setFetchSize(1000); //每次獲取1萬條記錄
            //ps.setMaxRows(1000);
            ResultSet rs = ps.executeQuery();
            int i = 0;
            while (rs.next()) {
                i++;
                if (i % 100 == 0) {
                    System.out.println(i);
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

這次再一次執行,發現根本不卡。

感悟:類似這種問題都的慢慢跟蹤程式碼,更重要的是身邊需要有同事可以相互討論,形成氛圍,因為這個過程十分乏味,自己很難堅持下來。