SpringBoot首筆交易慢問題排查與優(yōu)化方案
問題背景
在我們的微服務系統(tǒng)中,首筆交易響應明顯偏慢,經(jīng)過初步排查發(fā)現(xiàn):
- Flowable 流程部署、Redis 連接建立、PageHelper 代理生成和 Hibernate Validator 校驗等操作均集中在首筆交易時進行;
- 后續(xù)交易響應迅速,說明業(yè)務邏輯本身并無性能瓶頸,而主要問題出在各類資源的首次初始化上。
這種“懶加載”機制雖然能夠延遲資源加載,但在首筆交易時往往會導致嚴重延時,影響整體體驗。實際項目中需平衡啟動速度與首次響應效率,主動預熱關(guān)鍵組件。
排查步驟
1. 日志分析
首先,將日志級別調(diào)為 DEBUG
,詳細觀察首筆交易與后續(xù)交易之間的差異。
在 Flowable 工作流啟動時,日志中會出現(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)用時會看到大量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)用時,系統(tǒng)才開始部署流程、建立 Redis 連接以及加載其它第三方組件,從而導致延遲。
2. 性能工具定位
由于單純依賴日志排查比較繁瑣,我們還使用了 Java VisualVM(JDK 自帶工具,也可選擇其它工具)進行采樣分析。
在 VisualVM 中選擇目標進程后通過 CPU 取樣,示意圖如下(也可配置JMX遠程連接)。
觀察結(jié)果如下:
發(fā)現(xiàn)首筆交易相比后續(xù)交易多出以下方法的調(diào)用(省略的部分二方包慢代碼):
com.github.pagehelper.dialect.auto.DataSourceAutoDialect.<init>
org.hibernate.validator.internal.engine.ValidatorImpl.validate()
這些方法的初始化也成為首筆交易慢的原因之一。
優(yōu)化方案:提前預熱各種資源
針對上述問題,我們的優(yōu)化思路很簡單:提前初始化各項資源,確保首筆交易時不再觸發(fā)大量懶加載。為此,我們將所有預熱操作改寫成基于 ApplicationRunner 的實現(xiàn),保證在 Spring Boot 啟動后就自動執(zhí)行。
1. Flowable 流程部署預熱
在應用啟動時,通過掃描 BPMN 文件提前部署流程,避免在交易中首次部署導致延遲。
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("自動部署流程"); for (Resource resource : resources) { deploymentBuilder.addInputStream(resource.getFilename(), resource.getInputStream()); } deploymentBuilder.deploy(); System.out.println("流程定義已部署,數(shù)量:" + resources.length); } }
2. Redis 連接預熱
利用 ApplicationRunner 發(fā)送一次 PING 請求,提前建立 Redis 連接,避免首筆交易時因連接建立而耗時。
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 預熱
通過執(zhí)行一條簡單的查詢語句,觸發(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()); } } }
(請確保配置文件中已開啟 SQL Runner 功能:mybatis-plus.global-config.enable-sql-runner=true
)
4. Hibernate Validator 預熱
通過一次 dummy 校驗操作,提前加載 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 預熱(可選)
如果使用 Undertow 作為內(nèi)嵌服務器,也可以通過主動發(fā)送 HTTP 請求預熱相關(guān)資源。此外,在配置文件中開啟過濾器提前初始化也有助于降低延遲。
在 application.yml
中設(shè)置:
server: undertow: eager-init-filters: true
再通過下面的代碼發(fā)送一次預熱請求:
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é)
通過上述方案,我們將 Flowable 流程部署、Redis 連接、PageHelper 初始化、Hibernate Validator 校驗和 Undertow 相關(guān)組件的預熱操作全部遷移到 ApplicationRunner 中,在應用啟動后就自動執(zhí)行。這樣,首筆交易時不再需要進行大量初始化工作,各項資源已預先加載,確保后續(xù)請求能達到毫秒級響應,大大提升了用戶體驗并避免了無效的監(jiān)控告警。
以上就是SpringBoot首筆交易慢問題排查與優(yōu)化方案的詳細內(nèi)容,更多關(guān)于SpringBoot首筆交易慢問題的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
淺談SpringBoot項目如何讓前端開發(fā)提高效率(小技巧)
這篇文章主要介紹了淺談SpringBoot項目如何讓前端開發(fā)提高效率(小技巧),主要介紹了Swagger和Nginx提高效率的方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-04-04java書店系統(tǒng)畢業(yè)設(shè)計 用戶模塊(3)
這篇文章主要介紹了java書店系統(tǒng)畢業(yè)設(shè)計,第三步系統(tǒng)總體設(shè)計,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-10-10springBoot動態(tài)加載jar及如何將類注冊到IOC
在SpringBoot項目中動態(tài)加載jar文件并將其類注冊到IOC容器是一種高級應用方式,,這種方法為SpringBoot項目提供了更靈活的擴展能力,使得項目可以在不修改原有代碼的基礎(chǔ)上增加新的功能模塊,感興趣的朋友一起看看吧2024-11-11Spring實現(xiàn)資源的動態(tài)加載和卸載的方法小結(jié)
這篇文章主要介紹了Spring實現(xiàn)資源的動態(tài)加載和卸載的方法小結(jié),文中通過代碼示例講解的非常詳細,對大家的學習或工作有一定的幫助,需要的朋友可以參考下2024-06-06Java中LinkedHashSet的實現(xiàn)原理詳解
這篇文章主要介紹了Java中LinkedHasSet的實現(xiàn)原理詳解,LinkedHashSet?是具有可預知迭代順序的?Set?接口的哈希表和鏈接列表實現(xiàn),此實現(xiàn)與HashSet?的不同之處在于,后者維護著一個運行于所有條目的雙重鏈接列表,需要的朋友可以參考下2023-09-09