30s到0.8s害碾,記錄一次接口優(yōu)化成功案例矢劲!

場景

在高并發(fā)的數(shù)據(jù)處理場景中,接口響應(yīng)時間的優(yōu)化顯得尤為重要慌随。本文將分享一個真實案例芬沉,其中一個數(shù)據(jù)量達(dá)到200萬+的接口的響應(yīng)時間從30秒降低到了0.8秒內(nèi)。

這個案例不僅展示了問題診斷的過程阁猜,也提供了一系列有效的優(yōu)化措施丸逸。

交易系統(tǒng)中,系統(tǒng)需要針對每一筆交易進(jìn)行攔截(每一筆支付或轉(zhuǎn)賬就是一筆交易)剃袍,攔截時需要根據(jù)定義好的規(guī)則攔截黄刚,這次需要優(yōu)化的接口是一個統(tǒng)計規(guī)則攔截率的接口。

問題診斷

最初民效,接口的延遲非常高憔维,大約需要30秒才能完成涛救。為了定位問題,我們首先排除了網(wǎng)絡(luò)和服務(wù)器設(shè)備因素业扒,并打印了關(guān)鍵代碼的執(zhí)行時間检吆。經(jīng)過分析,發(fā)現(xiàn)問題出在SQL執(zhí)行上程储。

發(fā)現(xiàn)Sql執(zhí)行時間太久蹭沛,查詢200萬條數(shù)據(jù)的執(zhí)行時間竟然達(dá)到了30s,下面是是最耗時的部分相關(guān)代碼邏輯:

查詢代碼(其實就是使用Mybatis查詢章鲤,看起來正常的很)

List<Map<String, Object>> list = transhandleFlowMapper.selectDataTransHandleFlowAdd(selectSql);

統(tǒng)計當(dāng)天的Id號(programhandleidlist字段)

SELECT programhandleidlist FROM anti_transhandle WHERE create_time BETWEEN '2024-01-08 00:00:00.0' AND '2024-01-09 00:00:00.0';

表結(jié)構(gòu)(Postgresql)

我以為是Sql寫的有問題摊灭,先拿著sql執(zhí)行了一邊,發(fā)現(xiàn)只執(zhí)行sql的執(zhí)行時間是大約800毫秒咏窿,和30秒差距巨大斟或。

Sql層面分析

使用EXPLAIN ANALYZE函數(shù)分析sql。

EXPLAIN ANALYZE

SELECT programhandleidlist FROM anti_transhandle WHERE create_time BETWEEN '2024-01-08 00:00:00.0' AND '2024-01-09 00:00:00.0';

分析結(jié)果

看來是代碼的部分有問題。

代碼層面分析

List<Map<String, Object>> list = transhandleFlowMapper.selectDataTransHandleFlowAdd(selectSql);

Map的Key是programhandleIdList,Map的value是每一行的值树绩。

在Java層面不脯,每條數(shù)據(jù)都創(chuàng)建了一個Map對象,對于200萬+的數(shù)據(jù)量來說畏陕,這顯然是非常耗時的操作,速度是被創(chuàng)建了大量的Map集合給拖垮的。酥泛。

為了解決這個問題,我們嘗試了將200萬行數(shù)據(jù)轉(zhuǎn)換為單行返回嫌拣,使用PostgreSQL的array_agg和unnest函數(shù)來優(yōu)化查詢柔袁。

第一次遇到Mybatis查詢返回導(dǎo)致接口速度慢的問題。

優(yōu)化措施

1. SQL優(yōu)化

我的思路是將200萬行轉(zhuǎn)為一行返回异逐。

要將 PostgreSQL 中查詢出的programhandleidlist字段(假設(shè)這是一個數(shù)組類型)的所有元素拼接為一行捶索,您可以使用數(shù)組聚合函數(shù)array_agg結(jié)合 unnest 函數(shù)。

這樣做可以先將數(shù)組展開為多行灰瞻,然后將這些行再次聚合為一個單一的數(shù)組腥例。如果您希望最終結(jié)果是一個字符串,而不是數(shù)組酝润,您還可以使用 string_agg 函數(shù)燎竖。

以下是相應(yīng)的 SQL 語句:

SELECT array_agg(elem) AS concatenated_array

FROM (

? ? SELECT unnest(programhandleidlist) AS elem

? ? FROM anti_transhandle

? ? WHERE create_time BETWEEN '2024-01-08 00:00:00.0' AND '2024-01-09 00:00:00.0'

) sub;

在這個查詢中:

unnest(programhandleidlist)將programhandleidlist數(shù)組展開成多行。

string_agg(elem)將這些行聚合成一個以逗號分隔的字符串要销。

