从接口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三段式
相关推荐
名字还没想好☜4 分钟前
Go 的 select 实战:超时、非阻塞收发与优雅退出的三个套路
开发语言·数据库·golang·go·并发
NWU_LK9 分钟前
【WebFlux】第六篇 —— 全链路响应式与数据库交互
java·数据库
叶总没有会16 分钟前
3.2 构建AI智能体项目扩展知识
java·数据库·人工智能·spring·ai
YOU OU8 小时前
Redis基础常识与命令
数据库·redis·缓存
Database_Cool_11 小时前
OLTP 和 OLAP 区别详解:分析型数据库和事务型数据库怎么选(附阿里云 AnalyticDB MySQL 选型指南)
数据库·mysql·阿里云
爱写代码的阿森11 小时前
鸿蒙三方库 | harmony-utils之KvUtil键值型数据库操作详解
数据库·华为·harmonyos·鸿蒙·huawei
Database_Cool_11 小时前
单机 MySQL 迁移到分布式数据库方便吗?阿里云 PolarDB-X 100% MySQL 协议兼容零改造平滑迁移
数据库·分布式·mysql
姜太小白12 小时前
【MySQL】 索引优化实战:解决 WHERE 等值 + IS NULL 查询,TEXT 字段报错 1167 的完整指南
数据库·mysql
窝子面13 小时前
手搓最简前后端协作-node
javascript·数据库