新聞中心
在開發(fā)中,我們經(jīng)常會遇到這樣的需求,將數(shù)據(jù)庫中的圖片導(dǎo)出到本地,再傳給別人。

10年積累的做網(wǎng)站、網(wǎng)站建設(shè)經(jīng)驗,可以快速應(yīng)對客戶對網(wǎng)站的新想法和需求。提供各種問題對應(yīng)的解決方案。讓選擇我們的客戶得到更好、更有力的網(wǎng)絡(luò)服務(wù)。我雖然不認識你,你也不認識我。但先網(wǎng)站策劃后付款的網(wǎng)站建設(shè)流程,更有建鄴免費網(wǎng)站建設(shè)讓你可以放心的選擇與我們合作。
一、一般我會這樣做:
- 通過接口或者定時任務(wù)的形式。
- 讀取Oracle或者MySQL數(shù)據(jù)庫。
- 通過FileOutputStream將Base64解密后的byte[]存儲到本地。
- 遍歷本地文件夾,將圖片通過FTP上傳到第三方服務(wù)器。
現(xiàn)場炸鍋了!
實際的數(shù)據(jù)量非常大,據(jù)統(tǒng)計差不多有400G的圖片需要導(dǎo)出。
現(xiàn)場人員的反饋是,已經(jīng)跑了12個小時了,還在繼續(xù),不知道啥時候能導(dǎo)完。
停下來呢?之前的白導(dǎo)了,不停呢?不知道要等到啥時候才能導(dǎo)完。
這不行啊,速度太慢了,一個簡單的任務(wù),不能被這東西耗死吧?
@Value("${months}")
private String months;
@Value("${imgDir}")
private String imgDir;
@Resource
private UserDao userDao;
@Override
public void getUserInfoImg() {
try {
// 獲取需要導(dǎo)出的月表
String[] monthArr = months.split(",");
for (int i = 0; i < monthArr.length; i++) {
// 獲取月表中的圖片
Map map = new HashMap();
String tableName = "USER_INFO_" + monthArr[i];
map.put("tableName", tableName);
map.put("status", 1);
List 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);
}
}
二、誰寫的?趕緊加班優(yōu)化,會追責(zé)嗎?
經(jīng)過1小時的深思熟慮,慢的原因可能有以下幾點:
- 查詢數(shù)據(jù)庫
- 程序串行
- base64解密
- 圖片落地
- FTP上傳到服務(wù)器
優(yōu)化1:數(shù)據(jù)庫中添加對應(yīng)的索引,提高查詢速度。
優(yōu)化2:采用 增加索引 + 異步 + 多線程 的方式進行導(dǎo)出。
優(yōu)化3:不解密 + 圖片不落地,直接通過FTP傳給第三方。
使用 索引 + 異步 + 不解密 + 不落地 后,40G圖片的導(dǎo)出上傳,從 12+小時 優(yōu)化到 15 分鐘,你敢信?
差不多的代碼,效率差距竟如此之大。
下面貼出導(dǎo)出圖片不落地的關(guān)鍵代碼。
@Resource
private UserAsyncService userAsyncService;
@Override
public void getUserInfoImg() {
try {
// 獲取需要導(dǎo)出的月表
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
public void getUserInfoImgAsync(String month) {
try {
// 獲取月表中的圖片
Map map = new HashMap();
String tableName = "USER_INFO_" + month;
map.put("tableName", tableName);
map.put("status", 1);
List 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的作用就是異步處理任務(wù)。
- 在方法上添加@Async,表示此方法是異步方法。
- 在類上添加@Async,表示類中的所有方法都是異步方法。
- 使用此注解的類,必須是Spring管理的類。
- 需要在啟動類或配置類中加入@EnableAsync注解,@Async才會生效。
在使用@Async時,如果不指定線程池的名稱,也就是不自定義線程池,@Async是有默認線程池的,使用的是Spring默認的線程池SimpleAsyncTaskExecutor。
默認線程池的默認配置如下:
- 默認核心線程數(shù):8。
- 最大線程數(shù):Integet.MAX_VALUE。
- 隊列使用LinkedBlockingQueue。
- 容量是:Integet.MAX_VALUE。
- 空閑線程保留時間:60s。
- 線程池拒絕策略:AbortPolicy。
從最大線程數(shù)可以看出,在并發(fā)情況下,會無限制的創(chuàng)建線程,我勒個嗎啊。
也可以通過yml重新配置:
spring:
task:
execution:
pool:
max-size: 10
core-size: 5
keep-alive: 3s
queue-capacity: 1000
thread-name-prefix: my-executor
也可以自定義線程池,下面通過簡單的代碼來實現(xiàn)以下@Async自定義線程池。
@EnableAsync// 支持異步操作
@Configuration
public class AsyncTaskConfig {
/**
* com.google.guava中的線程池
* @return
*/
@Bean("my-executor")
public Executor firstExecutor() {
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("my-executor").build();
// 獲取CPU的處理器數(shù)量
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();
// 核心線程數(shù)
taskExecutor.setCorePoolSize(24);
// 線程池維護線程的最大數(shù)量,只有在緩沖隊列滿了之后才會申請超過核心線程數(shù)的線程
taskExecutor.setMaxPoolSize(200);
// 緩存隊列
taskExecutor.setQueueCapacity(50);
// 空閑時間,當(dāng)超過了核心線程數(shù)之外的線程在空閑時間到達之后會被銷毀
taskExecutor.setKeepAliveSeconds(200);
// 異步方法內(nèi)部線程名稱
taskExecutor.setThreadNamePrefix("async-executor-");
/**
* 當(dāng)線程池的任務(wù)緩存隊列已滿并且線程池中的線程數(shù)目達到maximumPoolSize,如果還有任務(wù)到來就會采取任務(wù)拒絕策略
* 通常有以下四種策略:
* ThreadPoolExecutor.AbortPolicy:丟棄任務(wù)并拋出RejectedExecutionException異常。
* ThreadPoolExecutor.DiscardPolicy:也是丟棄任務(wù),但是不拋出異常。
* ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務(wù),然后重新嘗試執(zhí)行任務(wù)(重復(fù)此過程)
* ThreadPoolExecutor.CallerRunsPolicy:重試添加當(dāng)前的任務(wù),自動重復(fù)調(diào)用 execute() 方法,直到成功
*/
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}
}
三、告別劣質(zhì)代碼,優(yōu)化從何入手?
我覺得優(yōu)化有兩個大方向:
- 業(yè)務(wù)優(yōu)化
- 代碼優(yōu)化
1、業(yè)務(wù)優(yōu)化
業(yè)務(wù)優(yōu)化的影響力非常大,但它一般屬于產(chǎn)品和項目經(jīng)理的范疇,CRUD程序員很少能接觸到。
比如上面說的圖片導(dǎo)出上傳需求,經(jīng)過產(chǎn)品經(jīng)理和項目經(jīng)理的不懈努力,這個需求不做了,這優(yōu)化力度,史無前例啊。
2、代碼優(yōu)化
- 數(shù)據(jù)庫優(yōu)化
- 復(fù)用優(yōu)化
- 并行優(yōu)化
- 算法優(yōu)化
四、數(shù)據(jù)庫優(yōu)化
- inner join 、left join、right join,優(yōu)先使用inner join
- 表連接不宜太多,索引不宜太多,一般5個以內(nèi)
- 復(fù)合索引最左特性
- 操作delete或者update語句,加個limit或者循環(huán)分批次刪除
- 使用explain分析你SQL執(zhí)行計劃
- ...
五、復(fù)用優(yōu)化
寫代碼的時候,大家一般都會將重復(fù)性的代碼提取出來,寫成工具方法,在下次用的時候,就不用重新編碼,直接調(diào)用就可以了。
這個就是復(fù)用。
數(shù)據(jù)庫連接池、線程池、長連接也都是復(fù)用手段,這些對象的創(chuàng)建和銷毀成本過高,復(fù)用之后,效率提升顯著。
1、連接池
連接池是一種常見的優(yōu)化網(wǎng)絡(luò)連接復(fù)用性的方法。連接池管理著一定數(shù)量的網(wǎng)絡(luò)連接,并且在需要時將這些連接分配給客戶端,客戶端使用完后將連接歸還給連接池。這樣可以避免每次通信都建立新的連接,減少了連接的建立和銷毀過程,提高了系統(tǒng)的性能和效率。
在Java開發(fā)中,常用的連接池技術(shù)有Apache Commons Pool、Druid等。使用連接池時,需要合理設(shè)置連接池的大小,并根據(jù)實際情況進行調(diào)優(yōu)。連接池的大小過小會導(dǎo)致連接不夠用,而過大則會占用過多的系統(tǒng)資源。
2、長連接
長連接是另一種優(yōu)化網(wǎng)絡(luò)連接復(fù)用性的方法。長連接指的是在一次通信后,保持網(wǎng)絡(luò)連接不關(guān)閉,以便后續(xù)的通信繼續(xù)復(fù)用該連接。與短連接相比,長連接在一定程度上減少了連接的建立和銷毀過程,提高了網(wǎng)絡(luò)連接的復(fù)用性和效率。
在Java開發(fā)中,可以通過使用Socket編程實現(xiàn)長連接??蛻舳嗽诮⑦B接后,通過設(shè)置Socket的Keep-Alive選項,使得連接保持活躍狀態(tài)。這樣可以避免頻繁地建立新的連接,提高網(wǎng)絡(luò)連接的復(fù)用性和效率。
3、緩存
緩存也是比較常用的復(fù)用,屬于數(shù)據(jù)復(fù)用。
緩存一般是將數(shù)據(jù)庫中的數(shù)據(jù)緩存到內(nèi)存或者Redis中,也就是緩存到相對高速的區(qū)域,下次查詢時,直接訪問緩存,就不用查詢數(shù)據(jù)庫了,緩存主要針對的是讀操作。
4、緩沖
緩沖常見于對數(shù)據(jù)的暫存,然后批量傳輸或者寫入。多使用順序方式,用來緩解不同設(shè)備之間頻繁地、緩慢地隨機寫,緩沖主要針對的是寫操作。
六、并行優(yōu)化
1、異步編程
上面的優(yōu)化方式就是異步優(yōu)化,充分利用多核處理器的性能,將串行的程序改為并行,大大提高了程序的執(zhí)行效率。
異步編程是一種編程模型,其中任務(wù)的執(zhí)行不會阻塞當(dāng)前線程的執(zhí)行。通過將任務(wù)提交給其他線程或線程池來處理,當(dāng)前線程可以繼續(xù)執(zhí)行其他操作,而不必等待任務(wù)完成。
2、異步編程的特點
- 非阻塞:異步任務(wù)的執(zhí)行不會導(dǎo)致調(diào)用線程的阻塞,允許線程繼續(xù)執(zhí)行其他任務(wù);
- 回調(diào)機制:異步任務(wù)通常會注冊回調(diào)函數(shù),當(dāng)任務(wù)完成時,會調(diào)用相應(yīng)的回調(diào)函數(shù)進行后續(xù)處理;
- 提高響應(yīng)性:異步編程能夠提高程序的響應(yīng)性,尤其適用于處理IO密集型任務(wù),如網(wǎng)絡(luò)請求、數(shù)據(jù)庫查詢等;
Java 8引入了CompletableFuture類,可以方便地進行異步編程。
3、并行編程
并行編程是一種利用多個線程或處理器同時執(zhí)行多個任務(wù)的編程模型。它將大任務(wù)劃分為多個子任務(wù),并發(fā)地執(zhí)行這些子任務(wù),從而加速整體任務(wù)的完成時間。
4、并行編程的特點
- 分布式任務(wù):并行編程將大任務(wù)劃分為多個獨立的子任務(wù),每個子任務(wù)在不同的線程中并行執(zhí)行;
- 數(shù)據(jù)共享:并行編程需要考慮多個線程之間的數(shù)據(jù)共享和同步問題,以避免出現(xiàn)競態(tài)條件和數(shù)據(jù)不一致的情況;
- 提高性能:并行編程能夠充分利用多核處理器的計算能力,加速程序的執(zhí)行速度。
5、并行編程如何實現(xiàn)?
- 多線程:Java提供了Thread類和Runnable接口,用于創(chuàng)建和管理多個線程。通過創(chuàng)建多個線程并發(fā)執(zhí)行任務(wù),可以實現(xiàn)并行編程。
- 線程池:Java的Executor框架提供了線程池的支持,可以方便地管理和調(diào)度多個線程。通過線程池,可以復(fù)用線程對象,減少線程創(chuàng)建和銷毀的開銷;
- 并發(fā)集合:Java提供了一系列的并發(fā)集合類,如ConcurrentHashMap、ConcurrentLinkedQueue等,用于在并行編程中實現(xiàn)線程安全的數(shù)據(jù)共享。
異步編程和并行編程是Java中處理任務(wù)并提高程序性能的兩種重要方法。
異步編程通過非阻塞的方式處理任務(wù),提高程序的響應(yīng)性,并適用于IO密集型任務(wù)。
而并行編程則是通過多個線程或處理器并發(fā)執(zhí)行任務(wù),充分利用計算資源,加速程序的執(zhí)行速度。
在Java中,可以使用CompletableFuture和回調(diào)接口實現(xiàn)異步編程,使用多線程、線程池和并發(fā)集合實現(xiàn)并行編程。通過合理地運用異步和并行編程,我們可以在Java中高效地處理任務(wù)和提升程序的性能。
6、代碼示例
public static void main(String[] args) {
// 創(chuàng)建線程池
ExecutorService executor = Executors.newFixedThreadPool(10);
// 使用線程池創(chuàng)建CompletableFuture對象
CompletableFuture future = CompletableFuture.supplyAsync(() -> {
// 一些不為人知的操作
return "result"; // 返回結(jié)果
}, executor);
// 使用CompletableFuture對象執(zhí)行任務(wù)
CompletableFuture result = future.thenApply(result -> {
// 一些不為人知的操作
return "result"; // 返回結(jié)果
});
// 處理任務(wù)結(jié)果
String finalResult = result.join();
// 關(guān)閉線程池
executor.shutdown();
}
7、Java 8 parallel
(1)parallel()是什么
Stream.parallel() 方法是 Java 8 中 Stream API 提供的一種并行處理方式。在處理大量數(shù)據(jù)或者耗時操作時,使用 Stream.parallel() 方法可以充分利用多核 CPU 的優(yōu)勢,提高程序的性能。
Stream.parallel() 方法是將串行流轉(zhuǎn)化為并行流的方法。通過該方法可以將大量數(shù)據(jù)劃分為多個子任務(wù)交由多個線程并行處理,最終將各個子任務(wù)的計算結(jié)果合并得到最終結(jié)果。使用 Stream.parallel() 可以簡化多線程編程,減少開發(fā)難度。
需要注意的是,并行處理可能會引入線程安全等問題,需要根據(jù)具體情況進行選擇。
(2)舉一個簡單的demo
定義一個list,然后通過parallel() 方法將集合轉(zhuǎn)化為并行流,對每個元素進行i++,最后通過 collect(Collectors.toList()) 方法將結(jié)果轉(zhuǎn)化為 List 集合。
使用并行處理可以充分利用多核 CPU 的優(yōu)勢,加快處理速度。
public class StreamTest {
public static void main(String[] args) {
List list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i);
}
System.out.println(list);
List result = list.stream().parallel().map(i -> i++).collect(Collectors.toList());
System.out.println(result);
}
}
我勒個去,什么情況?
(3)parallel()的優(yōu)缺點
① 優(yōu)點:
- 充分利用多核 CPU 的優(yōu)勢,提高程序的性能;
- 可以簡化多線程編程,減少開發(fā)難度。
② 缺點:
- 并行處理可能會引入線程安全等問題,需要根據(jù)具體情況進行選擇;
- 并行處理需要付出額外的開銷,例如線程池的創(chuàng)建和銷毀、線程切換等,對于小數(shù)據(jù)量和簡單計算而言,串行處理可能更快。
(4)何時使用parallel()?
在實際開發(fā)中,應(yīng)該根據(jù)數(shù)據(jù)量、計算復(fù)雜度、硬件等因素綜合考慮。
比如:
- 數(shù)據(jù)量較大,有1萬個元素;
- 計算復(fù)雜度過大,需要對每個元素進行復(fù)雜的計算;
- 硬件夠硬,比如多核CPU。
七、算法優(yōu)化
在上面的例子中,避免base64解密,就應(yīng)該歸類于算法優(yōu)化。
程序就是由數(shù)據(jù)結(jié)構(gòu)和算法組成,一個優(yōu)質(zhì)的算法可以顯著提高程序的執(zhí)行效率,從而減少運行時間和資源消耗。相比之下,一個低效的算法就可能導(dǎo)致運行非常緩慢,并占用大量系統(tǒng)資源。
很多問題都可以通過算法優(yōu)化來解決,比如:
1、循環(huán)和遞歸
循環(huán)和遞歸是Java編程中常見的操作,然而,過于復(fù)雜的業(yè)務(wù)邏輯往往會帶來多層循環(huán)套用,不必要的重復(fù)循環(huán)會大大降低程序的執(zhí)行效率。
遞歸是一種函數(shù)自我調(diào)用的技術(shù),類似于循環(huán),雖然遞歸可以解決很多問題,但是,遞歸的效率有待提高。
2、內(nèi)存管理
Java自帶垃圾收集器,開發(fā)人員不用手動釋放內(nèi)存。
但是,不合理的內(nèi)存使用可能導(dǎo)致內(nèi)存泄漏和性能下降,確保及時釋放不再使用的對象,避免創(chuàng)建過多的臨時對象。
3、字符串
我覺得字符串是Java編程中使用頻率最高的技術(shù),很多程序員恨不得把所有的變量都定義成字符串。
然而,由于字符串是不可變的,每次執(zhí)行字符串拼接、替換時,都會創(chuàng)建一個新的字符串。這會占用大量的內(nèi)存和處理時間。
使用StringBuilder來處理字符串的拼接可以顯著的提高性能。
4、IO操作
IO操作通常是最耗費性能和資源的操作。在處理大量數(shù)據(jù)IO操作時,務(wù)必注意優(yōu)化IO代碼,提高程序性能,比如上面提高的圖片不落地就是徹底解決IO問題。
5、數(shù)據(jù)結(jié)構(gòu)的選擇
選擇適當(dāng)?shù)臄?shù)據(jù)結(jié)構(gòu)對程序的性能至關(guān)重要。
比如Java世界中用的第二多的Map,比較常用的有HashMap、HashTable、ConcurrentHashMap。
- HashMap,底層數(shù)組+鏈表實現(xiàn),可以存儲null鍵和null值,線程不安全;
- HashTable,底層數(shù)組+鏈表實現(xiàn),無論key還是value都不能為null,線程安全,實現(xiàn)線程安全的方式是在修改數(shù)據(jù)時鎖住整個HashTable,效率低,ConcurrentHashMap做了相關(guān)優(yōu)化;
- ConcurrentHashMap,底層采用分段的數(shù)組+鏈表實現(xiàn),線程安全,通過把整個Map分為N個Segment,可以提供相同的線程安全,但是效率提升N倍,默認提升16倍。
Hashtable的synchronized是針對整張Hash表的,即每次鎖住整張表讓線程獨占,ConcurrentHashMap允許多個修改操作并發(fā)進行,其關(guān)鍵在于使用了鎖分離技術(shù)。
本文名稱:增加索引+異步+不落地后,從12h優(yōu)化到15min
當(dāng)前鏈接:http://m.fisionsoft.com.cn/article/cdigpgs.html


咨詢
建站咨詢
