从接口400ms到20ms,记录一次JVM、MySQL、Redis的混合双打


​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. 血泪教训​

  1. ​不要相信本地测试​:压测要用生产级数据量
  2. ​监控比优化更重要​:提前部署Prometheus+Grafana
  3. ​分布式锁要设超时​:避免死锁导致线程池爆炸
  4. ​Key命名要规范​ :建议业务:类型:ID三段式
相关推荐
段帅龙呀1 小时前
Redis构建缓存服务器
服务器·redis·缓存
互联网搬砖老肖4 小时前
运维打铁: MongoDB 数据库集群搭建与管理
运维·数据库·mongodb
典学长编程4 小时前
数据库Oracle从入门到精通!第四天(并发、锁、视图)
数据库·oracle
积跬步,慕至千里5 小时前
clickhouse数据库表和doris数据库表迁移starrocks数据库时建表注意事项总结
数据库·clickhouse
极限实验室5 小时前
搭建持久化的 INFINI Console 与 Easysearch 容器环境
数据库
星辰离彬5 小时前
Java 与 MySQL 性能优化:Java应用中MySQL慢SQL诊断与优化实战
java·后端·sql·mysql·性能优化
白仑色6 小时前
Oracle PL/SQL 编程基础详解(从块结构到游标操作)
数据库·oracle·数据库开发·存储过程·plsql编程
程序猿小D7 小时前
[附源码+数据库+毕业论文]基于Spring+MyBatis+MySQL+Maven+jsp实现的个人财务管理系统,推荐!
java·数据库·mysql·spring·毕业论文·ssm框架·个人财务管理系统
库森学长8 小时前
面试官:发生OOM后,JVM还能运行吗?
jvm·后端·面试
钢铁男儿8 小时前
C# 接口(什么是接口)
java·数据库·c#