當前位置: 妍妍網 > 碼農

增加索引 + 異步 + 不落地後,從 12h 最佳化到 15 min

2024-06-27碼農

大家好,我是哪咤。

在開發中,我們經常會遇到這樣的需求,將資料庫中的圖片匯出到本地,再傳給別人。

一、一般我會這樣做:

  1. 透過介面或者定時任務的形式

  2. 讀取Oracle或者MySQL資料庫

  3. 透過FileOutputStream將Base64解密後的byte[]儲存到本地

  4. 遍歷本地資料夾,將圖片透過FTP上傳到第三方伺服器

現場炸鍋了!

實際的數據量非常大,據統計差不多有400G的圖片需要匯出。

現場人員的反饋是,已經跑了12個小時了,還在繼續,不知道啥時候能導完。

停下來呢?之前的白導了,不停呢?不知道要等到啥時候才能導完。

這不行啊,速度太慢了,一個簡單的任務,不能被這東西耗死吧?

@Value("${months}")
private String months;
@Value("${imgDir}")
private String imgDir;
@Resource
private UserDao userDao;
@Override
publicvoidgetUserInfoImg(){
try {
// 獲取需要匯出的月表
String[] monthArr = months.split(",");
for (int i = 0; i < monthArr.length; i++) {
// 獲取月表中的圖片
Map<String, Object> map = new HashMap<String, Object>();
String tableName = "USER_INFO_" + monthArr[i];
map.put("tableName", tableName);
map.put("status"1);
List<UserInfo> userInfoList = userDao.getUserInfoImg(map);
if (userInfoList == null || userInfoList.size() == 0) {
return;
}
for (int j = 0; j < userInfoList.size(); j++) {
UserInfo user = userInfoList.get(j);
String userId = user.getUserId();
String userName = user.getUserName();
byte[] content = user.getImgContent;
// 下載圖片到本地
FileUtil.dowmloadImage(imgDir + userId+"-"+userName+".png", content);
// 將下載好的圖片,透過FTP上傳給第三方
FileUtil.uploadByFtp(imgDir);
}
}
 } catch (Exception e) {
serviceLogger.error("獲取圖片異常:", e);
 }
}





二、誰寫的?趕緊加班最佳化,會追責嗎?

經過1小時的深思熟慮,慢的原因可能有以下幾點:

  1. 查詢資料庫

  2. 程式序列

  3. base64解密

  4. 圖片落地

  5. FTP上傳到伺服器

最佳化1:資料庫中添加對應的索引,提高查詢速度

最佳化2:采用 增加索引 + 異步 + 多執行緒 的方式進行匯出

最佳化3:不解密 + 圖片不落地,直接透過FTP傳給第三方

使用 索引 + 異步 + 不解密 + 不落地 後,40G圖片的匯出上傳,從 12+小時 最佳化到 15 分鐘,你敢信?

國內直接使用ChatGPT4o:

用官方一半價格的錢,用跟官方 ChatGPT4.0 一模一樣功能的工具,而且不需要魔法,直接使用,不用擔心網路問題。

國內直接使用 ChatGPT4o

  1. 無需魔法,同時支持電腦、手機,瀏覽器直接使用

  2. ChatGPT3.5永久免費, 提供免費共享GPT3.5授權碼

  3. 支持 Chat GPT-4o文本對話、 Copilot編程、DALL-E AI繪畫、AI語音對話等

長按辨識下方二維碼,備註ai,無需魔法,國內直接使用ChatGPT4o

差不多的程式碼,效率差距竟如此之大。

下面貼出匯出圖片不落地的關鍵程式碼。

@Resource
private UserAsyncService userAsyncService;
@Override
publicvoidgetUserInfoImg(){
try {
// 獲取需要匯出的月表
String[] monthArr = months.split(",");
for (int i = 0; i < monthArr.length; i++) {
userAsyncService.getUserInfoImgAsync(monthArr[i]);
}
 } catch (Exception e) {
serviceLogger.error("獲取圖片異常:", e);
 }
}

