从接口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三段式
相关推荐
正在走向自律8 分钟前
金仓数据库KingbaseES基础语法详解与实践指南
数据库·国产数据库·ddl·dml·kingbasees·sql语法·电科金仓
alonewolf_999 分钟前
MySQL全局优化详解与8.0新特性全面解读
数据库·mysql
雪域迷影10 分钟前
Windows11上安装Redis服务和Redis可视化客户端
windows·redis
ASS-ASH11 分钟前
快速处理虚拟机磁盘扩容问题
linux·数据库·vmware·虚拟机·磁盘扩容
爱写bug的野原新之助17 分钟前
数据库及navicat工具
数据库·网络爬虫·工具
青云交17 分钟前
Java 大视界 -- 基于 Java+Redis Cluster 构建分布式缓存系统:实战与一致性保障(444)
java·redis·缓存·缓存穿透·分布式缓存·一致性保障·java+redis clus
数据知道20 分钟前
一文掌握 MongoDB 存储引擎 WiredTiger 的原理
数据库·mongodb·数据库架构
Full Stack Developme24 分钟前
Mycat 2 实现 MySQL 读写分离,并且实现 主从同步
android·数据库·mysql
Hello.Reader25 分钟前
PyFlink JAR、Python 包、requirements、虚拟环境、模型文件,远程集群怎么一次搞定?
java·python·jar
我是人✓28 分钟前
Spring IOC入门
java·数据库·spring