需求:把一個500M的txt文件導入mysql數據庫,數據量大概有幾千萬。
1、項目架構
項目采用微服務架構,上傳文件的后臺管理系統是作為應用層,業務處理的作為服務層,應用層和服務層都有多個節點。
如果是單一節點處理,那么讀取500M的文件(可能更大),并把幾百萬的數據要快速的寫入數據庫,是非常有難度的。但是我們可以利用微服務多個節點分散處理,即應用層只讀取數據,讀10000條就放入線程池向服務層發起請求,而服務層有多個節點,可以把應用層的請求平均到多個節點處理,在應用層每個節點先做數據解析,然后插入mysql。
2、服務層處理
2.1 文件讀取
文件讀取我們用BufferedReader,BufferedReader使用裝飾器模式,它的IO行為是每次讀進來8K的數據到緩沖區(當然,緩沖區的大小我們是可以通過構造器修改的),如果需要使用數據的時候,再直接從緩沖區里面拿出數據來使用。
而FileReader的read方法,每調用一次就會read一次file,進行一次IO。不管是多次read還是一次性的read,都不是很優雅的在read文件的方式。多次read必然會產生多次IO,一次性的read如果遇到很大的文件,對內存是極不友好的。
所以BufferedReader既能提高的讀取速度,又節省了IO的次數,是一種比較優雅的讀取文件的方式。
BufferedWriter和FIleWriter同理。
研究一下BufferedReader的源碼,就會發現,BufferedReader中對文件的讀取還是通過FileReader來實現的,BufferedReader只是對其讀取到的數據做一下緩沖,api如下。其中buffer操作的api和java nio中對buffer的操作類似。
有緩沖區 VS 沒有緩沖區
- 沒有緩沖區時,每次讀取操作都會導致一次文件讀取操作(就是告訴操作系統內核我要讀這個文件的這個部分,麻煩你幫我把它取過來)。
- 有緩沖區時,會一次性讀取很多數據,然后按要求分次交給上層調用者。
讀取塊大小通常是按最適合硬件的大小來讀的,因為對于硬件來說,一次讀取一塊連續數據(比如 1K)和一次讀取一個字節需要的時間幾乎是一樣的(都是一次讀操作,只是最終提交的數據量有差異),所以帶緩沖的 I/O 和不帶緩沖的相比效率差異是非常顯著的
我們這里的需求是順序讀取,如果是隨機讀取,則使用RandomAccessFile。
所謂隨機讀取,就是說我們需要自由訪問文件的任意位置(指定位置讀,指定位置寫),所以如果需要訪問文件的部分內容,RandomAccessFile將是更好的選擇。所以當我們要下載一個大文件時,可以通過多線程使用RandomAccessFile來實現。同樣的,對于文件的切割、合并,使用RandomAccessFile效率都會很高。
2.2 業務處理
BufferedReader讀取文件后,每讀10000行,就丟入線程池,然后調用服務層處理,應用層這里不做任何業務邏輯處理,因為應用層必然是用一個節點處理業務,但是=我們通過http調用服務層后,是可以通過多個節點處理的,這樣就可以提升處理效率。
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 50, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
BufferedReader bufferedReader = null;
try {
bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream("E:\\code\\esg-cemp-core-nonactivity-01394067\\logs\\system\\system2.log"), StandardCharsets.UTF_8));
List<String> lines = new ArrayList<>();
String lineTXT="";
long count = 0;
while ((lineTXT = bufferedReader.readLine()) != null) {
if(count % 10000 == 0){
executorService.execute(new Runnable() {
@Override
public void run() {
//調用微服務
System.out.println("------調用微服務--------");
}
});
lines.clear();
}
lines.add(lineTXT);
count++;
}
if(lines.size()>0){
//調用微服務
System.out.println("-------調用微服務-------");
}
//及時關閉線程池,對于這種使用不是很頻繁的線程池使用完畢以后,可以及時關閉以節省資源
//shutdown關閉以后,就不能再提交任務了,
threadPoolExecutor.shutdown();
} catch (Exception e) {
e.printStackTrace();
}
3、業務層處理
3.1 業務處理
業務處理比較簡單,就是把String 切割成多個字段,然后變換成PO對象。
3.2 mysql 插入
-
方法一:mybatis foreach
foreach的作用是構建in條件,通過foreach可以在SQL語句中進行迭代一個集合。
優點是使用方便;
缺點是每條sql是有長度限制的,所以list的數量跟表字段多少直接關聯;activityProductUserFlowPoMapper.insertList(list2);
<insert id="insertList" > insert into t_non_standard_act_user_flow (act_pro_id, user_id, mobile, point, status, extend) values <foreach collection="list" separator="," item="item"> (#{item.actProId,jdbcType=BIGINT}, #{item.userId,jdbcType=VARCHAR}, #{item.mobile,jdbcType=VARCHAR}, #{item.point,jdbcType=INTEGER}, #{item.status,jdbcType=TINYINT}, #{item.extend,jdbcType=VARCHAR}) </foreach> </insert>
-
方法二:mybatis batch
Mybatis內置的ExecutorType有3種,默認的是simple,該模式下它為每個語句的執行創建一個新的預處理語句,單條提交sql;而batch模式重復使用已經預處理的語句,并且批量執行所有更新語句。SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); Mapper userMapper = sqlSession.getMapper(Mapper.class); int size = 10000; try { for (int i = 0; i < size; i++) { User user = new User(); user.setName(String.valueOf(System.currentTimeMillis())); fooMapper.insert(foo); if (i % 1000 == 0 || i == size - 1) { //手動每1000個一提交,提交后無法回滾 session.commit(); //清理緩存,防止溢出 session.clearCache(); } } } catch (Exception e) { //沒有提交的數據可以回滾 session.rollback(); } finally { session.close(); }
需要在jdbc連接url上追加rewriteBatchedStatements=true,否則不起作用
-
方法三:jdbc batch
采用PreparedStatement.addBatch()方式實現,發送的是預編譯后的SQL語句,執行效率高。Long start = System.currentTimeMillis(); DruidDataSource dataSource = SpringUtils.getBean("dataSource"); //從連接池中獲取connection DruidPooledConnection conn =dataSource.getConnection(); conn.setAutoCommit(false); try { String sql = "insert into act_user_flow (act_pro_id, user_id, mobile, point, status) values(?,?,?,?,?)"; PreparedStatement ps = conn.prepareStatement(sql); for (int i = 0; i < list.size; i++) { ps.setLong(1,list[i].getActProId()); ps.setString(2,list[i].getUserId()); ps.setString(3,list[i].getMobile()); ps.setInt(4,list[i].getPoint()); ps.setInt(5,DuobaoGoodEnum.ACTIVITY_PRODUCT_ONLINE.getCode()); ps.addBatch(); //小批量提交,避免OOM if((i+1) % 1000 == 0) { ps.executeBatch(); ps.clearBatch(); } } //提交剩余的數據 ps.executeBatch(); conn.commit(); } catch (SQLException throwables) { throwables.printStackTrace(); } finally { //使用完以后放回連接池 conn.close(); } System.out.println("insert cost time = " + (System.currentTimeMillis()-start));
需要在jdbc連接url上追加rewriteBatchedStatements=true,否則不起作用
-
性能比較
同個表插入一萬條數據時間近似值:
JDBC BATCH 1.1秒左右 > Mybatis BATCH 2.2秒左右 >Mybatis foreach 4.5秒左右方法二和方法三利用的是mysql的批量提交,需要在jdbc連接url上追加rewriteBatchedStatements=true,如下
jdbc:mysql://xxx.com.cn/xxx?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&rewriteBatchedStatements=true
MySQL JDBC驅動在默認情況下會無視executeBatch()語句,把我們期望批量執行的一組sql語句拆散,一條一條地發給MySQL數據庫,批量插入實際上是單條插入,直接造成較低的性能。
只有把rewriteBatchedStatements參數置為true, 驅動才會幫你批量執行SQL
另外這個選項對INSERT/UPDATE/DELETE都有效
擴展:
- jdbc連接url上allowMultiQueries=true的作用:
1.可以在sql語句后攜帶分號,實現多語句執行。
2.可以執行批處理,同時發出多個SQL語句。
- $ 和#的區別
#{}:是以預編譯的形式,將參數設置到sql語句中;PreparedStatement;防止sql注入
${}:取出的值直接拼裝在sql語句中;會有安全問題;
如下所示:
select * from tbl_employee where id=${id} and last_name=#{lastName}
Preparing: select * from tbl_employee where id=2 and last_name=?
如果id為 '2 or 1=1',則會發生數據泄露,這就是sql注入。
Preparing: select * from tbl_employee where id=2 or 1=1 and last_name=?
- 獲取DruidDataSource當前活躍連接數
//當前活躍的,即正在被使用的連接數
System.out.println(dataSource.getActiveCount()); ——活躍連接都在activeConnections(Map)中
//最大連接數
System.out.println("========="+dataSource.getMaxActive());
//poolingCount值代表剩余可用的連接數,每次從末尾拿走連接
System.out.println(dataSource.getPoolingCount()); ——剩余可用連接都在connections(數組)中
//activeCount + poolingCount只能小于等于maxActive
activeCount + poolingCount <= maxActive