這將返回一個包含所有元素的單一數(shù)組构回。

查詢結(jié)果由多行,拼接為了一行。

再測試捐凭,現(xiàn)在是正常速度了拨扶,但是查詢時間依舊很高。Sql查詢時間0.8秒茁肠,代碼中平均1秒8左右患民,還有優(yōu)化的空間。

將一列數(shù)據(jù)轉(zhuǎn)換為了數(shù)組類型垦梆,查看一下內(nèi)存占用匹颤,這一段占用了54比特,雖然占用不大托猩,但是不知道為什么會mybatis處理時間這么久印蓖。

因為mybatis不知道數(shù)組的大小,先給數(shù)組設(shè)定一個初始大小京腥,如果超出了數(shù)組長度赦肃,因為數(shù)組不能擴(kuò)容,增加長度只能再復(fù)制一份到另一塊內(nèi)存中公浪,復(fù)制的次數(shù)多了也就增加了計算時間他宛。

數(shù)據(jù)需要在兩個設(shè)備之間傳輸厅各,磁盤和網(wǎng)絡(luò)都需要時間宜鸯。

2. 部分業(yè)務(wù)邏輯轉(zhuǎn)到數(shù)據(jù)庫中計算

再次優(yōu)化sql,將一部分的邏輯放到Sql中處理灸芳,減少數(shù)據(jù)量冯遂。

業(yè)務(wù)上我需要統(tǒng)計programhandleidlist字段中id出現(xiàn)的次數(shù),所以我直接在sql中做統(tǒng)計。

要統(tǒng)計每個數(shù)組中元素出現(xiàn)的次數(shù),您需要首先使用 unnest 函數(shù)將數(shù)組展開為單獨的行,然后使用 GROUP BY 和聚合函數(shù)(如 count)來計算每個元素的出現(xiàn)次數(shù)推盛。這里是修改后的 SQL 語句:

SELECT elem, COUNT(*) AS count

FROM (

? ? SELECT unnest(programhandleidlist) AS elem

? ? FROM anti_transhandle

? ? WHERE create_time BETWEEN '2024-01-08 00:00:00.0' AND '2024-01-09 00:00:00.0'

) sub

GROUP BY elem;

在這個查詢中:

unnest(programhandleidlist)將每個programhandleidlist數(shù)組展開成多個行驹闰。

GROUP BY elem對每個獨立的元素進(jìn)行分組控嗜。

COUNT(*)計算每個分組(即每個元素)的出現(xiàn)次數(shù)惫谤。

這個查詢將返回兩列:一列是元素(elem)若专,另一列是該元素在所有數(shù)組中出現(xiàn)的次數(shù)(count)。

這條sql在代碼中執(zhí)行時間是0.7秒沛豌,還是時間太長叫确,畢竟數(shù)據(jù)庫的數(shù)據(jù)量太大,搜了很多方法,已經(jīng)是我能做到的最快查詢了照瘾。

關(guān)系型數(shù)據(jù)庫 不適合做海量數(shù)據(jù)計算查詢逃默。

這個業(yè)務(wù)場景牽扯到了海量數(shù)據(jù)的統(tǒng)計,并不適合使用關(guān)系型數(shù)據(jù)庫,如果想要真正的做到毫秒級的查詢,需要從設(shè)計上改變數(shù)據(jù)的存儲結(jié)果。比如使用cilckhouse、hive等存儲計算然低。

3. 引入緩存機(jī)制

減少查詢數(shù)據(jù)庫的次數(shù),決定引入本地緩存機(jī)制刚照。選擇了Caffeine作為緩存框架,易于與Spring集成喧兄。

分析業(yè)務(wù)后无畔,當(dāng)天的統(tǒng)計數(shù)據(jù)必須查詢數(shù)據(jù)庫,但是查詢歷史日期的采用緩存的方式吠冤。如果業(yè)務(wù)中對時效性不敏感浑彰,也可以緩存當(dāng)天的數(shù)據(jù),每隔一段時間更新一次拯辙。我這里采用緩存歷史日期的數(shù)據(jù)郭变。

1.引入Caffeine依賴

<dependency>

? ? <groupId>com.github.ben-manes.caffeine</groupId>

? ? <artifactId>caffeine</artifactId>

? ? <version>3.1.8</version>

</dependency>

2.配置Caffeine緩存

創(chuàng)建一個專門的Caffeine緩存配置。使用本地緩存選擇淘汰策略很重要涯保,由于我的業(yè)務(wù)場景使根據(jù)實現(xiàn)來查詢诉濒,所以Caffeine將按照最近最少使用(LRU)的策略來淘汰舊數(shù)據(jù)成符合業(yè)務(wù)。

