SpringBoot首筆交易慢問(wèn)題排查與優(yōu)化方案
問(wèn)題背景
在我們的微服務(wù)系統(tǒng)中,首筆交易響應(yīng)明顯偏慢,經(jīng)過(guò)初步排查發(fā)現(xiàn):
- Flowable 流程部署、Redis 連接建立、PageHelper 代理生成和 Hibernate Validator 校驗(yàn)等操作均集中在首筆交易時(shí)進(jìn)行;
- 后續(xù)交易響應(yīng)迅速,說(shuō)明業(yè)務(wù)邏輯本身并無(wú)性能瓶頸,而主要問(wèn)題出在各類資源的首次初始化上。
這種“懶加載”機(jī)制雖然能夠延遲資源加載,但在首筆交易時(shí)往往會(huì)導(dǎo)致嚴(yán)重延時(shí),影響整體體驗(yàn)。實(shí)際項(xiàng)目中需平衡啟動(dòng)速度與首次響應(yīng)效率,主動(dòng)預(yù)熱關(guān)鍵組件。
排查步驟
1. 日志分析
首先,將日志級(jí)別調(diào)為 DEBUG
,詳細(xì)觀察首筆交易與后續(xù)交易之間的差異。
在 Flowable 工作流啟動(dòng)時(shí),日志中會(huì)出現(xiàn)如下部署信息:
2025-03-31-15:24:25:326 [thread1] DEBUG o.f.e.i.bpmn.deployer.BpmnDeployer.deploy.72 -- Processing deployment SpringBootAutoDeployment 2025-03-31-15:24:25:340 [thread1] DEBUG o.f.e.i.b.d.ParsedDeploymentBuilder.build.54 -- Processing BPMN resource E:\gitProjects\flowableProject\target\classes\processes\eib.bpmn20.xml
同樣,Redis 連接在首次調(diào)用時(shí)會(huì)看到大量lettuce包日志,如:
2025-03-31-15:24:23:587 [XNIO-1 task-1] DEBUG io.lettuce.core.RedisClient.initializeChannelAsync0.304 -- Connecting to Redis at 10.240.75.250:7379
這些信息表明,在首次調(diào)用時(shí),系統(tǒng)才開(kāi)始部署流程、建立 Redis 連接以及加載其它第三方組件,從而導(dǎo)致延遲。
2. 性能工具定位
由于單純依賴日志排查比較繁瑣,我們還使用了 Java VisualVM(JDK 自帶工具,也可選擇其它工具)進(jìn)行采樣分析。
在 VisualVM 中選擇目標(biāo)進(jìn)程后通過(guò) CPU 取樣,示意圖如下(也可配置JMX遠(yuǎn)程連接)。
觀察結(jié)果如下:
發(fā)現(xiàn)首筆交易相比后續(xù)交易多出以下方法的調(diào)用(省略的部分二方包慢代碼):
com.github.pagehelper.dialect.auto.DataSourceAutoDialect.<init>
org.hibernate.validator.internal.engine.ValidatorImpl.validate()
這些方法的初始化也成為首筆交易慢的原因之一。
優(yōu)化方案:提前預(yù)熱各種資源
針對(duì)上述問(wèn)題,我們的優(yōu)化思路很簡(jiǎn)單:提前初始化各項(xiàng)資源,確保首筆交易時(shí)不再觸發(fā)大量懶加載。為此,我們將所有預(yù)熱操作改寫(xiě)成基于 ApplicationRunner 的實(shí)現(xiàn),保證在 Spring Boot 啟動(dòng)后就自動(dòng)執(zhí)行。
1. Flowable 流程部署預(yù)熱
在應(yīng)用啟動(dòng)時(shí),通過(guò)掃描 BPMN 文件提前部署流程,避免在交易中首次部署導(dǎo)致延遲。
import org.flowable.engine.RepositoryService; import org.flowable.engine.repository.DeploymentBuilder; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.stereotype.Component; @Component public class ProcessDeploymentRunner implements ApplicationRunner { private final RepositoryService repositoryService; public ProcessDeploymentRunner(RepositoryService repositoryService) { this.repositoryService = repositoryService; } @Override public void run(ApplicationArguments args) throws Exception { // 掃描 processes 目錄下的所有 BPMN 文件 PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); Resource[] resources = resolver.getResources("classpath:/processes/*.bpmn20.xml"); if (resources.length == 0) { System.out.println("未在 processes 目錄下找到 BPMN 文件"); return; } DeploymentBuilder deploymentBuilder = repositoryService.createDeployment() .name("自動(dòng)部署流程"); for (Resource resource : resources) { deploymentBuilder.addInputStream(resource.getFilename(), resource.getInputStream()); } deploymentBuilder.deploy(); System.out.println("流程定義已部署,數(shù)量:" + resources.length); } }
2. Redis 連接預(yù)熱
利用 ApplicationRunner 發(fā)送一次 PING 請(qǐng)求,提前建立 Redis 連接,避免首筆交易時(shí)因連接建立而耗時(shí)。
import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; @Component public class RedisWarmupRunner implements ApplicationRunner { private final StringRedisTemplate redisTemplate; public RedisWarmupRunner(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } @Override public void run(ApplicationArguments args) { try { String pingResult = redisTemplate.getConnectionFactory().getConnection().ping(); System.out.println("? Redis connection pre-warmed successfully: " + pingResult); } catch (Exception e) { System.err.println("? Redis warm-up failed: " + e.getMessage()); } } }
3. PageHelper 預(yù)熱
通過(guò)執(zhí)行一條簡(jiǎn)單的查詢語(yǔ)句,觸發(fā) PageHelper 及相關(guān) MyBatis Mapper 的初始化。
import com.baomidou.mybatisplus.extension.toolkit.SqlRunner; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; @Component public class PageHelperWarmupRunner implements ApplicationRunner { @Override public void run(ApplicationArguments args) { try { boolean result = SqlRunner.db().selectObjs("SELECT 1").size() > 0; System.out.println("? PageHelper & SqlRunner pre-warm completed, result: " + result); } catch (Exception e) { System.err.println("? PageHelper pre-warm failed: " + e.getMessage()); } } }
(請(qǐng)確保配置文件中已開(kāi)啟 SQL Runner 功能:mybatis-plus.global-config.enable-sql-runner=true
)
4. Hibernate Validator 預(yù)熱
通過(guò)一次 dummy 校驗(yàn)操作,提前加載 Hibernate Validator 相關(guān)類和反射邏輯
import jakarta.validation.Validation; import jakarta.validation.Validator; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; @Component public class ValidatorWarmupRunner implements ApplicationRunner { @Override public void run(ApplicationArguments args) { try { Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); DummyEntity dummy = new DummyEntity(); validator.validate(dummy); System.out.println("? Hibernate Validator pre-warm completed!"); } catch (Exception e) { System.err.println("? Hibernate Validator pre-warm failed: " + e.getMessage()); } } private static class DummyEntity { @jakarta.validation.constraints.NotNull private String name; } }
5. Undertow 預(yù)熱(可選)
如果使用 Undertow 作為內(nèi)嵌服務(wù)器,也可以通過(guò)主動(dòng)發(fā)送 HTTP 請(qǐng)求預(yù)熱相關(guān)資源。此外,在配置文件中開(kāi)啟過(guò)濾器提前初始化也有助于降低延遲。
在 application.yml
中設(shè)置:
server: undertow: eager-init-filters: true
再通過(guò)下面的代碼發(fā)送一次預(yù)熱請(qǐng)求:
import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; @Component public class UndertowWarmupRunner implements ApplicationRunner { @Override public void run(ApplicationArguments args) { try { RestTemplate restTemplate = new RestTemplate(); String response = restTemplate.getForObject("http://localhost:8080/health", String.class); System.out.println("? Undertow pre-warm completed, response: " + response); } catch (Exception e) { System.err.println("? Undertow pre-warm failed: " + e.getMessage()); } } }
總結(jié)
通過(guò)上述方案,我們將 Flowable 流程部署、Redis 連接、PageHelper 初始化、Hibernate Validator 校驗(yàn)和 Undertow 相關(guān)組件的預(yù)熱操作全部遷移到 ApplicationRunner 中,在應(yīng)用啟動(dòng)后就自動(dòng)執(zhí)行。這樣,首筆交易時(shí)不再需要進(jìn)行大量初始化工作,各項(xiàng)資源已預(yù)先加載,確保后續(xù)請(qǐng)求能達(dá)到毫秒級(jí)響應(yīng),大大提升了用戶體驗(yàn)并避免了無(wú)效的監(jiān)控告警。
以上就是SpringBoot首筆交易慢問(wèn)題排查與優(yōu)化方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot首筆交易慢問(wèn)題的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java使用PDFBox實(shí)現(xiàn)操作PDF文檔
這篇文章主要為大家詳細(xì)介紹了Java如何使用PDFBox實(shí)現(xiàn)操作PDF文檔,例如添加本地圖片、添加網(wǎng)絡(luò)圖片、圖片寬高自適應(yīng)、圖片水平垂直居中對(duì)齊等功能,需要的可以了解下2024-03-03淺談SpringBoot項(xiàng)目如何讓前端開(kāi)發(fā)提高效率(小技巧)
這篇文章主要介紹了淺談SpringBoot項(xiàng)目如何讓前端開(kāi)發(fā)提高效率(小技巧),主要介紹了Swagger和Nginx提高效率的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-04-04java書(shū)店系統(tǒng)畢業(yè)設(shè)計(jì) 用戶模塊(3)
這篇文章主要介紹了java書(shū)店系統(tǒng)畢業(yè)設(shè)計(jì),第三步系統(tǒng)總體設(shè)計(jì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10mybatis不加@Parm注解報(bào)錯(cuò)的解決方案
這篇文章主要介紹了mybatis不加@Parm注解報(bào)錯(cuò)的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-11-11springBoot動(dòng)態(tài)加載jar及如何將類注冊(cè)到IOC
在SpringBoot項(xiàng)目中動(dòng)態(tài)加載jar文件并將其類注冊(cè)到IOC容器是一種高級(jí)應(yīng)用方式,,這種方法為SpringBoot項(xiàng)目提供了更靈活的擴(kuò)展能力,使得項(xiàng)目可以在不修改原有代碼的基礎(chǔ)上增加新的功能模塊,感興趣的朋友一起看看吧2024-11-11Spring實(shí)現(xiàn)資源的動(dòng)態(tài)加載和卸載的方法小結(jié)
這篇文章主要介紹了Spring實(shí)現(xiàn)資源的動(dòng)態(tài)加載和卸載的方法小結(jié),文中通過(guò)代碼示例講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-06-06xxl-job對(duì)比ElasticJob使用示例詳解
這篇文章主要為大家介紹了xxl-job對(duì)比ElasticJob使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06Java中LinkedHashSet的實(shí)現(xiàn)原理詳解
這篇文章主要介紹了Java中LinkedHasSet的實(shí)現(xiàn)原理詳解,LinkedHashSet?是具有可預(yù)知迭代順序的?Set?接口的哈希表和鏈接列表實(shí)現(xiàn),此實(shí)現(xiàn)與HashSet?的不同之處在于,后者維護(hù)著一個(gè)運(yùn)行于所有條目的雙重鏈接列表,需要的朋友可以參考下2023-09-09IDEA一致卡在build時(shí)間過(guò)長(zhǎng)問(wèn)題解決
有很多小伙伴在起項(xiàng)目的時(shí)候巨慢,特別影響開(kāi)發(fā)效率,本文主要介紹了IDEA一致卡在build時(shí)間過(guò)長(zhǎng)問(wèn)題解決,具有一定的參考價(jià)值,感興趣的可以了解一下2024-06-06