从接口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三段式
相关推荐
woshilys1 小时前
mysql 删除表等待
数据库·mysql
SEO-狼术2 小时前
dbForge Documenter for Oracle Crack
数据库·oracle
limnade2 小时前
MySQL中动态生成SQL语句去掉所有字段的空格
sql·mysql
老李不敲代码2 小时前
榕壹云外卖跑腿系统:基于Spring Boot+MySQL+UniApp的智慧生活服务平台
spring boot·mysql·微信小程序·uni-app·软件需求
极限实验室3 小时前
如何使用 Grafana 连接 Easyearch
数据库
文or野3 小时前
MySQL 在 CentOS 7 环境安装完整步骤
数据库·mysql·adb
牧羊狼的狼3 小时前
主键索引和唯一索引的区别
数据库·sql·oracle
Justice link3 小时前
部署redis cluster
数据库·redis·缓存
何似在人间5753 小时前
多级缓存模型设计
java·jvm·redis·缓存
会飞的土拨鼠呀3 小时前
SQL Server AlwaysOn (SQL 查询数据详解及监控用途)
数据库