@Value("${months}")
private String months;
@Resource
private UserDao userDao;
@Async("async-executor")
@Override
publicvoidgetUserInfoImgAsync(String month){
try {
// 獲取月表中的圖片
Map<String, Object> map = new HashMap<String, Object>();
String tableName = "USER_INFO_" + month;
map.put("tableName", tableName);
map.put("status"1);
List<UserInfo> userInfoList = userDao.getUserInfoImg(map);
if (userInfoList == null || userInfoList.size() == 0) {
return;
}
for (int i = 0; i < userInfoList.size(); i++) {
UserInfo user = userInfoList.get(i);
String userId = user.getUserId();
String userName = user.getUserName();
byte[] content = user.getImgContent;
// 不落地,直接透過FTP上傳給第三方
FileUtil.uploadByFtp(content);
}
 } catch (Exception e) {
serviceLogger.error("獲取圖片異常:", e);
 }
}



4、異步執行緒池工具類

@Async的作用就是異步處理任務。

  1. 在方法上添加@Async,表示此方法是異步方法;

  2. 在類上添加@Async,表示類中的所有方法都是異步方法;

  3. 使用此註解的類,必須是Spring管理的類;

  4. 需要在啟動類或配置類中加入@EnableAsync註解,@Async才會生效;

在使用@Async時,如果不指定執行緒池的名稱,也就是不自訂執行緒池,@Async是有預設執行緒池的,使用的是Spring預設的執行緒池SimpleAsyncTaskExecutor。

預設執行緒池的預設配置如下:

  1. 預設核心執行緒數:8;

  2. 最大執行緒數:Integet.MAX_VALUE;

  3. 佇列使用LinkedBlockingQueue;

  4. 容量是:Integet.MAX_VALUE;

  5. 空閑執行緒保留時間:60s;

  6. 執行緒池拒絕策略:AbortPolicy;

從最大執行緒數可以看出,在並行情況下,會無限制的建立執行緒,我勒個嗎啊。

也可以透過yml重新配置:

spring:
task:
execution:
pool:
max-size: 10
core-size: 5
keep-alive: 3s
queue-capacity: 1000
thread-name-prefix: my-executor

也可以自訂執行緒池,下面透過簡單的程式碼來實作以下@Async自訂執行緒池。

@EnableAsync// 支持異步操作
@Configuration
public classAsyncTaskConfig{
/**
* com.google.guava中的執行緒池
@return
*/

@Bean("my-executor")
public Executor firstExecutor(){
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("my-executor").build();
// 獲取CPU的處理器數量
int curSystemThreads = Runtime.getRuntime().availableProcessors() * 2;
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(curSystemThreads, 100,
200, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(), threadFactory);
threadPool.allowsCoreThreadTimeOut();
return threadPool;
}
/**
* Spring執行緒池
@return
*/

@Bean("async-executor")
public Executor asyncExecutor(){
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
// 核心執行緒數
taskExecutor.setCorePoolSize(24);
// 執行緒池維護執行緒的最大數量,只有在緩沖佇列滿了之後才會申請超過核心執行緒數的執行緒
taskExecutor.setMaxPoolSize(200);
// 緩存佇列
taskExecutor.setQueueCapacity(50);
// 空閑時間,當超過了核心執行緒數之外的執行緒在空閑時間到達之後會被銷毀
taskExecutor.setKeepAliveSeconds(200);
// 異步方法內部執行緒名稱
taskExecutor.setThreadNamePrefix("async-executor-");
/**
* 當執行緒池的任務緩存佇列已滿並且執行緒池中的執行緒數目達到maximumPoolSize,如果還有任務到來就會采取任務拒絕策略
* 通常有以下四種策略:
* ThreadPoolExecutor.AbortPolicy:丟棄任務並丟擲RejectedExecutionException異常。
* ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不丟擲異常。
* ThreadPoolExecutor.DiscardOldestPolicy:丟棄佇列最前面的任務,然後重新嘗試執行任務(重復此過程)
* ThreadPoolExecutor.CallerRunsPolicy:重試添加當前的任務,自動重復呼叫 execute() 方法,直到成功
*/

taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}
}

三、告別劣質程式碼,最佳化從何入手?

我覺得最佳化有兩個大方向:

  1. 業務最佳化

  2. 程式碼最佳化

1、業務最佳化

業務最佳化的影響力非常大,但它一般屬於產品和計畫經理的範疇,CRUD程式設計師很少能接觸到。

