1. 场景:促销活动的崩溃
接到报警短信,核心接口响应时间突破5秒,DB CPU飙到100%。
用Arthas抓取线上火焰图后发现:
---[ 4763ms ] com.example.service.OrderService.createOrder() |---[ 98% ] com.example.mapper.OrderMapper.insert() # MySQL写入 |---[ 1.5% ] RedisUtils.get() # 缓存查询
结论:
- 每秒8000+订单直接打穿MySQL
- 分布式锁使用不当导致线程堆积
2. 第一阶段优化:MySQL突围战
2.1 从全表扫描到索引优化
原SQL(执行时间1.2s):
SELECT * FROM orders WHERE user_id=123 AND status IN (1,2,3) ORDER BY create_time DESC;
优化方案:
ALTER TABLE orders ADD INDEX idx_user_status (user_id, status); # 联合索引
EXPLAIN SELECT id FROM orders WHERE user_id=123 AND status=1; # 确认索引命中
效果:单次查询从1200ms → 8ms
2.2 从单条insert到批量插入
原始代码:
for (OrderItem item : items) {orderItemMapper.insert(item); // 循环插入
}
优化代码:
orderItemMapper.batchInsert(items); // MyBatis批量插入
XML配置:
<insert id="batchInsert">INSERT INTO order_item VALUES <foreach collection="list" item="item" separator=",">(#{item.id}, #{item.orderId}, ...)</foreach>
</insert>
效果:1000条数据插入从12s → 0.8s
3. 第二阶段优化:Redis缓存设计
3.1 缓存击穿解决方案
问题场景:热点商品缓存失效瞬间,5万QPS直接打穿DB
解决方案:
public Product getProduct(Long id) {// 1. 尝试从缓存获取String key = "product:" + id;Product product = redisTemplate.opsForValue().get(key);if (product != null) return product;// 2. 获取分布式锁(防止并发重建缓存)String lockKey = "lock:" + key;try {if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS)) {// 3. 二次检查缓存(防止其他线程已写入)product = redisTemplate.opsForValue().get(key);if (product != null) return product;// 4. 查数据库并写入缓存product = productMapper.selectById(id);redisTemplate.opsForValue().set(key, product, 5, TimeUnit.MINUTES);return product;} else {// 5. 未抢到锁的线程短暂休眠后重试Thread.sleep(100);return getProduct(id);}} finally {redisTemplate.delete(lockKey);}
}
3.2 大Value拆分
问题发现:Redis内存报警,某个2MB的缓存Key导致慢查询
优化方案:
- 将商品详情拆分为基础信息(高频访问)和扩展信息(低频访问)
- 采用Hash结构存储而非JSON序列化
// 存储
redisTemplate.opsForHash().putAll("product:base:"+id, Map.of("name", product.getName(),"price", product.getPrice()
));
redisTemplate.opsForHash().putAll("product:ext:"+id, Map.of("description", product.getDescription(),"spec", product.getSpec()
));// 查询
Map<String, Object> base = redisTemplate.opsForHash().entries("product:base:"+id);
4. 第三阶段优化:JVM调优
4.1 GC日志分析
在启动参数中添加:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/logs/gc.log
通过GCViewer分析发现:
- Young GC频率高达5次/秒
- 老年代使用率持续90%+
4.2 参数调整
原配置:
-Xms1g -Xmx1g -XX:NewRatio=2
优化后:
-Xms4g -Xmx4g -XX:NewRatio=1 -XX:SurvivorRatio=8 -XX:+UseG1GC
效果:
- Young GC频率降至0.2次/秒
- 接口TP99从200ms → 80ms
5. 最终效果对比
指标 | 优化前 | 优化后 |
---|---|---|
接口RT | 400ms | 20ms |
MySQL QPS | 3000 | 800 |
Redis命中率 | 70% | 99.8% |
GC停顿时间 | 200ms/次 | 50ms/次 |
6. 血泪教训
- 不要相信本地测试:压测要用生产级数据量
- 监控比优化更重要:提前部署Prometheus+Grafana
- 分布式锁要设超时:避免死锁导致线程池爆炸
- Key命名要规范:建议
业务:类型:ID
三段式