从一个"慢查询"到全链路重构,这一周值不值?我觉得值。
事情要从一个普通的周三说起。
产品经理在群里@了我:"运营反馈说后台订单导出一直在转圈,三分钟了还没出来,客户在催。"
我打开监控面板,看到那个接口的响应时间——平均12.8秒,P99已经超过了30秒。关键是,这接口每天被调用上千次。
排查过程并不复杂。打开慢查询日志,一条SQL映入眼帘:
SELECT o., u.name, u.phone, p.product_name, p.sku FROM orders o LEFT JOIN users u ON o.user_id = u.id LEFT JOIN products p ON o.product_id = p.id WHERE o.create_time BETWEEN '2025-01-01' AND '2025-01-31' ORDER BY o.create_time DESC LIMIT 10000 OFFSET 0;
十万订单量,三张表关联,没有覆盖索引,还要排序——MySQL看了都沉默。
当时的第一反应是加索引、调参数。但当我仔细看了一遍整个数据访问层的代码后,我发现问题远不止一条SQL那么简单:
Entity里堆满了@ManyToOne和@OneToMany,JPA的N+1查询遍地都是;分页用的是内存分页,先把所有数据查出来再截取;批量操作是一条条循环insert的。
这不是修一个接口的问题,是整个底层数据访问层出了系统性故障。
一、放弃"优雅",选择"可控"
原来的代码用的是Spring Data JPA,写起来确实爽——几行注解就能搞定关联查询。但代价是,你根本不知道Hibernate替你生成了什么样的SQL。
我决定换成MyBatis Plus。
不是因为JPA不好,而是在这个场景下,我需要完全掌控每一条SQL。MyBatis Plus的LambdaQueryWrapper写起来也不比JPA复杂多少,但SQL是白纸黑字写在Mapper里的,执行计划一看便知。
// 之前:JPA自动生成,你猜它怎么查的 Page<Order> findByCreateTimeBetween(LocalDateTime start, LocalDateTime end, Pageable pageable); // 之后:SQL完全在手,explain看得明明白白 @Select("SELECT o., u.name, u.phone, p.product_name " + "FROM orders o " + "LEFT JOIN users u ON o.user_id = u.id " + "LEFT JOIN products p ON o.product_id = p.id " + "WHERE o.create_time BETWEEN #{start} AND #{end} " + "ORDER BY o.create_time DESC") Page<OrderVO> queryOrderPage(Page<OrderVO> page, @Param("start") LocalDateTime start, @Param("end") LocalDateTime end);二、分页不是"查出来再截取"
原来代码里一个很隐蔽的问题:PageHelper在数据量大的时候,会先查全部数据再内存分页。十万条数据全查出来,内存不炸才怪。
重写后,分页逻辑下推到数据库层面。配合覆盖索引(create_time, id),分页查询的扫描行数从十万降到了几十条。
三、批量操作,别一条条来
原来的代码里有个定时任务,每晚同步五千条商品数据:
for (Product product : productList) { productMapper.insert(product); }五千条循环插入,每次都要建立一次数据库连接、解析一次SQL、刷一次磁盘。我改成了:
productMapper.insertBatch(productList); // 一条SQL插入五千条
配合MyBatis Plus的批量注入和rewriteBatchedStatements参数,五千条插入从12秒降到了0.8秒。
四、缓存是最后的防线
数据访问层还有一个问题:同样的字典数据、配置数据,每次都去查数据库。我把这部分用Caffeine做了本地缓存,TTL设置5分钟,命中率直接拉满。
效果
前后花了一周时间,从设计方案、改写代码、测试对比到灰度上线。
上线后第一天的监控数据:
那个订单导出接口:从12.8秒降到1.2秒
所有涉及订单查询的接口:平均响应时间下降78%
数据库CPU使用率:从65%降到22%
定时任务的执行时间:从8分钟缩短到不到2分钟
产品经理再没来催过。
一点思考
这一周的重构,本质上是在做一件事:把"方便"换成"可控"。
JPA很优雅,但你得接受它的黑盒;MyBatis啰嗦一点,但每一条SQL都摆在明面上。在业务高速增长期,优雅可能是负担,可控才是安全感。
另外,不要等到接口超时才去优化。慢查询日志每周扫一遍,EXPLAIN成为肌肉记忆,批量操作的写法刻进代码模板——这些习惯,比重构本身更重要。
一周的时间,换一个稳定的底层和一个不焦虑的晚上。这笔账,怎么算都不亏。