比如上面說的圖片匯出上傳需求, 經過產品經理和計畫經理的不懈努力,這個需求不做了 ,這最佳化力度,史無前例啊。

2、程式碼最佳化

  1. 資料庫最佳化

  2. 復用最佳化

  3. 並列最佳化

  4. 演算法最佳化

四、資料庫最佳化

  1. inner join 、left join、right join,優先使用inner join

  2. 表連線不宜太多,索引不宜太多,一般5個以內

  3. 復合索引最左特性

  4. 操作delete或者update語句,加個limit或者迴圈分批次刪除

  5. 使用explain分析你SQL執行計劃

  6. ...

資料庫最佳化的方式有很多,之前總結過,這裏不再贅述。


五、復用最佳化

寫程式碼的時候,大家一般都會將重復性的程式碼提取出來,寫成工具方法,在下次用的時候,就不用重新編碼,直接呼叫就可以了。

這個就是復用。

資料庫連線池、執行緒池、長連線也都是復用手段,這些物件的建立和銷毀成本過高,復用之後,效率提升顯著。

1、連線池

連線池 是一種常見的最佳化網路連線復用性的方法。連線池管理著一定數量的網路連線,並且在需要時將這些連線分配給客戶端,客戶端使用完後將連線歸還給連線池。這樣可以避免每次通訊都建立新的連線,減少了連線的建立和銷毀過程,提高了系統的效能和效率。

在Java開發中,常用的連線池技術有Apache Commons Pool、Druid等。使用連線池時,需要合理設定連線池的大小,並根據實際情況進行調優。連線池的大小過小會導致連線不夠用,而過大則會占用過多的系統資源。

2、長連線

長連線 是另一種最佳化網路連線復用性的方法。長連線指的是在一次通訊後,保持網路連線不關閉,以便後續的通訊繼續復用該連線。與短連線相比,長連線在一定程度上減少了連線的建立和銷毀過程,提高了網路連線的復用性和效率。

在Java開發中,可以透過使用Socket編程實作長連線。客戶端在建立連線後,透過設定Socket的Keep-Alive選項,使得連線保持活躍狀態。這樣可以避免頻繁地建立新的連線,提高網路連線的復用性和效率。

3、緩存

緩存 也是比較常用的復用,屬於數據復用。

緩存一般是將資料庫中的數據緩存到記憶體或者Redis中,也就是緩存到相對高速的區域,下次查詢時,直接存取緩存,就不用查詢資料庫了,緩存主要針對的是讀操作。

4、緩沖

緩沖 常見於對數據的暫存,然後批次傳輸或者寫入。多使用順序方式,用來緩解不同裝置之間頻繁地、緩慢地隨機寫,緩沖主要針對的是寫操作。

六、並列最佳化

1、異步編程

上面的最佳化方式就是異步最佳化,充分利用多核處理器的效能,將序列的程式改為並列,大大提高了程式的執行效率。

異步編程是一種編程模型,其中任務的執行不會阻塞當前執行緒的執行。透過將任務送出給其他執行緒或執行緒池來處理,當前執行緒可以繼續執行其他操作,而不必等待任務完成。

2、異步編程的特點

  1. 非阻塞 :異步任務的執行不會導致呼叫執行緒的阻塞,允許執行緒繼續執行其他任務;

  2. 回呼機制 :異步任務通常會註冊回呼函式,當任務完成時,會呼叫相應的回呼函式進行後續處理;

  3. 提高響應性 :異步編程能夠提高程式的響應性,尤其適用於處理IO密集型任務,如網路請求、資料庫查詢等;

Java 8引入了CompletableFuture類,可以方便地進行異步編程。

3、並列編程

並列編程是一種利用多個執行緒或處理器同時執行多個任務的編程模型。它將大任務劃分為多個子任務,並行地執行這些子任務,從而加速整體任務的完成時間。

4、並列編程的特點

  1. 分布式任務 :並列編程將大任務劃分為多個獨立的子任務,每個子任務在不同的執行緒中並列執行;

  2. 數據共享 :並列編程需要考慮多個執行緒之間的數據共享和同步問題,以避免出現競態條件和數據不一致的情況;

  3. 提高效能 :並列編程能夠充分利用多核處理器的計算能力,加速程式的執行速度。

