業務背景描述:
主數據同步:調用主數據查詢接口,返回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種優化方法,本人不才,感謝瀏覽,僅供參考