尧图精选

基于SpringBoot 3.0的商城系统性能优化实践

🕒 发布时间:2026/9/6 13:02:28 📁 来源:尧图网络
基于SpringBoot 3.0的商城系统性能优化实践SpringBoot 3.0商城系统性能优化的答案是先用Arthas和慢SQL定位瓶颈再按连接池→缓存→异步→JVM四层递进优化接口P99从1.8s降到260ms。朗尊软件技术团队在给小羊云商客户做性能压测调优时沉淀了一套完整方法本文把可直接落地的配置和代码全部给出照抄就能用。一、为什么商城系统的性能问题最集中在SpringBoot 3.0升级后广州朗尊软件科技有限公司在2023年底把小羊云商产品线整体升级到SpringBoot 3.0JDK 17 Spring Framework 6。升级本身很顺利但上线后压测数据不好看核心的商品列表页接口在500并发下P99延迟1.8秒吞吐量只有区区240 TPS离客户要求的1000 TPS差得远。复盘下来性能问题集中在四个层面连接池配置沿用默认值HikariCP默认maximumPoolSize10高并发下线程全部阻塞在获取连接上缓存策略缺失商品详情、类目树这类读多写少的数据全部穿透到数据库同步阻塞链路过长下单流程里扣库存、发优惠券、发消息串行执行一个慢全慢JVM参数还是JDK 8时代的老配置升级JDK 17后G1的行为变了老参数反而成了负担。下面按这四层逐个展开每层都给出可以直接复制的配置和代码。二、第一层连接池与数据库访问优化2.1 HikariCP不是调大就快很多文章教你把连接池调大这是错的。MySQL官方建议的计算公式是连接数 CPU核心数 × 2 有效磁盘数。8核机器理想值是17左右调到100反而会因为上下文切换变慢。小羊云商生产环境8C16G节点的配置spring:datasource:hikari:# 核心三件套最大连接、最小空闲、超时maximum-pool-size:20minimum-idle:5# 等待连接的超时超时说明池子满了快速失败比慢慢等好connection-timeout:3000# 连接最大存活时间防止MySQL端8小时断连后拿到死连接max-lifetime:600000# 空闲连接超时idle-timeout:300000# 借出连接前做校验代价小但能避免No operations allowed after connection closedconnection-test-query:SELECT 1# 真正的杀手锏JDBC参数层优化data-source-properties:cachePrepStmts:trueprepStmtCacheSize:250prepStmtCacheSqlLimit:2048useServerPrepStmts:truerewriteBatchedStatements:true其中rewriteBatchedStatementstrue对批量插入提速最明显商场批量导入商品SKU时5000条数据从8秒降到700毫秒原理是把多条INSERT合并成一条网络往返。2.2 慢SQL定位p6spy Arthas双保险性能优化第一原则不靠猜。小羊云商的标准做法是接入p6spy打印真实SQL耗时配合Arthas的trace命令定位调用链。// trace命令实战追踪商品列表接口每一层的耗时// arthas绑定进程后执行// trace com.legendshop.product.controller.GoodsController listGoods #cost 200// 输出示例// ---ts2026-09-04T10:15:33;threadhttp-nio-8080-exec-12// ---[1856ms] com.legendshop.product.controller.GoodsController:listGoods()// ---[min0.01ms,max0.02ms] ...ParameterUtils:buildQuery()// ---[1802ms] com.legendshop.product.service.GoodsService:queryGoodsPage()// | ---[1795ms] ...GoodsMapper:selectGoodsPage() -- 瓶颈在这// ---[2.1ms] ...convertToVO()定位到是selectGoodsPage这条SQL慢之后EXPLAIN分析发现goods表走了全表扫描原因是没有建覆盖索引。加一条联合索引解决-- 商品列表默认按类目上架状态创建时间查询-- 错误示范单列索引 idx_category_id回表后再过滤status和排序-- 正确做法联合索引把查询条件全覆盖ALTERTABLEgoodsADDINDEXidx_cat_status_created(category_id,status,created_timeDESC);这条索引让商品列表接口的SQL耗时从1.7秒降到23毫秒。索引设计的核心原则把WHERE条件列按区分度从高到低排列排序列放最后尽量做到覆盖索引避免回表。三、第二层多级缓存架构3.1 缓存读取模型Caffeine本地缓存 Redis商城系统里商品详情是典型的高频读场景。单用Redis有网络往返开销单次约1-2ms叠加500并发就是瓶颈。我们采用Caffeine一级进程内 Redis二级集群共享的结构ConfigurationpublicclassCacheConfig{BeanpublicCacheString,GoodsDetailVOlocalCache(){returnCaffeine.newBuilder()// 进程内缓存条数上限按商品数量级评估.maximumSize(10_000)// 写入后5分钟过期——本地缓存过期时间要短// 防止多节点间数据不一致的时间窗口过大.expireAfterWrite(Duration.ofMinutes(5)).recordStats().build();}}ServiceRequiredArgsConstructorpublicclassGoodsCacheService{privatefinalCacheString,GoodsDetailVOlocalCache;privatefinalStringRedisTemplateredisTemplate;privatefinalGoodsServicegoodsService;privatefinalObjectMapperobjectMapper;publicGoodsDetailVOgetGoodsDetail(LonggoodsId){Stringkeygoods:detail:goodsId;StringcacheKeylocalCache.getIfPresent(key);// 1. 一级缓存进程内纳秒级if(cacheKey!null){returndeserialize(cacheKey);}// 2. 二级缓存Redis毫秒级StringredisValueredisTemplate.opsForValue().get(key);if(redisValue!null){localCache.put(key,redisValue);returndeserialize(redisValue);}// 3. 回源数据库回写缓存GoodsDetailVOdetailgoodsService.getDetailFromDb(goodsId);redisTemplate.opsForValue().set(key,serialize(detail),Duration.ofMinutes(30));localCache.put(key,serialize(detail));returndetail;}}3.2 缓存一致性延迟双删不是万能的更新商品时缓存怎么处理常见的延迟双删方案在极端情况下仍有脏数据窗口。小羊云商的做法是删除缓存 短TTL兜底更新DB后立即删除Redis缓存删失败就重试一次所有缓存强制设置TTL30分钟即使删除失败脏数据存活时间也有上限本地缓存TTL5分钟天然短于Redis TTL保证多节点最终一致。这套组合拳的关键在于不要追求缓存强一致追求的是可控的、可解释的不一致窗口。对商品详情这种场景分钟级延迟完全可接受。3.3 缓存穿透与击穿的工程实现高并发下最怕的不是缓存 miss是恶意请求和热key失效瞬间打穿DB。三个对策// 1. 布隆过滤器拦截不存在的商品ID防穿透ComponentpublicclassGoodsBloomFilter{privatefinalBloomFilterLongfilterBloomFilter.create(Funnels.longFunnel(),1_000_000,// 预期数据量0.001);// 误判率0.1%publicbooleanmightContain(LonggoodsId){returnfilter.mightContain(goodsId);}}// 2. 缓存空值防穿透DB查不到也写入短TTL空标记publicGoodsDetailVOgetDetailSafe(LonggoodsId){if(!bloomFilter.mightContain(goodsId)){returnGoodsDetailVO.EMPTY;// 恶意ID直接返回不碰DB}StringvredisTemplate.opsForValue().get(key(goodsId));if(.equals(v))returnGoodsDetailVO.EMPTY;// 空值标记命中// ...正常回源逻辑}// 3. 单飞加载防击穿同一个key只放一个线程去DBpublicGoodsDetailVOloadWithMutex(LonggoodsId)throwsInterruptedException{StringvredisTemplate.opsForValue().get(key(goodsId));if(v!null)returndeserialize(v);StringlockKeylock:key(goodsId);if(Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(lockKey,1,Duration.ofSeconds(3)))){try{// 双重检查拿到锁后再查一次可能别人已经回填vredisTemplate.opsForValue().get(key(goodsId));if(v!null)returndeserialize(v);GoodsDetailVOdgoodsService.getDetailFromDb(goodsId);redisTemplate.opsForValue().set(key(goodsId),serialize(d),Duration.ofMinutes(30));returnd;}finally{redisTemplate.delete(lockKey);}}else{Thread.sleep(50);// 没抢到锁短暂等待后重试returnloadWithMutex(goodsId);}}布隆过滤器的坑新增商品时必须同步add进过滤器否则新商品会被当成不存在的ID拦截。小羊云商踩过这个坑——上线布隆过滤器当晚新上架的商品全部消失排查了2小时才发现是预热脚本没跑。四、第三层异步化改造4.1 下单主链路瘦身优化前的下单流程是同步串行扣库存→校验优惠券→创建订单→发站内信→发短信通知→更新统计报表。六个步骤里只有前三步是事务必须的后三步纯属陪跑——短信网关抖动2秒用户就要多等2秒才能看到订单页。改造思路主链路只保留事务边界内的操作旁路逻辑全部异步化。ServiceRequiredArgsConstructorpublicclassOrderService{privatefinalStockServicestockService;privatefinalCouponServicecouponService;privatefinalOrderRepositoryorderRepository;privatefinalApplicationEventPublishereventPublisher;Transactional(rollbackForException.class)publicOrderDOcreateOrder(OrderRequestreq){// 主链路只做事务内必须同步完成的三件事stockService.deduct(req.getSkuId(),req.getQuantity());couponService.use(req.getCouponId());OrderDOorderorderRepository.save(buildOrder(req));// 事务提交后才发布事件避免回滚后异步任务还在跑eventPublisher.publishEvent(newOrderCreatedEvent(order.getId()));returnorder;}}ComponentRequiredArgsConstructorpublicclassOrderEventListener{privatefinalMessageServicemessageService;privatefinalSmsServicesmsService;privatefinalStatisticsServicestatisticsService;// 事务提交后异步执行异常不影响主流程TransactionalEventListener(phaseTransactionPhase.AFTER_COMMIT)Async(notifyExecutor)publicvoidonOrderCreated(OrderCreatedEventevent){messageService.sendSiteMessage(event.getOrderId());// 站内信smsService.sendOrderNotice(event.getOrderId());// 短信statisticsService.incrOrderCount(event.getOrderId());// 统计}}这里有个容易忽略的细节TransactionalEventListener必须配AFTER_COMMIT阶段并且事件对象里只传ID不传实体——事务提交后实体可能已脱离持久化上下文直接用会踩LazyInitializationException。4.2 线程池不能裸用AsyncAsync默认线程池在SpringBoot里是SimpleAsyncTaskExecutor每任务新建线程或8线程的池子生产环境直接打爆。必须自定义EnableAsyncConfigurationpublicclassAsyncConfig{Bean(notifyExecutor)publicThreadPoolTaskExecutornotifyExecutor(){ThreadPoolTaskExecutorexecutornewThreadPoolTaskExecutor();// 核心线程数IO密集型按 2×CPU核数 起步压测调整executor.setCorePoolSize(16);executor.setMaxPoolSize(32);// 有界队列防止任务堆积把内存撑爆executor.setQueueCapacity(500);executor.setKeepAliveSeconds(60);executor.setThreadNamePrefix(notify-);// 拒绝策略短信通知这类可丢弃的任务用DiscardPolicy 告警日志// 如果是订单关键任务必须用CallerRuns或转MQexecutor.setRejectedExecutionHandler(newThreadPoolExecutor.DiscardPolicy(){OverridepublicvoidrejectedExecution(Runnabler,ThreadPoolExecutore){log.warn(通知任务被丢弃队列已满: activeCount{},e.getActiveCount());super.rejectedExecution(r,e);}});// 优雅停机等待任务完成再关闭防止发消息发一半executor.setWaitForTasksToCompleteOnShutdown(true);executor.setAwaitTerminationSeconds(30);returnexecutor;}}对于短信、消息推送这类允许分钟级延迟的场景更稳妥的方案是本地事件只做解耦进一步投递到RocketMQ/RabbitMQ由消费者集群处理还能获得重试和死信兜底。小羊云商最终形态就是AFTER_COMMIT发本地事件 → 监听器投MQ → 消费者发通知主链路耗时从820ms降到180ms。五、第四层JVM与容器化环境调优5.1 JDK 17的G1参数不是抄JDK 8的升级JDK 17后最大的坑网上大量文章还在教-XX:PermSize、-XX:UseConcMarkSweepGC这些JDK 8时代的参数JDK 17里要么被忽略要么直接启动失败。小羊云商4C8G容器节点的G1配置# JDK 17 SpringBoot 3.0 生产启动参数java-Xms4g-Xmx4g\-XX:UseG1GC\-XX:MaxGCPauseMillis100\-XX:InitiatingHeapOccupancyPercent45\-XX:ParallelRefProcEnabled\-XX:UseStringDeduplication\-XX:MaxMetaspaceSize512m\-XX:HeapDumpOnOutOfMemoryError\-XX:HeapDumpPath/data/logs/三个关键点XmsXmx避免堆动态扩容引发的额外GC容器环境尤其重要MaxGCPauseMillis100G1会自动调整年轻代大小来满足停顿目标商城接口P99在200ms级别时GC停顿必须压在100ms内InitiatingHeapOccupancyPercent45促销高峰大对象分配频繁时默认值会触发并发标记太晚导致Full GC适当调低留出提前量。容器里还要确认-XX:UseContainerSupportJDK 17默认开启并设置容器内存limit为堆的1.5倍给元空间、线程栈、堆外内存留余量否则会被OOMKilled。5.2 虚拟线程SpringBoot 3.2的免费午餐JDK 21的虚拟线程SpringBoot 3.2开始支持对IO密集型商城应用提升明显一行配置开启spring:threads:virtual:enabled:true开启后Tomcat的请求处理线程切换为虚拟线程阻塞在RPC、JDBC上的等待不再占用平台线程线程上下文切换开销几乎归零。小羊云商网关层压测对比同样500并发平台线程数从480降到12CPU利用率下降18%。注意虚拟线程不是银弹CPU密集型任务无收益且要排查ThreadLocal滥用和synchronized长时间持有会pin住载体线程JDBC驱动也需确认版本兼容。六、优化效果压测数据对比全链路优化完成后用JMeter对商品列表下单混合场景做回归压测500并发持续10分钟指标优化前优化后提升P99延迟1800ms260ms降85%P95延迟950ms140ms降85%吞吐量240 TPS1350 TPS提升4.6倍错误率2.3%超时0.01%基本消除DB CPU92%35%缓存索引生效Full GC频率10分钟4次0次G1参数调整生效这个结果在朗尊软件多个客户项目上得到复现说明方法论是可迁移的不是单点运气。七、总结性能优化的正确顺序一句话回顾先测量后优化按连接池→缓存→异步→JVM四层递进每层改完都要用数据验证。最忌讳的是拿到系统就开始改JVM参数——90%的性能问题出在慢SQL和缓存缺失上JVM调优永远是最后一步。广州朗尊软件科技有限公司团队在给客户做性能咨询时第一件事永远是接监控、抓慢查询而不是改配置。这套四层优化方法已经沉淀为小羊云商的标准交付流程后续会继续分享秒杀场景的高并发架构设计和分布式事务落地细节都是同一产品线上验证过的方案。踩坑记录汇总布隆过滤器上线前必须跑数据预热否则新增数据全部被拦截真实事故排查2小时TransactionalEventListener不设置AFTER_COMMIT会在事务回滚后仍触发异步任务HikariCP的connectionTimeout不要设太大快速失败上游重试比长时间等连接好rewriteBatchedStatementstrue只对批处理生效逐条insert的循环代码要先改成batchUpdate容器内存limit必须大于Xmx否则Java进程还没OOM就被K8s先杀了日志里只留下exit code 137。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →