新聞中心
一、問題&分析
性能優(yōu)化是技術人的永恒話題,當我們遇到性能問題時,你的第一反應是什么?

克拉瑪依區(qū)ssl適用于網(wǎng)站、小程序/APP、API接口等需要進行數(shù)據(jù)傳輸應用場景,ssl證書未來市場廣闊!成為創(chuàng)新互聯(lián)公司的ssl證書銷售渠道,可以享受市場價格4-6折優(yōu)惠!如果有意向歡迎電話聯(lián)系或者加微信:028-86922220(備注:SSL證書合作)期待與您的合作!
數(shù)據(jù)庫索引優(yōu)化,緩存優(yōu)化,算法優(yōu)化?
但,有時性能殺手往往就是性能優(yōu)化引入的。
1.1. 案例
今天一大早,小艾剛到公司便收到一組系統(tǒng)報警,原來有一個接口報了一堆的慢情況。仔細排查,發(fā)現(xiàn)是前兩天為服務域提供的一個訂單的查詢接口,該接口剛上線不久,正處于放量階段,小艾立即驚出一身冷汗,不會是數(shù)據(jù)庫出現(xiàn)了 慢SQL?記得上線前通過 explain 指令對 sql 進行過分析,明確已經(jīng)使用了數(shù)據(jù)庫索引。他趕緊打開阿里云控制臺,快速進入 慢查詢功能進行查看,但奇怪的是監(jiān)控顯示沒有一條 慢查詢,真是太詭異了。
還好不是數(shù)據(jù)庫慢查詢,不然可能存在將整個 MySQL 數(shù)據(jù)庫拖垮的可能,小艾的懸著的心也終于放了下來。
可問題出在哪里呢?
這個查詢接口非常簡單,示例代碼如下:
@GetMapping("getOrdersByUsers")
public RestResult> allOrderByUsers(@RequestParam List users){
Stopwatch stopwatch = Stopwatch.createStarted();
List orders = getByUserId(users);
List orderVOS = orders.stream()
.map(order -> OrderVO.applyByParallel(order))
.collect(Collectors.toList());
log.info("get order by user cost {} ms", stopwatch.stop().elapsed(TimeUnit.MILLISECONDS));
return RestResult.success(orderVOS);
}
邏輯簡單到令人發(fā)指,只有兩步:
- 根據(jù)傳入的 user id 從數(shù)據(jù)庫中查詢訂單
- 將查詢的 Order 轉(zhuǎn)換為 OrderVO 返回用戶
小艾,仔細觀察這個接口,發(fā)現(xiàn)一個現(xiàn)象:當入?yún)⑤^多時,接口的性能變的非常差。
這個也比較好理解,系統(tǒng)使用的是 in 語句對數(shù)據(jù)進行查詢,示例:select * from order_info where user_id in (?),當入?yún)?shù)據(jù)量非常大時,sql 執(zhí)行耗時變高。這可能是一個原因,但MySQL 慢請求中未記錄任何信息,說明 sql 的執(zhí)行時間沒有超過 1 秒,所以,這個只是一個表因。
為了更好的驗證猜想,小艾對日志進行完善,整體如下:
@GetMapping("getOrdersByUsers")
public RestResult> allOrderByUsers(@RequestParam List users){
Stopwatch stopwatch = Stopwatch.createStarted();
List orders = getByUserId(users);
log.info("get data from DB cost {} ms", stopwatch.stop().elapsed(TimeUnit.MILLISECONDS));
stopwatch = Stopwatch.createStarted();
List orderVOS = orders.stream()
.map(order -> OrderVO.applyByParallel(order))
.collect(Collectors.toList());
log.info("convert to OrderVO cost {} ms", stopwatch.stop().elapsed(TimeUnit.MILLISECONDS));
return RestResult.success(orderVOS);
}
選了幾個訂單較多的用戶進行測試,打印日志如下:
圖片
好奇怪,數(shù)據(jù)庫操作耗時有限,但 Order 向 OrderVO 的轉(zhuǎn)換居然耗時這么多,真是太不可思議!
1.2. 問題分析
很明顯是轉(zhuǎn)化這步出了問題,其核心代碼如下所示:
// 使用 Stream 流進行類型轉(zhuǎn)化
List orderVOS = orders.stream()
.map(order -> OrderVO.applyByParallel(order))
.collect(Collectors.toList());
// Order 到 OrderVO 的轉(zhuǎn)化邏輯
public static OrderVO applyByParallel(Order order){
OrderVO orderVO = new OrderVO();
orderVO.setId(order.getId());
orderVO.setUserId(order.getUserId());
orderVO.setStatus(OrderStatus.parallelParseByCode(order.getOrderStatus()));
orderVO.setOrderType(OrderType.parallelParseByCode(order.getOrderType()));
orderVO.setProductType(ProductType.parallelParseByCode(order.getProductType()));
orderVO.setPromotionType(PromotionType.parallelParseByCode(order.getPromotionType()));
return orderVO;
}
// 將 Code 轉(zhuǎn)換為對應的枚舉
public static OrderStatus parallelParseByCode(int code) {
return Stream.of(values())
.parallel()
.filter(status -> status.getCode() == code)
.findFirst()
.orElse(null);
} 看完核心代碼,請思考幾分鐘,問題可能出現(xiàn)在哪里?
- Stream 操作?Stream 比 for 循環(huán)性能超差些,但還不至于有這么大差異
- 反射、BeanCopy?核心代碼沒有使用這些 API,乖乖的進行 Coding
那問題究竟在哪?答案是 Stream 的 parallel() 函數(shù)。使用 parallel 函數(shù)最初的目標便是提升性能,為什么在這里卻成了性能殺手?在解答前,先快速了解下這個函數(shù):
`Stream.parallel()` 函數(shù)是 Java 8 中引入的新特性,底層采用了 Fork/Join 框架來實現(xiàn)并行處理。當你調(diào)用 `parallel()` 函數(shù)時,實際上是將流的并行性設計為 true。這意味著所進行的任何操作,如 `map` 或 `filter`,都是在并行流(parallel stream)上執(zhí)行的。Fork/Join 框架首先會將一個大任務拆分成若干個小任務(Fork),然后分別對這些小任務進行處理,最后將得到的結果合并(Join)來得到最終結果。
這種方式能有效地將任務進行了分解,使得每個線程都可以獨立地處理一部分任務,從而發(fā)揮了多核 CPU 的優(yōu)勢,提高了整體的處理效率。
從上述解釋中可以看出,parallel 底層使用 Fork/Join 框架,對任務進行拆解,可以發(fā)揮多核的優(yōu)勢,那怎么就成了性能殺手呢?
先看下 Fork/Join 的整體執(zhí)行流程:
圖片
其執(zhí)行主要分為以下幾個階段:
- 分割階段(Fork Phase):將大任務拆分成若干個小任務,直到任務的規(guī)模足夠小,可以直接執(zhí)行。這通常是通過遞歸方式實現(xiàn)的。
- 執(zhí)行階段(Computation Phase):執(zhí)行每個小任務,并生成結果。
- 結果合并階段(Join Phase):合并小任務的結果,生成大任務的結果。這也通常通過遞歸的方式實現(xiàn),與拆分階段對應。
- 善后階段(Finalize Phase):所有任務的結果都已合并完畢,大任務的結果也已經(jīng)生成,可以進行善后工作,比如釋放資源等。
每個階段都有一定開銷,從整個執(zhí)行流程上看,執(zhí)行階段占的時間越長,性能提升就越高。在數(shù)據(jù)量較少,或者執(zhí)行操作開銷較大時,并行處理不但不能提高性能,還會由于線程管理和任務分配的開銷而導致性能下降。
再次回到上面這個案例:
// 將 Code 轉(zhuǎn)換為對應的枚舉
public static OrderStatus parallelParseByCode(int code) {
return Stream.of(values())
.parallel()
.filter(status -> status.getCode() == code)
.findFirst()
.orElse(null);
}首先,枚舉的數(shù)量非常小,其次,執(zhí)行邏輯非常簡單,僅進行一個等值比較。在這種情況下使用 parallel 函數(shù),將致使線程管理和任務分配開銷巨大,從而成為系統(tǒng)瓶頸。
二、解決方案
既然問題是通過 parallel 函數(shù)引入的,那解決方案便是:刪除 parallel 函數(shù)調(diào)用,直接串行執(zhí)行即可。
修改后的代碼如下:
public static OrderStatus parseByCode(int code) {
return Stream.of(values())
// .parallel() 直接使用串行執(zhí)行
.filter(status -> status.getCode() == code)
.findFirst()
.orElse(null);
}使用相同的數(shù)據(jù)重新測試,耗時如下圖所示:
圖片
可見,性能直接提升 10 倍不止。
三、示例&源碼
代碼倉庫:https://gitee.com/litao851025/learnFromBug
代碼地址:https://gitee.com/litao851025/learnFromBug/tree/master/src/main/java/com/geekhalo/demo/thread/parallelfun
新聞標題:我被Parallel函數(shù)雷了
標題來源:http://fisionsoft.com.cn/article/cdhpoep.html


咨詢
建站咨詢
