什么是N+1查询问题?
N+1查询问题是ORM框架(如Hibernate、MyBatis等)中常见的性能问题。当需要获取主实体及其关联的多个子实体时,如果不进行优化,会执行:
1次查询:获取主实体列表
N次查询:为每个主实体单独查询关联数据
结果就是执行了 1 + N 次数据库查询。
例如:
1、查询用户及其所有订单
产生N+1问题
// 1. 第一次查询:获取所有用户
List<User> users = userRepository.findAll();
for (User user : users) {
// N次查询:为每个用户单独查询订单
List<Order> orders = orderRepository.findByUserId(user.getId());
user.setOrders(orders);
}
SQL执行过程,
-- 第一次查询(获取用户列表)
SELECT * FROM users;
-- 然后对每个用户执行一次:
SELECT * FROM orders WHERE user_id = 1; -- 用户1
SELECT * FROM orders WHERE user_id = 2; -- 用户2
SELECT * FROM orders WHERE user_id = 3; -- 用户3
-- ... 假设有100个用户,总共101次查询
危害?
| 危害 | 说明 |
|---|---|
| 性能瓶颈 | 数据库连接次数暴增,响应时间呈线性增长 |
| 资源浪费 | 每个查询都有网络开销,Sql解析开销 |
| 连接池耗尽 | 高并发下可能导致数据库连接不够用 |
| 难以扩展 | 随着数据量增加,性能急剧下降 |
解决办法
批量处理
例如查询多个app内容,每个app关联用户查询,
可以先
public List<AppVO> getAppVOList(List<App> appList) {
// 传入已经查询出来的appList
if (CollUtil.isEmpty(appList)) {
return new ArrayList<>();
}
// 查询对应用户, 批量获取用户信息,避免N + 1查询问题
// 提取所有用户id
// List<Long> userIdList = appList.stream().map(App::getUserId).collect(Collectors.toList());
// 使用Set,去重
Set<Long> userIdsSet = appList.stream().map(App::getUserId).collect(Collectors.toSet());
// 建议用户和UserVO映射
Map<Long, UserVO> userToUserVOMap = userService.listByIds(userIdsSet).stream()
.collect(Collectors.toMap(User::getId, userService::getUserVO));
// 填充用户信息到VO
return appList.stream().map(app -> {
AppVO appVO = getAppVO(app);
UserVO userVO = userToUserVOMap.get(app.getUserId());
if (userVO != null) {
appVO.setUserVO(userVO);
}
return appVO;
}).collect(Collectors.toList());
}
Join查询,适合简单关联
-- 如果数据量不大且关联关系简单
SELECT a.*, u.nickname, u.avatar
FROM app a
LEFT JOIN user u ON a.user_id = u.id
WHERE a.status = 1
ORDER BY a.create_time DESC
LIMIT 20
缓存优化
@Component
public class UserCacheService {
@Cacheable(value = "userCache", key = "'user:' + #userId")
public UserVO getUserVOById(Long userId) {
return userService.getUserVO(userId);
}
@Cacheable(value = "usersBatch", key = "#userIds.hashCode()")
public Map<Long, UserVO> batchGetUserVOs(Set<Long> userIds) {
return userService.listByIds(userIds).stream()
.collect(Collectors.toMap(User::getId, userService::getUserVO));
}
}
延迟加载 + 本地缓存(Caffeine)
public class AppVOService {
private final LoadingCache<Long, UserVO> userCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(userId -> userService.getUserVO(userId));
public List<AppVO> getAppVOList(List<App> appList) {
Set<Long> userIds = appList.stream()
.map(App::getUserId)
.collect(Collectors.toSet());
// 批量预加载到缓存
Map<Long, UserVO> userMap = userCache.getAll(userIds);
return appList.stream().map(app -> {
AppVO appVO = getAppVO(app);
appVO.setUserVO(userMap.get(app.getUserId()));
return appVO;
}).collect(Collectors.toList());
}
}
Redis Hash批量操作
public Map<Long, UserVO> batchGetUsersFromRedis(Set<Long> userIds) {
Map<Long, UserVO> result = new HashMap<>();
// 批量从Redis获取
List<String> keys = userIds.stream()
.map(id -> "user:" + id)
.collect(Collectors.toList());
List<UserVO> cachedUsers = redisTemplate.opsForValue().multiGet(keys);
// 检查缺失的用户,从数据库补充
Set<Long> missingIds = new HashSet<>();
for (int i = 0; i < userIds.size(); i++) {
Long userId = userIds.get(i);
if (cachedUsers.get(i) == null) {
missingIds.add(userId);
} else {
result.put(userId, cachedUsers.get(i));
}
}
if (!missingIds.isEmpty()) {
Map<Long, UserVO> dbUsers = userService.batchGetUserVOs(missingIds);
result.putAll(dbUsers);
// 异步缓存到Redis
cacheUsersToRedis(dbUsers);
}
return result;
}
此外,应用多级缓存批量获取用户信息。
应用场景
- 内容社交类应用
文章/帖子列表显示作者信息;
评论列表显示评论者头像昵称;
消息列表显示发送者消息;
动态/朋友圈显示发布者信息; - 电商系统
订单列表显示买家/卖家信息;
商品评价列表显示评价者信息;
客服消息显示用户信息;
物流信息关联收货人; - 协同办公系统
任务列表显示负责人、创建人;
文件分享列表显示分享者;
审批流程显示审批人;
日程安排显示参与者;


926

被折叠的 条评论
为什么被折叠?