5、並列編程如何實作?

  1. 多執行緒 :Java提供了Thread類和Runnable介面,用於建立和管理多個執行緒。透過建立多個執行緒並行執行任務,可以實作並列編程。

  2. 執行緒池 :Java的Executor框架提供了執行緒池的支持,可以方便地管理和排程多個執行緒。透過執行緒池,可以復用執行緒物件,減少執行緒建立和銷毀的開銷;

  3. 並行集合 :Java提供了一系列的並行集合類,如ConcurrentHashMap、ConcurrentLinkedQueue等,用於在並列編程中實作執行緒安全的數據共享。

異步編程和並列編程是Java中處理任務並提高程式效能的兩種重要方法。

異步編程透過非阻塞的方式處理任務,提高程式的響應性,並適用於IO密集型任務。

而並列編程則是透過多個執行緒或處理器並行執行任務,充分利用計算資源,加速程式的執行速度。

在Java中,可以使用CompletableFuture和回呼介面實作異步編程,使用多執行緒、執行緒池和並行集合實作並列編程。透過合理地運用異步和並列編程,我們可以在Java中高效地處理任務和提升程式的效能。

6、程式碼範例

publicstaticvoidmain(String[] args){
// 建立執行緒池
ExecutorService executor = Executors.newFixedThreadPool(10);
// 使用執行緒池建立CompletableFuture物件
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
// 一些不為人知的操作
return"result"// 返回結果
}, executor);
// 使用CompletableFuture物件執行任務
CompletableFuture<String> result = future.thenApply(result -> {
// 一些不為人知的操作
return"result"// 返回結果
});
// 處理任務結果
String finalResult = result.join();
// 關閉執行緒池
executor.shutdown();
}


7、Java 8 parallel

(1)parallel()是什麽

Stream.parallel() 方法是 Java 8 中 Stream API 提供的一種並列處理方式。在處理大量數據或者耗時操作時,使用 Stream.parallel() 方法可以充分利用多核 CPU 的優勢,提高程式的效能。

Stream.parallel() 方法是將序列流轉化為並列流的方法。透過該方法可以將大量數據劃分為多個子任務交由多個執行緒並列處理,最終將各個子任務的計算結果合並得到最終結果。使用 Stream.parallel() 可以簡化多執行緒編程,減少開發難度。

需要註意的是,並列處理可能會引入執行緒安全等問題,需要根據具體情況進行選擇。

(2)舉一個簡單的demo

定義一個list,然後透過parallel() 方法將集合轉化為並列流,對每個元素進行i++,最後透過 collect(Collectors.toList()) 方法將結果轉化為 List 集合。

使用並列處理可以充分利用多核 CPU 的優勢,加快處理速度。

public classStreamTest{
publicstaticvoidmain(String[] args){
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i);
}
System.out.println(list);
List<Integer> result = list.stream().parallel().map(i -> i++).collect(Collectors.toList());
System.out.println(result);
}
}

我勒個去,什麽情況?

(3)parallel()的優缺點

① 優點:

  1. 充分利用多核 CPU 的優勢,提高程式的效能;

  2. 可以簡化多執行緒編程,減少開發難度。

② 缺點:

  1. 並列處理可能會引入執行緒安全等問題,需要根據具體情況進行選擇;

  2. 並列處理需要付出額外的開銷,例如執行緒池的建立和銷毀、執行緒切換等,對於小數據量和簡單計算而言,序列處理可能更快。

(4)何時使用parallel()?

在實際開發中,應該根據數據量、計算復雜度、硬體等因素綜合考慮。

比如:

  1. 數據量較大,有1萬個元素;

  2. 計算復雜度過大,需要對每個元素進行復雜的計算;

  3. 硬體夠硬,比如多核CPU。

七、演算法最佳化

在上面的例子中,避免base64解密,就應該歸類於演算法最佳化。

程式就是由數據結構和演算法組成,一個優質的演算法可以顯著提高程式的執行效率,從而減少執行時間和資源消耗。相比之下,一個低效的演算法就可能導致執行非常緩慢,並占用大量系統資源。

很多問題都可以透過演算法最佳化來解決,比如:

1、迴圈和遞迴

