MyBatis-Plus性能优化实战:从CRUD到百万级数据处理
1. MyBatis-Plus实战优化全景图作为国内Java开发者最常用的ORM框架之一MyBatis-Plus在简化CRUD操作方面确实表现出色。但很多团队在落地实践中常常陷入两个极端要么过度依赖其开箱即用的功能导致性能瓶颈要么因担心性能问题而放弃使用其高级特性。我在金融、电商等多个千万级数据量项目中深度使用MP后总结出这套30分钟快速见效的优化方案。先看一个典型反例某电商平台的商品分类查询接口原始实现直接用MP的LambdaQueryWrapper做全字段查询当分类数据达到3万条时接口响应时间从200ms飙升到1.2秒。经过下文介绍的复合优化后最终稳定在150ms以内。2. CRUD优化四重奏2.1 查询优化黄金法则字段精确控制是首要原则。MP默认的selectList()会查询所有字段这在大表场景绝对是性能杀手。推荐两种解决方案// 方案1使用select()明确指定字段 LambdaQueryWrapperUser wrapper new LambdaQueryWrapper() .select(User::getId, User::getName) .eq(User::getStatus, 1); // 方案2实体类加TableField(select false) public class User { TableField(select false) private String detailInfo; }批处理优化同样关键。当需要处理ID列表查询时避免循环单条查询// 错误示范 ListUser users ids.stream() .map(id - userMapper.selectById(id)) .collect(Collectors.toList()); // 正确做法 ListUser users userMapper.selectBatchIds(ids);实测数据在1000条ID查询场景下批处理方式比单条查询快15倍以上2.2 更新操作避坑指南动态更新字段是高频痛点。很多开发者会犯这样的错误// 反例全字段更新 User user new User(); user.setId(1L); user.setName(newName); userMapper.updateById(user); // 未变更的字段也被更新MP提供了两种优雅解决方案// 方案1使用UpdateWrapper动态构建 UpdateWrapperUser wrapper new UpdateWrapper() .eq(id, 1L) .set(name, newName); userMapper.update(null, wrapper); // 方案2配合Version乐观锁 public class User { Version private Integer version; }2.3 分页查询性能跃升默认的Page对象在百万级数据时会出现严重性能问题。优化方案需要分场景场景1前端分页// 使用优化后的PageParam PageParamUser pageParam new PageParam(1, 10); pageParam.setOptimizeCountSql(true); // 禁用COUNT查询 PageUser page userMapper.selectPage(pageParam, wrapper);场景2导出全部数据// 采用游标方式处理 try (CursorUser cursor userMapper.selectCursor(wrapper)) { cursor.forEach(user - { // 处理每条数据 }); }2.4 事务与批处理最佳实践批量插入的经典错误示范// 低效做法 for (int i 0; i 1000; i) { userMapper.insert(users.get(i)); }优化后的方案// 方案1使用SqlSession批量模式 SqlSession sqlSession sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH); UserMapper batchMapper sqlSession.getMapper(UserMapper.class); users.forEach(batchMapper::insert); sqlSession.commit(); // 方案2使用MP的saveBatch需配合rewriteBatchedStatementstrue userService.saveBatch(users, 1000); // 每批1000条3. 高阶性能调优策略3.1 二级缓存深度优化MP默认整合了Redis缓存但直接使用会有序列化性能问题。推荐自定义配置Bean public MybatisRedisCacheFactory mybatisRedisCacheFactory() { return new MybatisRedisCacheFactory() { Override public Cache createCache(String id) { return new CustomRedisCache(id, redisTemplate, 600, // 过期时间 new FastJsonRedisSerializer()); // 自定义序列化 } }; }缓存命中率监控建议Cache cache sqlSessionFactory.getConfiguration().getCache(com.example.mapper.UserMapper); if (cache instanceof CustomRedisCache) { CustomRedisCache redisCache (CustomRedisCache) cache; log.info(缓存命中率{}, redisCache.getHitRatio()); }3.2 SQL执行监控方案通过自定义拦截器实现慢SQL监控Intercepts({ Signature(type Executor.class, methodupdate, args{MappedStatement.class,Object.class}), Signature(type Executor.class, methodquery, args{MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}) }) public class PerformanceInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { long start System.currentTimeMillis(); Object result invocation.proceed(); long time System.currentTimeMillis() - start; if (time 200) { // 超过200ms记录警告 MappedStatement ms (MappedStatement) invocation.getArgs()[0]; log.warn(慢SQL[{}ms]{}, time, ms.getId()); } return result; } }3.3 动态数据源路由在多租户场景下动态数据源是必备方案。MP与Druid的整合配置spring: datasource: dynamic: primary: master strict: true datasource: master: url: jdbc:mysql://localhost:3306/master username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver tenant1: url: jdbc:mysql://localhost:3306/tenant1 username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver通过AOP实现自动路由Around(execution(* com.example.mapper.*.*(..))) public Object around(ProceedingJoinPoint point) throws Throwable { String tenantId TenantContext.getCurrentTenant(); DynamicDataSourceContextHolder.push(tenantId); try { return point.proceed(); } finally { DynamicDataSourceContextHolder.poll(); } }4. 实战问题排查手册4.1 N1查询问题典型症状单条主查询伴随大量附加查询-- 主查询 SELECT * FROM order WHERE user_id 1; -- 循环执行的附加查询 SELECT * FROM order_item WHERE order_id 1001; SELECT * FROM order_item WHERE order_id 1002;解决方案// 使用MP的TableField(select false) 手动join查询 TableField(exist false) private ListOrderItem items; // 在Service层手动处理关联查询 public Order getOrderWithItems(Long orderId) { Order order orderMapper.selectById(orderId); order.setItems(orderItemMapper.selectByOrderId(orderId)); return order; }4.2 索引失效场景MP生成的SQL可能导致索引失效的常见情况对索引列使用函数操作wrapper.apply(DATE(create_time) 2023-01-01); // 导致索引失效隐式类型转换wrapper.eq(user_id, 123); // user_id是数值类型错误使用LIKEwrapper.likeLeft(name, 张); // 正确张% wrapper.likeRight(name, 张); // 错误%张4.3 事务传播问题典型错误案例Transactional public void batchProcess() { dataList.forEach(item - { processItem(item); // 内部无事务 }); } private void processItem(DataItem item) { // 非事务操作 }优化方案// 方案1使用编程式事务 public void batchProcess() { TransactionTemplate transactionTemplate new TransactionTemplate(transactionManager); dataList.forEach(item - { transactionTemplate.execute(status - { return processItemWithTx(item); }); }); } // 方案2调整传播行为 Transactional(propagation Propagation.REQUIRES_NEW) public void processItem(DataItem item) { // 事务操作 }5. 性能对比实测数据通过JMeter对优化前后进行压测100并发场景TPS平均响应时间错误率原生MP实现128780ms1.2%字段精确控制215460ms0.3%批处理优化420238ms0%二级缓存加持150065ms0%特别提醒缓存方案需要根据业务特点谨慎设计高一致性要求的场景建议采用多级缓存策略。6. 扩展优化思路对于超大规模数据场景可以考虑以下进阶方案分库分表整合配合ShardingSphere实现// 分片策略配置 public class UserShardingAlgorithm implements PreciseShardingAlgorithmLong { Override public String doSharding(CollectionString availableTargetNames, PreciseShardingValueLong shardingValue) { // 按用户ID取模分片 long mod shardingValue.getValue() % availableTargetNames.size(); return ds_ mod; } }读写分离配置spring: shardingsphere: datasource: names: master,slave0,slave1 masterslave: load-balance-algorithm-type: round_robin name: ms master-data-source-name: master slave-data-source-names: slave0,slave1弹性数据源管理通过动态数据源支持运行时扩容public void addNewDataSource(String dsName, DataSourceProperty property) { DynamicDataSourceCreator creator new HikariDataSourceCreator(); DataSource newDataSource creator.createDataSource(property); dynamicRoutingDataSource.addDataSource(dsName, newDataSource); }这些年在不同业务场景中验证过的经验是MyBatis-Plus的优化从来不是一劳永逸的需要建立持续的性能监控机制。我们团队现在会在CI流程中加入SQL检查环节任何包含SELECT *的MR都会被自动打回。同时每周会分析慢查询日志将高频出现的慢SQL纳入优化待办列表。这种制度化的优化机制比临时性的性能调效要有效得多。