N+1:程序员必知的“性能杀手”

什么是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;
}

此外,应用多级缓存批量获取用户信息。

应用场景

  1. 内容社交类应用
    文章/帖子列表显示作者信息;
    评论列表显示评论者头像昵称;
    消息列表显示发送者消息;
    动态/朋友圈显示发布者信息;
  2. 电商系统
    订单列表显示买家/卖家信息;
    商品评价列表显示评价者信息;
    客服消息显示用户信息;
    物流信息关联收货人;
  3. 协同办公系统
    任务列表显示负责人、创建人;
    文件分享列表显示分享者;
    审批流程显示审批人;
    日程安排显示参与者;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

所谓远行Misnearch

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值