从接口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三段式
相关推荐
隔窗听雨眠19 分钟前
Oracle误Truncate操作恢复:从原理到实战的完整指南
数据库·oracle
Lethehong43 分钟前
MySQL迁移如何做到零改造?四层兼容方案详解
数据库·mysql·adb
Json____1 小时前
五金制品行业-企业官网源码
java·大数据·数据库·企业站·wwwoop.com
深念Y3 小时前
Opencode Event 表写入优化方案
数据库·人工智能·ai·node.js·bug·优化·opencode
小林ixn3 小时前
用 Next.js 和 Redis 撸一个 Markdown 笔记系统:RSC 实战与组件化拆解
前端·redis·next.js
竹枝溪3 小时前
MySQL进阶:约束、多表设计、多表查询与事务
java·数据库·mysql·事务·子查询·acid·多表查询
技术长镜头3 小时前
别再只会“加索引”:从磁盘页到 B+Tree,彻底理解 MySQL 索引的设计与运行
后端·mysql
weixin_431600443 小时前
NestJS 入门(9):连上数据库,SQL 写在哪?
数据库·后端·sql·学习·nest.js
math_hongfan4 小时前
一对多的数据库姻缘:ArkTS 为鸿蒙订单设计外键与明细表
数据库·华为·harmonyos
杰夫(简道云个人搭建)4 小时前
SAAS系统应该先解决业务问题,再优化体验
数据库·人工智能·低代码·excel·个人开发