MyBatis N+1查询坑,百万数据下接口直接超时
MyBatis N1 查询坑百万数据下接口直接超时做后端开发的小伙伴应该都遇到过这种特别诡异的性能问题本地测试、开发环境跑的好好的响应飞快一旦上线、数据量涨到几十万、上百万级别接口直接超时、页面卡死数据库CPU瞬间拉满。我前段时间排查线上列表接口超时问题排查了很久一开始以为是SQL没加索引、业务逻辑太复杂最后定位根源就是一个非常经典的老坑MyBatis N1 查询问题。说实话N1问题大家面试都会背但真正写业务代码时90%的人都会下意识写错。低数据量完全感知不到一旦数据量上来性能断崖式下跌。今天我结合真实线上故障手把手还原N1问题产生原因、错误代码、优化方案帮大家彻底根治这个隐形性能炸弹。一、简单搞懂什么是 MyBatis N1 查询通俗来讲N1 就是先查一次主表数据再循环N次查询关联子数据。比如查询用户列表再关联查询每个用户的订单信息。程序会先执行1次SQL查询所有用户再根据用户数量循环执行N次订单查询。总共执行N1 条SQL。数据量小的时候无所谓一旦列表有1000条数据就会多1000次SQL查询上万条数据直接让数据库压力爆炸接口超时完全是常态。二、线上故障还原典型N1错误代码目前绝大多数新项目都在用MyBatis-Plus很多人图方便直接用MP自带的列表查询然后通过关联属性获取子数据这也是N1问题最高发的场景。我还原一下当时线上出问题的原始代码大家一看就懂首先是用户实体一对多关联订单public class User { private Long id; private String userName; private String phone; // 一对多关联订单 private ListOrder orderList; }业务层错误写法也是很多人日常写法Service public class UserServiceImpl implements UserService { Autowired private UserMapper userMapper; // 【错误写法典型N1灾难】 Override public ListUser getUserList() { // 1次查询所有用户 ListUser userList userMapper.selectList(null); // 遍历取值触发N次关联查询 for (User user : userList) { // 每循环一次查一次订单 ListOrder orderList user.getOrderList(); } return userList; } }在百万级数据场景下这段代码直接崩盘。控制台疯狂打印SQL语句数据库连接数瞬间被打满接口响应从几十毫秒直接飙升到十几秒最终超时失败。三、为什么会产生 N1 问题很多新手疑惑为什么我没写查询子数据的代码它自己会查核心原因是MyBatis 懒加载/嵌套关联查询机制。我们在实体中配置了一对多关联默认开启懒加载只有当程序读取 orderList 属性时才会动态执行SQL查询。循环遍历列表读取关联属性就会无限触发单条查询。这也是为什么很多人代码看着干净实则隐患巨大。四、生产级最优解决方案联表一次性查询想要彻底解决N1核心思路只有一个杜绝循环查询一次性把所有数据查出来。放弃MP简单的selectList手写XML联表查询搭配ResultMap映射一次性封装主表关联数据全程只执行1条SQL性能提升几十倍。优化后的Mapper XML代码select idgetUserAndOrderList resultMapUserOrderResultMap SELECT u.id, u.user_name, u.phone, o.order_id, o.order_no, o.amount FROM user u LEFT JOIN order o ON u.id o.user_id /select resultMap idUserOrderResultMap typecom.entity.User id propertyid columnid/ result propertyuserName columnuser_name/ result propertyphone columnphone/ collection propertyorderList ofTypecom.entity.Order id propertyorderId columnorder_id/ result propertyorderNo columnorder_no/ result propertyamount columnamount/ /collection /resultMap优化后的业务代码极其简洁无需循环遍历没有多余SQLOverride public ListUser getUserList() { // 仅执行一次SQL全部数据封装完成 return userMapper.getUserAndOrderList(); }五、临时应急方案关闭全局懒加载如果项目太大来不及全部改造可以先临时关闭全局懒加载快速规避N1问题。在yml中添加配置mybatis: configuration: lazy-loading-enabled: false关闭之后关联数据不会动态触发查询能临时解决线上超时问题但治标不治本长期还是建议使用联表查询优化。六、个人总结日常开发避坑规范经过这次线上故障我团队统一定下开发规范彻底杜绝N1隐患1、凡是一对多、一对一关联查询禁止循环取值 2、列表关联数据必须手写联表SQL一次性查询封装 3、代码自测时观察控制台SQL打印出现大量重复SQL立即优化。结尾MyBatis N1查询属于典型的低并发无感、高并发致命的性能问题。很多线上接口超时、数据库压力过大根本不是服务器配置不够而是代码写法不规范导致的。看似小小的一个编码习惯在百万级数据量下会被无限放大。希望大家看完这篇文章彻底改掉循环关联查询的写法从根源避免线上性能事故写出更专业、更稳定的业务代码。