核心内容摘要
mantahaya1777kino的深层含义:一段探索未知的旅程
大家好我是做后端开发多年的博主最近负责的项目日均请求100W峰值QPS 5000上线后遭遇了高并发流量冲击出现了接口响应变慢、偶尔OOM、数据库连接池满的问题。
排查了一周最终通过JVM调优缓存优化将接口平均响应时间从500ms压到80ms以内OOM彻底解决数据库压力减少70%。
今天就把这份实操性极强的调优笔记分享给大家全程结合Spring Boot实际项目不讲空洞理论只讲能直接落地的方法新手也能跟着做避开我踩过的那些坑。
先说明一下项目背景方便大家对应自己的场景Spring Boot
2.
x MyBatis-Plus Redis MySQL核心接口是商品查询、订单提交属于典型的读多写少场景高并发主要集中在商品详情查询、活动商品列表查询。
先避坑高并发下Spring Boot接口的
常见问题我踩过的3个致命坑调优前一定要先定位问题不要盲目调参我一开始就是凭感觉调JVM参数结果越调越乱反而出现了GC频繁停顿的问题。
先给大家列下我遇到的核心问题你们可以对照排查接口响应延迟飙升峰值时段商品详情接口响应从100ms涨到500ms甚至出现超时1s阈值排查发现是JVM老年代GC频繁单次GC停顿达到300ms同时缓存未命中大量请求穿透到数据库。
OOM异常偶现上线3天后出现2次java.lang.OutOfMemoryError: Java heap space导出堆dump分析发现是商品缓存未设置过期时间大量过期商品数据堆积在堆内存加上频繁创建临时对象比如JSON序列化对象导致堆内存溢出。
数据库压力过大缓存穿透时单表查询QPS达到2000数据库连接池满出现连接超时进而引发接口雪崩连锁影响其他接口。
总结高并发下Spring Boot接口的性能瓶颈往往不是接口本身的代码问题而是JVM内存分配不合理、GC策略不合适以及缓存使用不规范导致的。
调优要循序渐进先解决缓存减少数据库压力再调JVM解决内存和GC问题最后验证效果。
缓存优化实践先减压再调优性价比最高缓存是高并发场景的“救命稻草”但用不好反而会添乱比如缓存穿透、击穿、雪崩。
我这里结合Spring BootRedis分享3个核心优化点亲测能快速减少数据库压力提升接口响应。
1 缓存选型本地缓存分布式缓存结合双重保障一开始我只用到了Redis分布式缓存所有请求都要去Redis查询虽然比数据库快但Redis网络开销依然存在峰值时Redis响应也会变慢。
后来引入Caffeine本地缓存Spring Boot
x默认支持实现“本地缓存优先Redis兜底”效果立竿见影。
实操步骤直接复制代码修改参数即可//
引入依赖Spring Boot
2.
x无需额外指定版本dependencygroupIdcom.github.ben-manes.caffeine/groupIdartifactIdcaffeine/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependency//
配置Caffeine本地缓存application.ymlspring:cache:type:caffeine caffeine:spec:initialCapacity500,maximumSize2000,expireAfterWrite300s # 核心参数 cache-names:productCache,activityCache # 缓存名称对应不同业务场景//
配置Redis分布式缓存application.ymlredis:host:
127.
0.
1port:6379password:123456lettuce:pool:max-active:200# 最大连接数根据峰值QPS调整 max-idle:50# 最大空闲连接 min-idle:10# 最小空闲连接 max-wait:1000ms # 连接超时时间//
自定义缓存管理器实现本地缓存Redis双重缓存ConfigurationEnableCachingpublicclassCacheConfig{BeanpublicCacheManagercacheManager(RedisConnectionFactoryredisConnectionFactory){// 配置Caffeine本地缓存CaffeineCacheManagercaffeineCacheManagernewCaffeineCacheManager();caffeineCacheManager.setCaffeine(Caffeine.newBuilder().initialCapacity(
// 初始缓存容量.maximumSize(
// 最大缓存容量超过后LRU淘汰.expireAfterWrite(300,TimeUnit.SECONDS));// 写入后300s过期// 配置Redis分布式缓存RedisCacheManagerredisCacheManagerRedisCacheManager.builder(redisConnectionFactory).cacheDefaults(RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(
)// Redis缓存默认过期时间10分钟.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(newStringRedisSerializer())).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(newGenericJackson2JsonRedisSerializer()))).build();// 自定义缓存解析器优先使用本地缓存本地未命中则查询RedisCompositeCacheManagercompositeCacheManagernewCompositeCacheManager(caffeineCacheManager,redisCacheManager);compositeCacheManager.setFallbackToNoOpCache(false);returncompositeCacheManager;}}避坑点本地缓存的maximumSize不要设置太大我一开始设为5000导致JVM堆内存占用过高根据业务热点数据量调整一般
足够expireAfterWrite时间要比Redis短避免本地缓存和Redis数据不一致。
2 解决缓存三大问题穿透、击穿、雪崩这三个问题是高并发缓存的“老大难”我踩过缓存穿透的坑导致数据库直接被打满分享具体解决方法结合代码落地
2.
1 缓存穿透查询不存在的数据缓存未命中直接打数据库问题场景比如恶意请求查询不存在的商品IDid-1缓存未命中所有请求都穿透到数据库导致数据库压力剧增。
解决方法缓存空值有效期短比如60s 布隆过滤器拦截不存在的ID我这里用了缓存空值简单易落地布隆过滤器适合数据量极大的场景。
// 商品查询接口缓存空值示例Cacheable(valueproductCache,key#productId,unless#result null)GetMapping(/product/{productId})publicResultProductVOgetProduct(PathVariableLongproductId){//
查询数据库ProductproductproductMapper.selectById(productId);if(productnull){//
缓存空值有效期60s避免重复穿透redisTemplate.opsForValue().set(productCache::productId,null,60,TimeUnit.SECONDS);returnResult.fail(商品不存在);}//
转换VO返回ProductVOproductVOBeanUtil.copyProperties(product,ProductVO.class);returnResult.success(productVO);}
2.
2 缓存击穿热点数据过期大量请求同时打数据库问题场景比如活动商品缓存过期瞬间 thousands of 请求同时查询缓存未命中全部打数据库导致数据库过载。
解决方法互斥锁Redis分布式锁 缓存预热确保只有一个请求去查询数据库其他请求等待缓存更新。
2.
3 缓存雪崩大量缓存同时过期或Redis宕机请求全部打数据库解决方法
缓存过期时间加随机值避免同时过期
Redis集群部署避免单点故障
降级策略Redis宕机时返回默认数据或提示服务繁忙。
// 缓存过期时间加随机值示例Redis缓存时使用// 写入Redis时过期时间10分钟
秒随机值intrandomSecondsnewRandom().nextInt(
;redisTemplate.opsForValue().set(key,value,10*60randomSeconds,TimeUnit.SECONDS);
3 缓存优化补充细节决定成败缓存预热上线前通过脚本将热点数据比如活动商品、热门分类提前加载到Redis和本地缓存避免高并发时缓存未命中。
避免缓存冗余只缓存核心数据比如商品ID、名称、价格不要缓存大字段比如商品详情大字段建议用数据库分页查询或单独接口返回。
缓存更新策略读多写少场景用“Cacheable CacheEvict”更新数据时删除缓存避免缓存和数据库数据不一致写多读少场景用“双写一致性”更新数据库后同步更新缓存。
JVM调优实践Spring Boot项目直接复制参数落地缓存优化完成后数据库压力减少接下来调JVM解决OOM和GC频繁的问题。
Spring Boot项目的JVM参数一般在启动脚本start.sh中配置不同服务器内存CPU、内存参数不同我这里以4核8G服务器为例生产环境最常用分享优化后的参数和思路。
1 调优前提JVM问题定位关键步骤不要跳过调优前必须先定位JVM的问题比如是堆内存溢出、方法区溢出还是GC停顿过长。
常用工具jps查看Spring Boot进程ID比如进程ID为12345。
jstat查看GC统计信息jstat -gc 12345 1000 10每1000ms查询一次共10次重点看YGC、FGC、YGCT、FGCTGC停顿时间。
jmap导出堆dump文件jmap -dump:formatb,fileheapdump.hprof 12345然后用MAT工具分析内存泄漏。
Arthas阿里开源工具快速定位JVM问题推荐新手使用可视化操作无需复杂命令。
我当时用Arthas排查发现老年代内存占比持续升高FGC频繁每10分钟一次单次FGCT达到300ms原因是堆内存分配不合理老年代太小且临时对象JSON序列化对象过多导致频繁晋升到老年代。
2 最终优化的JVM参数4核8G服务器Spring Boot
2.
x# Spring Boot启动脚本start.sh中的JVM参数java -jar -Xms4g -Xmx4g -XX:MetaspaceSize256m -XX:MaxMetaspaceSize512m -XX:SurvivorRatio8\-XX:Us
GC -XX:MaxGCPauseMillis100-XX:ParallelRefProcEnabled\-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/var/log/heapdump.hprof\-XX:PrintGCDetails -XX:PrintGCDateStamps -Xloggc:/var/log/gc.log\your-project.jar参数详解通俗易懂避免专业术语堆砌-Xms4g -Xmx4g堆内存初始值和最大值都设为4G8G服务器堆内存占比50%最佳避免内存不足或浪费两者设为一致避免JVM频繁调整堆内存大小减少性能损耗。
-XX:MetaspaceSize256m -XX:MaxMetaspaceSize512m方法区元空间初始值256m最大值512m存储类信息、常量、静态变量Spring Boot项目依赖多元空间不能太小否则会出现Metaspace OOM。
-XX:SurvivorRatio8 Eden区:From区:To区 8:1:1Eden区存放新创建的对象比例合理能减少年轻代GC次数。
-XX:Us
GC使用G1垃圾收集器Spring Boot
x默认使用适合大堆内存能有效控制GC停顿时间避免FGC频繁。
-XX:MaxGCPauseMillis100设置GC最大停顿时间为100msG1会尽量保证单次GC停顿不超过这个值适合高并发接口对响应时间敏感。
-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPathxxxOOM时自动导出堆dump文件方便后续排查问题关键。
-XX:PrintGCDetails -XX:PrintGCDateStamps -Xloggc:xxx打印GC日志方便监控GC情况后续优化调整。
3 调优后效果对比关键指标指标调优前调优后年轻代GCYGC频率每2分钟1次单次YGCT 50ms每5分钟1次单次YGCT 20ms以内老年代GCFGC频率每10分钟1次单次FGCT 300ms每30分钟1次单次FGCT 80ms以内堆内存占用峰值70%易OOM50%左右稳定接口平均响应时间500ms80ms以内
4 JVM调优避坑点重中之重不要盲目调大堆内存比如8G服务器把-Xmx设为6G看似内存充足但会导致G1GC收集范围变大GC停顿时间变长反而影响性能。
不要禁用System.gc()虽然可以用-XX:DisableExplicitGC禁用但Spring Boot部分依赖比如Redis客户端会主动调用System.gc()禁用后可能导致内存泄漏。
元空间不要设太大MetaspaceSize和MaxMetaspaceSize设太大会浪费内存一般256m-512m足够除非项目依赖极多。
一定要留存GC日志和堆dump文件后续遇到问题能快速定位避免重复踩坑。
调优
总结与后续优化方向高并发场景下Spring Boot接口的调优核心是“先减压再调优”缓存优化优先快速减少数据库和JVM压力再进行JVM调优解决内存和GC问题最后通过监控工具持续观察逐步优化。
本次调优后项目稳定运行1个月未再出现OOM和接口超时问题峰值QPS能稳定支撑5000完全满足业务需求。
给大家
总结几个核心要点缓存优化本地缓存Redis分布式缓存结合解决穿透、击穿、雪崩三大问题细节决定成败比如缓存空值、过期时间加随机值。
JVM调优根据服务器配置合理分配堆内存和元空间使用G1GC控制GC停顿时间留存日志方便排查。
调优不是一蹴而就需要结合实际业务场景反复测试、观察逐步调整参数没有通用的“最优参数”只有“最适合自己项目的参数”。
后续优化方向供大家参考引入Redis集群主从哨兵提升Redis可用性避免单点故障。
使用Arthas持续监控JVM和接口性能实时发现问题。
接口异步化将非核心流程比如日志记录、消息推送异步处理减少接口阻塞时间。
最后大家如果在调优过程中遇到什么问题或者有更好的调优技巧欢迎在评论区交流讨论一起避坑一起提升