1. 程式人生 > 實用技巧 >多條資料批量插入優化方案

多條資料批量插入優化方案

業務背景描述:

​ 主資料同步:呼叫主資料查詢介面,返回json字串,包含上萬條資料資訊。將所有資料資訊提取出來並插入指定資料表中。

​ tips:

1.要求資料同步介面為定時方法(比如每晚12點呼叫一次主資料介面查詢主資料),進行資料的同步更新
2.主資料基本不會發生變更,每天可能會有少量更新和新增資訊

此業務比較簡單,然後之前的程式碼是這樣實現

呼叫介面後獲得主資料資訊--> json 字串,然後轉為json物件,獲取所有主資料資訊
然後將主資料資訊轉為json陣列
到這裡json陣列中每一個元素就是要同步的資料,大概有上萬條
然後遍歷json陣列,取出每個json陣列的id,根據id去資料庫中查詢是否已經存在此條資料
(id為唯一主鍵)
然後進行判斷,如果查到此資料,說明已經存在,則執行修改操作
如果沒有查到,說明此資料之前不存在則執行新增操作

問題:

資料不算多,但是進行測試的時候,使用上述方法,此介面執行了5分鐘!!!
原因分析:
	有多少條資料就遍歷多少次,上萬條資料不算多,已經執行了5分鐘,如果資料大批量則會執行更長的時間。並且每次遍歷的邏輯比較冗餘,先是去資料庫中查是否存在,存在則執行修改
	
	可想而知:
		第一次同步的時候,資料表中完全是空的,所以全都是插入操作
		以後的每次同步,基本都是修改操作,因為之前提到過,每天主資料可能只有少量修改	  和新增
	所以基本後面每次呼叫此介面都是修改,效率可想而知

雖然此介面是凌晨呼叫,前人做的時候可能覺得效率快慢無所謂

但是此資料用到的頻率過高,比如其他介面開發的時候需要最新的主資料,就需要寫個測試介面去更新一下主資料,但是更新了5分鐘 實在太煩,所以很有必要優化一下的

正好業務需要,在其他專案中也要寫一遍主資料同步的邏輯,所以直接過了優化

優化的思路過程,如下

一、命中資料優化

每次都遍歷都去查詢一次資料,然後再做修改操作。

先從資料表中查詢出所有的id,因為id是唯一id,所以將查出的所有id用Set儲存起來,

同時也利用set集合查詢快的優點,然後同樣遍歷陣列

只不過遍歷的時候不是根據id去資料庫中查詢了,而是去set集合中查詢

//虛擬碼----------------------------------------
//查出所有的id
HashSet<Integer> idSet = userMapper.findAllId();

for(Json json:JsonArray){
        
        if(idSet.contains(json.get("id"))){
            //執行修改

            //從set集合中刪除此元素
            idSet.remove(json.get("id"));
        }
        //執行新增
        
    }

這樣只是將資料庫命中資料做了優化,但還是要每次遍歷都執行修改

經測試沒有顯著提升

二、批量插入優化1

想過做批量修改的優化,但是可能會得不償失,效率大可能性也不會提升

所以從根本上解決問題,每次都將資料表中的資料進行刪除,然後全部執行新增即可

這樣簡單直接,因為本身修改就很慢

並且有事務的支援,即使刪除後 新增資料失敗,也會進行回滾

可以保證資料的有效性

所以接下來的優化都是新增的優化

新增優化無非就是

1.使用批量新增, insert into 表名 values(值1,值2..),(),(),....減少連線資料庫的次數
2.插入時候保證主鍵的有序性,可以提高插入效率(因為主資料中的id本身就是無序的,再排序感覺沒必要)
3.使用原生jdbc進行插入,因為框架本來封裝的邏輯會影響效率

結合各種原因,選擇使用第一種優化

虛擬碼;

//定義集合用於批量新增
    List<User> list = new LinkedList<>();

    for(Json json:JsonArray){
        
        //遍歷將資料封裝進實體中
        list.add(new User(json));
        
        while (list.size()==500){
            //當集合中滿500個元素的時候,執行批量新增
            userMapper.add(list);
            
            //新增完成將集合清空,用於下一次批量新增
            list.clear();
        }

    }
    
    //遍歷完成之後,如何處理不滿500條的資料?

tips:

1.在User中已經做了賦值操作
2.因為集合要進行頻繁的插入操作,所以選擇插入資料較快的LinkedList
3.這樣每500個元素批量插入一次,最後肯定有不滿500條的資料沒有執行插入,如何處理?

三、批量插入優化2

關於處理最後一次不滿500條的資料,

想過幾個方案,比如根據總條數和每次插入的條數計算出總共處理的次數

然後最後一次插入的時候做一些處理

也想過在虛擬機器退出的時候,做一些處理操作,見下面程式碼

public static void main(String[] args) {

        //模擬資料
        LinkedList<Integer> list = new LinkedList<>();
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }

        //定義集合用於批量新增
        List<Integer> addList = new LinkedList<>();

        for (Integer i : list) {
            addList.add(i);
            while (addList.size()==3){
                System.out.println("addList = " + addList);
                addList.clear();
            }
        }

        //虛擬機器退出的時候,處理集合中殘餘資料
        Runtime.getRuntime().addShutdownHook(new Thread(() ->
                System.out.println("addList = " + addList)
        ));
    }

列印結果:

addList = [0, 1, 2]
addList = [3, 4, 5]
addList = [6, 7, 8]
addList = [9]

這樣確實能處理到所有的資料,我可真是異想天開,虛擬機器退出這個不適用啊

其實很簡單,馬上就想到了方案

每次都是滿500條才進行批量新增,然後批量新增成功才執行清空集合操作
那麼是不是不滿500條就不會新增,也就不會清空集合
並且用於批量新增的集合是定義在迴圈外面的
所以迴圈結束後集閤中肯定有殘餘資料

見下面程式碼:

public static void main(String[] args) {

        //模擬資料
        LinkedList<Integer> list = new LinkedList<>();
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }

        //定義集合用於批量新增
        List<Integer> addList = new LinkedList<>();

        for (Integer i : list) {
            addList.add(i);
            while (addList.size() == 3) {
                System.out.println("addList = " + addList);
                addList.clear();
            }
        }

        System.out.println("addList = " + addList);

    }

效果是一樣的,想複雜了

所以處理起來就簡單多了

	//定義集合用於批量新增
    List<User> list = new LinkedList<>();

    for(Json json:JsonArray){
        
        //遍歷將資料封裝進實體中
        list.add(new User(json));
        
        while (list.size()==500){
            //當集合中滿500個元素的時候,執行批量新增
            userMapper.add(list);
            
            //新增完成將集合清空,用於下一次批量新增
            list.clear();
        }
    }
    
    //遍歷完成之後,處理不滿500條的資料
	userMapper.add(list);

因為本身就是比較簡單的邏輯,稍微動一點心思,幾行程式碼能大大提高效率

最後經測試,之前舊的方案要執行5分鐘

此優化之後的方案只要執行 20 - 30秒,快的時候可以到15 - 18秒

效果顯而易見!!!

四、批量插入優化3

為什麼還有優化3呢,因為是這樣的,下面呢有一個需求 要求要將一個集合中的所有元素插入到資料表中,該集合的元素共有一萬多個

用上面的方法也可以實現,並且也是幾行程式碼的事情,也比較簡單

但是!!!

本著不安分,閒著蛋疼的心思,想著還能不能繼續優化?

上述的方案實現起來確實方便,但是同樣要去遍歷一萬多次,

如果資料量大了 總感覺這樣不妥?怎麼辦呢?

先看下面程式碼

package com.liqiliang.ssm.service.impl;