import com.github.benmanes.caffeine.cache.Caffeine;

import org.springframework.cache.CacheManager;

import org.springframework.cache.annotation.EnableCaching;

import org.springframework.cache.caffeine.CaffeineCacheManager;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

@Configuration

@EnableCaching

public class CacheConfig {

? ? @Bean

? ? public CacheManager cacheManager() {

? ? ? ? CaffeineCacheManager cacheManager = new CaffeineCacheManager();

? ? ? ? cacheManager.setCaffeine(Caffeine.newBuilder()

? ? ? ? ? ? ? ? .maximumSize(500)

? ? ? ? ? ? ? ? .expireAfterWrite(60, TimeUnit.MINUTES));

? ? ? ? return cacheManager;

? ? }

}

3.修改ruleHitRate方法來使用Caffeine緩存

在計算昨天命中率的邏輯前加入緩存檢查和更新的邏輯夕春。

使用Caffeine緩存:

@Autowired

private CacheManager cacheManager; // 注入Spring的CacheManager

private static final String YESTERDAY_HIT_RATE_CACHE = "hitRateCache"; // 緩存名稱

@Override

public RuleHitRateResponse ruleHitRate(LocalDate currentDate) {

? ? // ... 其他代碼 ...

? ? // 使用緩存獲取昨天的命中率

? ? double hitRate = cacheManager.getCache(YESTERDAY_HIT_RATE_CACHE).get(currentDate.minusDays(1), () -> {

? ? // 查詢數(shù)據(jù)庫

? ? ? ? Map<String, String> hitRateList = dataTunnelClient.selectTransHandleFlowByTime(currentDate.minusDays(1));


? // ... 其他代碼 ...

? // 返回計算后的結(jié)果

? ? ? ? return hitRate;

? ? });

? ? // ... 其他代碼 ...

}

總結(jié)

最后未荒,測試接口,成功將接口從30秒降低到了0.8秒以內(nèi)及志。

這次優(yōu)化讓我重新真正審視了關(guān)系型數(shù)據(jù)庫的劣勢片排。選擇哪種類型的數(shù)據(jù)庫,取決于具體的應(yīng)用場景和需求速侈。

關(guān)系型數(shù)據(jù)庫(Mysql划纽、Oracle等)適合事務(wù)性強(qiáng)、數(shù)據(jù)一致性和完整性要求高的應(yīng)用锌畸。

列式數(shù)據(jù)庫(HBase勇劣、ClickHouse等)則適合大數(shù)據(jù)量的分析和統(tǒng)計,特別是在讀取性能方面有顯著優(yōu)勢潭枣。

此次的業(yè)務(wù)場景顯然更適合使用列式數(shù)據(jù)庫比默,所以導(dǎo)致使用關(guān)系型數(shù)據(jù)庫無論如何也不能夠達(dá)到足夠高的性能。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末盆犁,一起剝皮案震驚了整個濱河市命咐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌谐岁,老刑警劉巖醋奠,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件榛臼,死亡現(xiàn)場離奇詭異,居然都是意外死亡窜司,警方通過查閱死者的電腦和手機(jī)沛善,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來塞祈,“玉大人金刁,你說我怎么就攤上這事∫樾剑” “怎么了尤蛮?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長斯议。 經(jīng)常有香客問我产捞,道長,這世上最難降的妖魔是什么哼御? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任轧葛,我火速辦了婚禮,結(jié)果婚禮上艇搀,老公的妹妹穿的比我還像新娘尿扯。我一直安慰自己,他們只是感情好焰雕,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布衷笋。 她就那樣靜靜地躺著,像睡著了一般矩屁。 火紅的嫁衣襯著肌膚如雪辟宗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天吝秕,我揣著相機(jī)與錄音泊脐,去河邊找鬼。 笑死烁峭,一個胖子當(dāng)著我的面吹牛容客,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播约郁,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼缩挑,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了鬓梅?” 一聲冷哼從身側(cè)響起供置,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎绽快,沒想到半個月后芥丧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體紧阔,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年续担,在試婚紗的時候發(fā)現(xiàn)自己被綠了擅耽。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡赤拒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出诱鞠,到底是詐尸還是另有隱情挎挖,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布航夺,位于F島的核電站蕉朵,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏阳掐。R本人自食惡果不足惜始衅,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望缭保。 院中可真熱鬧汛闸,春花似錦、人聲如沸艺骂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽钳恕。三九已至别伏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間忧额,已是汗流浹背厘肮。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留睦番,地道東北人类茂。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像托嚣,于是被迫代替她去往敵國和親大咱。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容