三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

MyBatis N+1查询坑,百万数据下接口直接超时

MyBatis N+1查询坑,百万数据下接口直接超时

MyBatis N+1 查询坑,百万数据下接口直接超时

做后端开发的小伙伴,应该都遇到过这种特别诡异的性能问题:本地测试、开发环境跑的好好的,响应飞快,一旦上线、数据量涨到几十万、上百万级别,接口直接超时、页面卡死,数据库CPU瞬间拉满。

我前段时间排查线上列表接口超时问题,排查了很久,一开始以为是SQL没加索引、业务逻辑太复杂,最后定位根源就是一个非常经典的老坑:MyBatis N+1 查询问题

说实话,N+1问题大家面试都会背,但真正写业务代码时,90%的人都会下意识写错。低数据量完全感知不到,一旦数据量上来,性能断崖式下跌。今天我结合真实线上故障,手把手还原N+1问题产生原因、错误代码、优化方案,帮大家彻底根治这个隐形性能炸弹。

一、简单搞懂:什么是 MyBatis N+1 查询?

通俗来讲,N+1 就是先查一次主表数据,再循环N次查询关联子数据

比如查询用户列表,再关联查询每个用户的订单信息。程序会先执行1次SQL查询所有用户,再根据用户数量,循环执行N次订单查询。总共执行N+1 条SQL

数据量小的时候无所谓,一旦列表有1000条数据,就会多1000次SQL查询;上万条数据直接让数据库压力爆炸,接口超时完全是常态。

二、线上故障还原:典型N+1错误代码

目前绝大多数新项目都在用MyBatis-Plus,很多人图方便,直接用MP自带的列表查询,然后通过关联属性获取子数据,这也是N+1问题最高发的场景。

我还原一下当时线上出问题的原始代码,大家一看就懂:

首先是用户实体,一对多关联订单:

public class User { private Long id; private String userName; private String phone; // 一对多关联订单 private List<Order> orderList; }

业务层错误写法,也是很多人日常写法:

@Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; // 【错误写法!典型N+1灾难】 @Override public List<User> getUserList() { // 1次查询所有用户 List<User> userList = userMapper.selectList(null); // 遍历取值,触发N次关联查询 for (User user : userList) { // 每循环一次查一次订单 List<Order> orderList = user.getOrderList(); } return userList; } }

在百万级数据场景下,这段代码直接崩盘。控制台疯狂打印SQL语句,数据库连接数瞬间被打满,接口响应从几十毫秒直接飙升到十几秒,最终超时失败。

三、为什么会产生 N+1 问题?

很多新手疑惑,为什么我没写查询子数据的代码,它自己会查?

核心原因是MyBatis 懒加载/嵌套关联查询机制。我们在实体中配置了一对多关联,默认开启懒加载,只有当程序读取 orderList 属性时,才会动态执行SQL查询。

循环遍历列表读取关联属性,就会无限触发单条查询。这也是为什么很多人代码看着干净,实则隐患巨大。

四、生产级最优解决方案:联表一次性查询

想要彻底解决N+1,核心思路只有一个:杜绝循环查询,一次性把所有数据查出来

放弃MP简单的selectList,手写XML联表查询,搭配ResultMap映射,一次性封装主表+关联数据,全程只执行1条SQL,性能提升几十倍。

优化后的Mapper XML代码:

<select id="getUserAndOrderList" resultMap="UserOrderResultMap"> 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 id="UserOrderResultMap" type="com.entity.User"> <id property="id" column="id"/> <result property="userName" column="user_name"/> <result property="phone" column="phone"/> <collection property="orderList" ofType="com.entity.Order"> <id property="orderId" column="order_id"/> <result property="orderNo" column="order_no"/> <result property="amount" column="amount"/> </collection> </resultMap>

优化后的业务代码极其简洁,无需循环遍历,没有多余SQL:

@Override public List<User> getUserList() { // 仅执行一次SQL,全部数据封装完成 return userMapper.getUserAndOrderList(); }

五、临时应急方案:关闭全局懒加载

如果项目太大,来不及全部改造,可以先临时关闭全局懒加载,快速规避N+1问题。在yml中添加配置:

mybatis: configuration: lazy-loading-enabled: false

关闭之后,关联数据不会动态触发查询,能临时解决线上超时问题,但治标不治本,长期还是建议使用联表查询优化。

六、个人总结:日常开发避坑规范

经过这次线上故障,我团队统一定下开发规范,彻底杜绝N+1隐患:

1、凡是一对多、一对一关联查询,禁止循环取值; 2、列表关联数据,必须手写联表SQL,一次性查询封装; 3、代码自测时观察控制台SQL打印,出现大量重复SQL立即优化。

结尾

MyBatis N+1查询属于典型的低并发无感、高并发致命的性能问题。很多线上接口超时、数据库压力过大,根本不是服务器配置不够,而是代码写法不规范导致的。

看似小小的一个编码习惯,在百万级数据量下会被无限放大。希望大家看完这篇文章,彻底改掉循环关联查询的写法,从根源避免线上性能事故,写出更专业、更稳定的业务代码。

← 返回列表