10年老兵带你学Java(第21课):性能优化与调优 - JVM / SQL / 接口全面优化
本课目标理解性能优化的核心指标RT响应时间、QPS、TPS掌握 JVM 调优的基本思路和常用参数掌握 MySQL 慢查询优化的方法学会使用 Redis 缓存减少数据库压力掌握接口性能优化的常用技巧一、性能优化的基础认知1.1 核心性能指标指标全称含义RTResponse Time一次请求的响应时间越低越好QPSQueries Per Second每秒查询数衡量系统吞吐能力TPSTransactions Per Second每秒事务数CPU使用率-处理器繁忙程度越低说明越不浪费内存使用率-内存占用情况过高会导致频繁GC或OOM行业参考普通接口 RT50-200ms缓存命中后 RT1-5ms高并发系统 QPS1000-100001.2 性能优化的原则不要过早优化先把功能做对再考虑优化先找瓶颈用工具定位哪里慢再针对性优化遵循二八定律80%的性能问题来自20%的代码监控先行没有数据支撑的优化都是瞎猜1.3 排查问题的工具Top / Htop查看CPU、内存占用Arthas阿里巴巴开源的Java诊断工具能看方法耗时、调用链SkyWalking全链路追踪能看到每个请求在各服务的耗时MySQL Slow Query Log慢查询日志Redis MONITOR查看所有Redis命令二、JVM 调优2.1 JVM 内存结构Java 程序运行时内存分为几大区域堆内存Heap对象存放的地方 ├── Young Generation新生代 │ ├── Eden 区新对象分配区 │ └── Survivor 区S0、S1存活对象升级前的中转区 └── Old Generation老年代长期存活的对象 非堆内存Non-Heap └── Metaspace类的元数据GC垃圾回收流程新对象 → Eden区 → Survivor区S0/S1反复 → 老年代 → Full GC Minor GC每次清理Young区 多次存活才升级 Old区满时触发2.2 常见 GC 问题频繁 Full GC老年代满了频繁触发全局垃圾回收系统卡顿OOMOutOfMemoryError内存不够用了GC停顿Stop The WorldGC时所有用户线程暂停高并发下明显卡顿2.3 JVM 参数配置常用的 JVM 调优参数Spring Boot 启动时加java-jarapp.jar\-Xms256m\# 堆最小内存-Xmx512m\# 堆最大内存生产环境建议设为一样避免动态扩展-Xmn128m\# 新生代大小-XX:MetaspaceSize128m\# 元空间大小-XX:UseG1GC\# 使用G1垃圾收集器延迟可控适合大内存-XX:MaxGCPauseMillis200# 最大GC停顿时间生产环境推荐配置4核8G服务器java-jarapp.jar\-Xms2g\-Xmx2g\-Xmn1g\-XX:UseG1GC\-XX:MaxGCPauseMillis200\-XX:HeapDumpOnOutOfMemoryError\-XX:HeapDumpPath/var/logs/java_heap.hprof\-Xloggc:/var/logs/java_gc.log\-XX:PrintGCDetails\-XX:PrintGCDateStamps2.4 使用 Arthas 定位慢方法安装 Arthascurl-Ohttps://arthas.aliyun.com/arthas-boot.jarjava-jararthas-boot.jar常用命令# 查看最耗时的方法Top 10stack-n10# 监听方法调用统计耗时watchcom.example.scanorder.service.* *{params, returnObj, throwExp}-x3# 生成火焰图方法调用栈性能分析profiler start--formathtml# 等待一段时间后profiler stop三、MySQL 慢查询优化3.1 开启慢查询日志-- 查看慢查询配置SHOWVARIABLESLIKEslow_query%;SHOWVARIABLESLIKElong_query_time;-- 开启慢查询临时SETGLOBALslow_query_logON;SETGLOBALlong_query_time1;-- 超过1秒记录-- 永久配置在 my.cnf 中添加slow_query_log1slow_query_log_file/var/log/mysql/slow.log long_query_time13.2 EXPLAIN 分析查询执行EXPLAIN看查询执行计划EXPLAINSELECT*FROMordersWHEREtable_id1ANDstatus1ORDERBYcreate_timeDESCLIMIT20;重点关注字段含义好值type访问类型const ref range ALLkey实际使用的索引-rows扫描行数越少越好Extra额外信息Using index Using filesort最差情况type ALLrows几十万 → 必须优化3.3 常见优化方法① 加索引-- 给常用查询条件加索引CREATEINDEXidx_table_statusONorders(table_id,status);CREATEINDEXidx_create_timeONorders(create_time);-- 复合索引遵守最左前缀原则-- 索引为 (a, b, c) 时-- ✅ 能命中a / a,b / a,b,c-- ❌ 不能命中b / c / b,c② 避免 SELECT *-- 禁止查出所有字段包括不需要的SELECT*FROMordersWHEREtable_id1;-- 优化只查需要的字段SELECTid,order_no,total_amount,statusFROMordersWHEREtable_id1;③ 分页优化-- 低效OFFSET 很大时MySQL先扫描前10000条再返回SELECT*FROMordersORDERBYidLIMIT10000,20;-- 优化利用ID先定位再往后取SELECT*FROMordersWHEREid(SELECTidFROMordersORDERBYidLIMIT10000,1)ORDERBYidLIMIT20;④ 避免大事务-- ❌ 错误在一个事务里查大量数据或循环更新BEGIN;SELECT*FROMorders;-- 查出10万条UPDATEordersSETstatus1WHEREidIN(...);-- 再逐条更新COMMIT;-- ✅ 正确小事务分批处理UPDATEordersSETstatus1WHEREidBETWEEN1AND1000;UPDATEordersSETstatus1WHEREidBETWEEN1001AND2000;-- ...四、Redis 缓存优化4.1 为什么要用缓存数据库每秒能承受的查询次数是有限的MySQL一般 1-5万 QPS而 Redis 能轻松达到 10万 QPS。缓存策略读多写少场景请求 → 先查Redis → 有数据直接返回命中缓存 ↓ 没有 查MySQL → 返回数据 → 写入Redis → 返回4.2 在 Spring Boot 中使用 Redis配置application.ymlspring:redis:host:localhostport:6379database:0timeout:3000mscache:type:redis注解方式最简单ServicepublicclassDishServiceImpl{// 查询菜品时自动缓存key dish:: idTTL 10分钟Cacheable(valuedish,key#id)publicDishgetById(Longid){returndishMapper.selectById(id);}// 更新菜品时自动删除缓存CacheEvict(valuedish,key#id)publicvoidupdate(Dishdish){dishMapper.updateById(dish);}}手动操作 Redis处理复杂逻辑ServicepublicclassOrderService{AutowiredprivateStringRedisTemplateredisTemplate;publicListDishrecommendDishes(){Stringkeyrecommend:dishes:home;// 先查缓存StringcachedredisTemplate.opsForValue().get(key);if(cached!null){returnJSON.parseArray(cached,Dish.class);}// 缓存没有查数据库ListDishdishesdishMapper.selectRecommended();// 写入缓存10分钟过期redisTemplate.opsForValue().set(key,JSON.toJSONString(dishes),10,TimeUnit.MINUTES);returndishes;}}4.3 缓存常见问题问题解决方案缓存穿透查询不存在的数据布隆过滤器 / 缓存空值缓存击穿热点key过期瞬间大量请求互斥锁 / 永不过期缓存雪崩大量key同时过期过期时间加随机数数据一致性缓存和数据库不一致最终一致性 延迟双删五、接口性能优化技巧5.1 异步处理非核心逻辑// ❌ 同步发短信太慢阻塞接口响应publicvoidcreateOrder(Orderorder){orderMapper.insert(order);smsService.send(order.getPhone(),下单成功);// 短信发2秒接口等2秒}// ✅ 异步用线程池或消息队列Async// Spring的异步注解publicvoidsendSmsAsync(Stringphone,Stringmsg){smsService.send(phone,msg);}publicvoidcreateOrder(Orderorder){orderMapper.insert(order);sendSmsAsync(order.getPhone(),下单成功);// 不等待发完就返回}5.2 批量操作减少数据库交互// ❌ 低效循环单条插入100条for(Dishdish:dishList){dishMapper.insert(dish);// 100次数据库交互}// ✅ 高效批量插入if(!dishList.isEmpty()){dishService.saveBatch(dishList);// 1次数据库交互}5.3 预编译 SQLMyBatis-Plus 默认使用预编译SQL天然防SQL注入且性能更好。5.4 连接池配置spring:datasource:hikari:maximum-pool-size:20# 最大连接数minimum-idle:5# 最小空闲连接connection-timeout:30000# 获取连接超时msidle-timeout:600000# 空闲连接超时max-lifetime:1800000# 连接最大生命周期六、本课作业使用 Arthas 的dashboard命令查看 Java 进程的 CPU 和内存占用给订单表加上idx_table_status索引用 EXPLAIN 对比优化前后在菜品查询接口中加入 Redis 缓存注解方式用 Arthastrace命令找出扫码点餐系统中最慢的3个方法七、下一课预告下一课最后一课我们将进行课程总结与职业规划回顾整个课程的知识体系并分享面试技巧、简历优化策略和offer选择建议。