SpringBoot整合MyBatis-Plus、Druid与多数据源实战指南

📅 2026/8/14 11:16:01
SpringBoot整合MyBatis-Plus、Druid与多数据源实战指南
1. 项目概述为什么我们需要这套组合拳如果你正在用SpringBoot做企业级开发尤其是那些数据访问稍微复杂点的项目比如需要对接多个数据库、或者对数据库连接的性能和监控有要求那你大概率绕不开这几个名字MyBatis-Plus、Druid、多数据源。把它们单独拎出来你可能都熟悉但把它们整合在一起形成一个稳定、高效、可监控的数据访问层这里面就有不少门道了。我见过不少项目初期图省事直接用SpringBoot默认的HikariCP连接池和JdbcTemplate等业务量上来需要分库分表或者对接异构数据源时才发现“船大难掉头”重构成本极高。所以今天我们就来聊聊如何从零开始在SpringBoot项目中系统地整合MyBatis-Plus、Druid连接池并配置多数据源。这不仅仅是把几个依赖加进去、配置文件写对那么简单。我会带你理解每一步背后的“为什么”比如为什么选Druid而不是HikariCP多数据源的事务怎么处理才安全MyBatis-Plus在多数据源环境下有哪些坑这些都是我趟过雷、踩过坑之后总结出来的实战经验目的就是让你拿到一套开箱即用、且知其所以然的配置方案避免在项目后期为数据层的问题头疼。2. 技术选型与核心组件解析在动手之前我们得先搞清楚手里的“兵器”都是干什么的以及为什么是它们仨组成了最佳拍档。盲目整合只会带来混乱。2.1 MyBatis-Plus不只是MyBatis的增强首先明确MyBatis-Plus简称MP是一个MyBatis的增强工具在MyBatis的基础上只做增强不做改变。它的核心价值在于简化开发、提高效率。对于多数据源场景MP提供了一个非常清晰的抽象层。核心优势无侵入性它不会影响你原有的MyBatis功能你仍然可以写XML映射文件和使用原生接口。强大的CRUD操作通过继承BaseMapper你的实体类Mapper接口瞬间就拥有了通用的单表CRUD方法无需编写任何SQL。这在快速原型开发和简单业务中能节省大量时间。条件构造器QueryWrapper、UpdateWrapper等提供了Lambda表达式风格的动态SQL构建能力让代码更清晰、类型更安全。分页插件内置了分页插件支持多种数据库配置简单与MP的条件构造器是天作之合。多数据源支持MP本身不直接处理多数据源但它能与Spring的抽象完美结合。我们通过配置多个SqlSessionFactory和MapperScannerConfigurer让不同的Mapper接口绑定到不同的数据源上。注意很多人误以为MP自带多数据源解决方案。实际上多数据源是Spring框架层面的事务管理和数据源路由能力MP只是在这个基础上为每个数据源提供对应的MyBatis会话工厂和Mapper扫描。理解这一点配置时才不会迷糊。2.2 Druid连接池为何是监控与稳定的首选SpringBoot 2.0之后默认使用HikariCP它很快但在监控和管理方面Druid是公认的王者。在复杂的多数据源生产环境中监控能力至关重要。选择Druid的核心理由强大的监控功能Druid内置了一个StatViewServlet可以提供Web界面实时查看连接池状态、SQL执行情况、URI访问统计等。这对于排查慢SQL、监控连接泄露有不可替代的作用。防SQL注入内置了WallFilter能有效防御SQL注入攻击。详细的统计信息除了基础监控还能提供执行时间分布druid stat的执行时间分布、连接持有时间分布等深度指标帮助进行性能调优。稳定性久经考验作为阿里开源的项目在超大规模应用中经过了长期验证特别是在连接泄露的预防和处理上机制更为完善。对于多数据源每个数据源都可以独立配置一个DruidDataSource并分别设置监控这样我们就能清晰地看到每个库的压力和性能表现。2.3 多数据源配置解决业务分离与性能瓶颈多数据源的需求通常来自以下几种场景业务分库用户库、订单库、商品库物理分离降低单库压力方便独立扩容。读写分离主库负责写一个或多个从库负责读提升查询性能。对接异构数据源同时操作MySQL和Oracle或者MySQL和达梦达梦 session idle timeout 连接池、Redis等。多租户架构每个租户有独立的数据库。Spring抽象出了AbstractRoutingDataSource这个类来实现动态数据源路由。我们的多数据源配置本质就是扩展这个类根据当前执行线程的上下文通常用一个ThreadLocal变量来决定使用哪个具体的数据源。3. 项目搭建与基础依赖配置理论说完了我们开始动手。这里我假设你使用IDEA基于SpringBoot 2.7.x这是一个长期支持版本生态稳定进行构建。MyBatis-Plus我们选择较新的3.5.x版本。3.1 创建项目与引入依赖首先在pom.xml中引入核心依赖。版本号建议使用属性管理便于统一升级。properties java.version1.8/java.version spring-boot.version2.7.18/spring-boot.version mybatis-plus.version3.5.7/mybatis-plus.version druid.version1.2.20/druid.version mysql.version8.0.33/mysql.version /properties dependencies !-- SpringBoot Web Starter (如果项目是Web应用) -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- SpringBoot JDBC 支持 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency !-- MyBatis-Plus 核心依赖 (已经包含了MyBatis) -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version${mybatis-plus.version}/version /dependency !-- Druid 连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version${druid.version}/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version${mysql.version}/version scoperuntime/scope /dependency !-- Lombok (可选但强烈推荐简化实体类) -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- 测试 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version${spring-boot.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement实操心得关于mybatis-plus version3.5.17对应的springboot版本MP 3.5.x 对SpringBoot的兼容性很好从2.x到3.x都支持。但如果你坚持用SpringBoot 2.x建议选择MP 3.5.3版本以获得更好的兼容性。SpringBoot 3.x则建议使用MP 3.5.7。我们这里用2.7.18 3.5.7是经过大量项目验证的稳定组合。3.2 基础配置文件单数据源预热在配置多数据源之前我们先在application.yml里把单数据源的Druid配置跑通理解每个参数的意义。这步很重要是多数据源配置的基础。spring: datasource: # 使用Druid连接池 type: com.alibaba.druid.pool.DruidDataSource # 驱动、URL、用户名、密码 driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/master_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 # Druid连接池专属配置 druid: # 初始化连接数 initial-size: 5 # 最小空闲连接数 min-idle: 5 # 最大活跃连接数 max-active: 20 # 获取连接时最大等待时间毫秒 max-wait: 60000 # 配置间隔多久检测一次空闲连接毫秒 time-between-eviction-runs-millis: 60000 # 连接在池中最小生存的时间毫秒 min-evictable-idle-time-millis: 300000 # 验证连接是否有效的SQL validation-query: SELECT 1 test-while-idle: true test-on-borrow: false test-on-return: false # 是否缓存preparedStatement对支持游标的数据库性能提升巨大如Oracle/DB2。MySQL下建议关闭。 pool-prepared-statements: false max-pool-prepared-statement-per-connection-size: 20 # 监控配置 # 开启WebStatFilter监控Web请求 web-stat-filter: enabled: true url-pattern: /* exclusions: *.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/* # 开启StatViewServlet提供监控后台 stat-view-servlet: enabled: true url-pattern: /druid/* # 登录监控后台的账号密码 login-username: admin login-password: admin123 reset-enable: false # 生产环境务必设为false # 配置监控统计拦截的filters filters: stat,wall,slf4j filter: stat: enabled: true # 合并多个相同的SQL merge-sql: true # 慢SQL记录阈值毫秒 slow-sql-millis: 2000 log-slow-sql: true wall: enabled: true config: # 不允许删除全表 drop-table-allow: false启动应用访问http://localhost:8080/druid输入上面配置的用户名密码就能看到Druid强大的监控界面了。这里能看到druid stat的执行时间分布直观了解SQL性能。4. 多数据源配置实战现在进入核心环节。假设我们有两个数据库master_db主库写操作和slave_db从库读操作。4.1 动态数据源定义与路由首先我们需要定义一个枚举来标识我们的数据源。public enum DataSourceType { MASTER, SLAVE }接着创建一个线程上下文持有者用于存储当前线程应该使用的数据源键。public class DynamicDataSourceContextHolder { private static final ThreadLocalString CONTEXT_HOLDER new ThreadLocal(); public static void setDataSourceKey(String key) { CONTEXT_HOLDER.set(key); } public static String getDataSourceKey() { return CONTEXT_HOLDER.get(); } public static void clearDataSourceKey() { CONTEXT_HOLDER.remove(); } }然后创建动态数据源类继承AbstractRoutingDataSource。Slf4j public class DynamicDataSource extends AbstractRoutingDataSource { Override protected Object determineCurrentLookupKey() { String dataSourceKey DynamicDataSourceContextHolder.getDataSourceKey(); log.debug(当前数据源: {}, dataSourceKey); // 如果未指定数据源默认使用MASTER return dataSourceKey ! null ? dataSourceKey : DataSourceType.MASTER.name(); } }4.2 配置多个Druid数据源接下来在配置类中我们显式地创建两个DruidDataSource Bean并将它们注入到DynamicDataSource中。Configuration MapperScan(basePackages com.yourpackage.mapper, sqlSessionTemplateRef sqlSessionTemplate) public class DataSourceConfig { Bean ConfigurationProperties(spring.datasource.druid.master) public DataSource masterDataSource() { return DruidDataSourceBuilder.create().build(); } Bean ConfigurationProperties(spring.datasource.druid.slave) public DataSource slaveDataSource() { return DruidDataSourceBuilder.create().build(); } Bean(name dynamicDataSource) Primary // 标记为主要数据源Spring容器在注入DataSource时优先使用它 public DataSource dynamicDataSource( Qualifier(masterDataSource) DataSource masterDataSource, Qualifier(slaveDataSource) DataSource slaveDataSource) { MapObject, Object targetDataSources new HashMap(2); targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource); targetDataSources.put(DataSourceType.SLAVE.name(), slaveDataSource); DynamicDataSource dataSource new DynamicDataSource(); // 设置默认数据源 dataSource.setDefaultTargetDataSource(masterDataSource); // 设置目标数据源映射 dataSource.setTargetDataSources(targetDataSources); return dataSource; } }对应的application.yml配置需要修改将连接信息拆分到两个数据源下spring: datasource: druid: # 主库配置 master: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/master_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 initial-size: 5 min-idle: 5 max-active: 20 # ... 其他Druid参数同上可以独立配置 # 从库配置 slave: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3307/slave_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 initial-size: 3 # 从库读多写少可以适当调小初始连接 min-idle: 3 max-active: 15 # ... 其他Druid参数 # 公共的监控配置WebStatFilter, StatViewServlet可以放在这里两个数据源共享 web-stat-filter: enabled: true url-pattern: /* stat-view-servlet: enabled: true url-pattern: /druid/* login-username: admin login-password: admin1234.3 配置MyBatis-Plus与SqlSessionFactory多数据源下我们需要为每个数据源或者说为我们最终使用的dynamicDataSource配置独立的SqlSessionFactory和事务管理器。Configuration public class MyBatisPlusConfig { Bean(name sqlSessionFactory) public SqlSessionFactory sqlSessionFactory(Qualifier(dynamicDataSource) DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean sessionFactory new MybatisSqlSessionFactoryBean(); sessionFactory.setDataSource(dataSource); // 设置MyBatis-Plus全局配置 MybatisConfiguration configuration new MybatisConfiguration(); configuration.setMapUnderscoreToCamelCase(true); // 下划线转驼峰 configuration.setCacheEnabled(false); sessionFactory.setConfiguration(configuration); // 设置MyBatis-Plus分页插件 Interceptor interceptor new PaginationInnerInterceptor(DbType.MYSQL); sessionFactory.setPlugins(interceptor); // 设置XML映射文件位置如果有的话 // sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(classpath*:mapper/**/*.xml)); return sessionFactory.getObject(); } Bean(name sqlSessionTemplate) public SqlSessionTemplate sqlSessionTemplate(Qualifier(sqlSessionFactory) SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } /** * 配置事务管理器指向动态数据源 */ Bean(name transactionManager) public DataSourceTransactionManager transactionManager(Qualifier(dynamicDataSource) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }4.4 使用AOP实现数据源动态切换最优雅的方式是使用自定义注解和AOP在Service层的方法上标记应该使用哪个数据源。首先定义注解Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) Documented public interface DataSource { DataSourceType value() default DataSourceType.MASTER; }然后编写切面在方法执行前切换数据源执行后清理。Aspect Component Order(-1) // 确保在事务切面之前执行 Slf4j public class DataSourceAspect { Pointcut(annotation(com.yourpackage.annotation.DataSource) || within(com.yourpackage.annotation.DataSource)) public void dataSourcePointCut() { } Around(dataSourcePointCut()) public Object around(ProceedingJoinPoint point) throws Throwable { MethodSignature signature (MethodSignature) point.getSignature(); Method method signature.getMethod(); DataSource dataSourceAnnotation method.getAnnotation(DataSource.class); if (dataSourceAnnotation null) { // 如果方法上没有尝试获取类上的注解 dataSourceAnnotation point.getTarget().getClass().getAnnotation(DataSource.class); } String dataSourceKey DataSourceType.MASTER.name(); if (dataSourceAnnotation ! null) { dataSourceKey dataSourceAnnotation.value().name(); } // 设置数据源上下文 DynamicDataSourceContextHolder.setDataSourceKey(dataSourceKey); log.debug(设置数据源为: {} 执行方法: {}, dataSourceKey, method.getName()); try { return point.proceed(); } finally { // 方法执行完毕后清除数据源上下文避免内存泄漏和污染后续操作 DynamicDataSourceContextHolder.clearDataSourceKey(); log.debug(清理数据源上下文); } } }现在在Service层就可以轻松使用了Service public class UserServiceImpl implements UserService { Autowired private UserMapper userMapper; Override DataSource(DataSourceType.MASTER) // 写操作走主库 public int createUser(User user) { return userMapper.insert(user); } Override DataSource(DataSourceType.SLAVE) // 读操作走从库 public User getUserById(Long id) { return userMapper.selectById(id); } Override // 没有注解默认走主库由DynamicDataSource决定 public int updateUser(User user) { return userMapper.updateById(user); } }5. 多数据源下的高级问题与解决方案配置跑通只是第一步真正考验的是在复杂场景下的稳定性和正确性。5.1 多数据源事务管理这是多数据源配置中最容易踩坑的地方。Spring的Transactional注解默认是基于单个DataSourceTransactionManager工作的。在我们配置了动态数据源后如果在一个事务方法内切换了数据源会导致事务失效或混乱。解决方案分布式事务或避免跨库事务最佳实践避免跨数据源事务。在设计上尽量让一个业务操作只涉及一个数据库。如果无法避免需要评估是否真的需要强一致性。使用分布式事务管理器对于强一致性要求的跨库事务可以引入Seata、Atomikos等分布式事务解决方案。例如springboot 引入seata1.4.2依赖就是为了解决这类问题。但这会显著增加系统复杂度和性能开销。编程式事务管理如果跨库操作不频繁可以在Service层手动管理每个数据源的事务但代码会变得非常复杂且容易出错。踩坑实录我曾在一个订单服务中需要同时写入订单库和用户积分库。最初简单地给Service方法加了Transactional结果积分库写入失败回滚了但订单库因为数据源切换事务并未回滚导致数据不一致。后来通过“最终一致性”方案解决先写订单库成功后发MQ消息由另一个服务异步更新积分即使失败也有补偿机制。5.2 MyBatis-Plus插件的多数据源适配MyBatis-Plus的插件如分页插件PaginationInnerInterceptor、乐观锁插件OptimisticLockerInnerInterceptor是绑定在SqlSessionFactory上的。在我们的配置中只有一个SqlSessionFactory绑定到了动态数据源上所以这些插件对所有数据源都生效这通常是符合预期的。但是如果你需要为不同的数据源配置不同的插件行为比如主库不拦截某些SQL那就需要配置多个SqlSessionFactory并为每个Mapper指定其对应的Factory。这会使配置复杂度指数级上升非必要不推荐。5.3 Druid监控页面的多数据源展示我们上面配置的Druid监控是全局的。在监控页面的“数据源”选项卡里你会看到两个数据源master和slave的详细状态包括活跃连接数、等待线程数、执行次数等。这是Druid-spring-boot-starter自动帮我们集成的非常方便。如果你想为每个数据源配置独立的监控过滤规则或统计参数可以在对应的master或slave配置项下单独配置filters和filter覆盖全局配置。6. 常见问题排查与性能调优在实际部署和运行中你可能会遇到以下问题。6.1 连接泄露与超时配置问题应用运行一段时间后出现“获取连接超时”或连接数耗尽。排查首先查看Druid监控台的“连接泄露检测”。Druid可以检测并打印出疑似泄露的SQL堆栈。检查max-wait配置是否过小。在高并发下如果连接池耗尽新请求等待获取连接的时间超过max-wait就会抛异常。可以适当调大但根本还是要优化慢SQL或增加max-active。检查remove-abandoned相关配置Druid高级功能。可以开启它自动回收长时间未关闭的连接但这只是治标要找到未正确关闭连接比如忘记关闭ResultSet、Statement的代码。针对达梦数据库像达梦 session idle timeout 连接池这类问题是数据库服务器端会话超时设置比连接池的回收时间短。需要调整Druid的min-evictable-idle-time-millis和time-between-eviction-runs-millis让连接池在数据库断开之前主动回收空闲连接或者配置validation-query如达梦的SELECT 1 FROM DUAL让连接池定期验证连接有效性。6.2 动态数据源切换失效问题加了DataSource(SLAVE)注解但查询依然走了主库。排查AOP顺序问题确保自定义的DataSourceAspect切面顺序在Spring事务切面Transactional之前。因为事务管理器需要在打开连接之前就知道用哪个数据源。我们通过Order(-1)来保证。注解未生效检查切面表达式Pointcut是否正确能否拦截到目标方法。确保Service类是被Spring代理的例如不要在同一类内部调用带DataSource注解的方法这会绕过AOP。ThreadLocal污染确保在finally块中清除了DynamicDataSourceContextHolder。否则一个线程处理完一个请求后其数据源标识可能残留影响下一个请求。6.3 读写分离下的数据一致性问题问题在主库写入后立刻到从库查询可能查不到刚写入的数据因为主从复制有延迟。解决方案强制读主库对于这类“写后立即读”的场景在查询方法上使用DataSource(MASTER)绕过从库。业务妥协在UI/UX设计上允许短暂的数据不一致例如提示用户“数据提交中请稍后查看”。使用数据库中间件引入ShardingSphere-Proxy或MyCat等它们可以提供更智能的读写分离和强制走主库的Hint。6.4 性能调优参数参考下表列出一些关键Druid参数在生产环境的调优思路参数默认值/示例调优建议initial-size5根据应用启动后的平均并发量设置避免启动时大量创建连接的耗时。min-idle5和initial-size保持一致即可维持一个基本的空闲连接缓冲。max-active20核心参数。估算公式(应用实例数) * (每个实例峰值QPS * 平均执行时间(秒))。需要监控实际使用情况调整设置过小会等待过大会耗尽数据库资源。max-wait60000 (1分钟)获取连接的超时时间。生产环境可以设为2000-5000毫秒超时快速失败避免线程堆积。time-between-eviction-runs-millis60000 (1分钟)检测空闲连接的间隔。不宜过短增加开销不宜过长连接失效发现慢。1-5分钟均可。min-evictable-idle-time-millis300000 (5分钟)连接在池中最小生存时间。建议30分钟以上避免频繁创建销毁。需配合数据库的wait_timeout设置应小于此值。validation-querySELECT 1简单的探测SQL。确保数据库能快速响应。test-while-idletrue建议开启。在取出空闲连接时验证保证取出的连接是有效的。test-on-borrowfalse建议关闭。在每次借用连接时都验证性能损耗大。依靠test-while-idle和数据库自身的超时机制即可。这套整合方案从技术选型、详细配置到避坑指南基本覆盖了SpringBoot项目中使用MyBatis-Plus、Druid和多数据源的核心场景。记住没有银弹所有的配置和架构选择都需要根据你的具体业务流量、数据一致性要求和运维能力来权衡。开始时可以保守一些随着监控数据的完善再逐步调优。