@Service
@Transactional
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public boolean add() {

        List<User02> list = new ArrayList<>();

        //模擬10000條資料
        for (int i = 1; i <= 10000; i++) {
            list.add(new User02(i, "a"));
        }

        System.out.println("list.size() = " + list.size());


        int count = 3000;                   //一個執行緒處理3000條資料
        int listSize = list.size();         //資料集合大小
        int runSize = listSize%count==0?listSize/count:listSize/count+1;  //開啟的執行緒數(處理的次數)
        List<User02> newlist = null;       //存放每個執行緒的執行資料

        ExecutorService executor = Executors.newFixedThreadPool(runSize);      //建立一個執行緒池,數量和開啟執行緒的數量一樣

        //迴圈建立執行緒
        for (int i = 0; i < runSize; i++) {
            //計算每個執行緒執行的資料
            if ((i + 1) == runSize) {
                int startIndex = (i * count);
                int endIndex = list.size();
                newlist = list.subList(startIndex, endIndex);
            } else {
                int startIndex = (i * count);
                int endIndex = (i + 1) * count;
                newlist = list.subList(startIndex, endIndex);
            }

            //呼叫方法處理資料
            method(newlist,executor);

        }

        //執行完關閉執行緒池
        executor.shutdown();


        return true;
    }


    private void method(List<User02> list,ExecutorService executor) {

        Thread thread = new Thread(() -> {

            userMapper.add(list);

            //打印出每次處理的資料
            System.out.println("list = " + list);
        });

        //執行執行緒
        executor.execute(thread);

    }
}

就不多囉嗦了,直接說明結果吧

上述方法 用postman呼叫  耗時1307ms

tips:

這是將快取清除之後 測得的資料,如果不清除快取,執行時間更短,有5ms和21ms,肯定不能作為參考資料

不進行任何優化的遍歷方法,一條一條插入

@Override
    public boolean add() {

        List<User02> list = new ArrayList<>();

        //模擬10000條資料
        for (int i = 1; i <= 10000; i++) {
            list.add(new User02(i, "a"));
        }

        System.out.println("list.size() = " + list.size());


        for (User02 user02 : list) {
            userMapper.addOne(user02);
        }
                return true;
    }

耗時:29.50s

效果顯而易見,簡單闡述下優化思路吧

首先有一個一萬個元素的集合需要將其中所有資料插入到資料庫表中
不希望進行全部遍歷
然後也是分次插入去實現,每次肯定是批量插入,這種插入最快
但是mysql一次最多可以批量插入多少條資料,
或者一次批量插入多少條資料效率是最高的這個沒做考量
(一次批量插入3000條這個是可以的)

知道了每次是具體怎麼實現的時候就是分幾次的問題了,這個簡單
10000條,每次處理3000條的話,就要處理4次
計算公式:總條數/每次處理的次數,看能不能除盡就能判斷

知道了處理幾次之後,然後去遍歷處理的次數
每遍歷一次都是做一次資料的處理
遍歷第一次,處理第0個到第3000個元素
遍歷第二次,處理第3000個到第6000個元素
遍歷第三次,處理第6000個到第9000個元素
遍歷第四次,處理第9000個到第10000個元素

------然後就有了上述的程式碼-----

前面幾次簡單,只不過最後一次遍歷的時候,需要做一下處理

然後這樣遍歷就只需要遍歷處理次數,然後每次遍歷的時候,對總集合資料進行subList對集合切割即可

然後每次處理,可以用多執行緒,處理一次開啟一個執行緒處理

----執行緒池建立執行緒數就是處理的次數----

每一次處理的時候,將處理的資料和執行緒池物件傳遞給處理方法
處理方法 開啟一個執行緒-->處理資料-->執行緒池執行執行緒

最後遍歷結束之後關閉執行緒池即可

結論:

多執行緒的優化方法程式碼相比來說要複雜了點,但是個人感覺效率高一點

優化2的程式碼更少,也更簡單,可以實現,也更好理解一點

但是至於兩種方案到底哪種效率更高一些,這個沒有做對比,因為感覺沒那麼重要,重要的是解決這個問題的思想,個人感覺解決問題的思路才是最重要的!!!

只是模擬了一萬條資料做了小demo進行簡單測試,優化2已經到生產中使用

並且這都是資料量少的情況,資料量大的話也應該會有更好的處理方法

以上為個人總結的3種優化方法,本人不才,感謝瀏覽,僅供參考