迴圈和遞迴是Java編程中常見的操作,然而,過於復雜的業務邏輯往往會帶來多層迴圈套用,不必要的重復迴圈會大大降低程式的執行效率。

遞迴是一種函式自我呼叫的技術,類似於迴圈,雖然遞迴可以解決很多問題,但是,遞迴的效率有待提高。

2、記憶體管理

Java內建垃圾收集器,開發人員不用手動釋放記憶體。

但是,不合理的記憶體使用可能導致記憶體泄漏和效能下降,確保及時釋放不再使用的物件,避免建立過多的臨時物件。

3、字串

我覺得字串是Java編程中使用頻率最高的技術,很多程式設計師恨不得把所有的變量都定義成字串。

然而,由於字串是不可變的,每次執行字串拼接、替換時,都會建立一個新的字串。這會占用大量的記憶體和處理時間。

使用StringBuilder來處理字串的拼接可以顯著的提高效能。

4、IO操作

IO操作通常是最耗費效能和資源的操作。在處理大量數據IO操作時,務必註意最佳化IO程式碼,提高程式效能,比如上面提高的圖片不落地就是徹底解決IO問題。

5、數據結構的選擇

選擇適當的數據結構對程式的效能至關重要。

比如Java世界中用的第二多的Map,比較常用的有HashMap、HashTable、ConcurrentHashMap。

  1. HashMap ,底層陣列+連結串列實作,可以儲存null鍵和null值,執行緒不安全;

  2. HashTable ,底層陣列+連結串列實作,無論key還是value都不能為null,執行緒安全,實作執行緒安全的方式是在修改數據時鎖住整個HashTable,效率低,ConcurrentHashMap做了相關最佳化;

  3. ConcurrentHashMap ,底層采用分段的陣列+連結串列實作,執行緒安全,透過把整個Map分為N個Segment,可以提供相同的執行緒安全,但是效率提升N倍,預設提升16倍。

Hashtable的synchronized是針對整張Hash表的,即每次鎖住整張表讓執行緒獨占,ConcurrentHashMap允許多個修改操作並行進行,其關鍵在於使用了鎖分離技術。

最佳化的方式有很多,程式中需要最佳化的地方也有很多,但是,切勿陷入不最佳化不舒服、花大力氣做小事的怪圈。

·················END·················

最後給大家推薦一個ChatGPT 4o國內網站,是我們團隊一直在使用的,我們對接的是OpenAI官網的帳號,給大家打造了一個一模一樣的ChatGPT,很多粉絲朋友現在也都透過我拿這種號,價格不貴,關鍵還有售後 。

一句話說明用官方一半價格的錢,用跟官方 ChatGPT4.0 一模一樣功能的工具,而且不需要魔法,直接使用,不用擔心網路問題。

功能簡介:

  1. AI語言功能全面上線

  2. GPT-4o知識問答:支持1000+token上下文記憶功能

  3. DALL-E AI繪畫:AI繪畫 + 剪輯 = 自媒體新時代

  4. 專職家教:精通語數外,拍照上傳即可辨識問題,給出權威回答

  5. 論文小能手:寫論文大模型Consensus、論文降重大模型

  6. 最強程式碼大模型Code Copilot:程式碼自動補全、程式碼最佳化建議、程式碼重構等。

  7. 聯網查詢(平替百度)、上傳檔、數據分析等。

國內直接使用ChatGPT4o

  1. 支持OpenAI最新的ChatGPT4o。

  2. 無需魔法,同時支持PC、手機、平板,瀏覽器直接使用

  3. 一個帳號一個專屬授權碼,保護個人私密,使用記錄長期保存。

  4. ChatGPT3.5永久免費,提供免費共享GPT3.5授權碼 。

  5. 官方獨立帳戶規定每3小時40次 4.0提問,我們這個不限制4.0提問次數。

  6. 我們這個不會出現封號的情況,避免你因為封號多花冤枉錢。

  7. 聯系站長18640839506,備註AI,直接使用ChatGPT4o,拉你進ChatGPT售後群,群公告有使用說明和註意事項,有任何問題群裏交流,群裏有專業的技術支持。

回復gpt,獲取ChatGPT4o直接使用地址

點選閱讀原文,國內直接使用ChatGpt4o