1. 多数据源配置的核心价值与场景解析
在真实的企业级应用开发中,单数据源往往难以满足复杂的业务需求。我经历过一个电商项目,需要同时对接主业务库、财务库和物流库,传统的单数据源模式会导致代码中充斥大量硬编码的JDBC连接切换逻辑。MyBatis-Plus结合dynamic-datasource的多数据源方案,正是解决这类痛点的利器。
这种方案的核心优势在于:
- 业务透明性:开发人员无需关心具体操作哪个数据库,通过注解即可切换
- 动态路由能力:可根据业务参数在运行时动态选择数据源
- 事务管理:支持跨数据源的分布式事务(需结合Seata等框架)
- 性能优化:内置连接池管理,避免频繁创建销毁连接
典型应用场景包括:
- 多租户SaaS系统(每个租户独立数据库)
- 读写分离架构(主库写,从库读)
- 分库分表前的过渡方案
- 异构数据库整合(如MySQL+Oracle混合使用)
重要提示:多数据源配置会增加系统复杂度,在单库能满足需求时不要过度设计。我曾见过团队为了"技术先进性"强行上多数据源,反而导致维护成本飙升的案例。
2. 环境准备与依赖配置
2.1 版本匹配要点
当前主流组合方案:
- Spring Boot 2.7.x + MyBatis-Plus 3.5.3.x
- Spring Boot 3.x + MyBatis-Plus 3.5.4+
我推荐使用经过验证的稳定组合:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.6.1</version> </dependency>2.2 Druid连接池集成
虽然dynamic-datasource自带HikariCP,但国内项目更常用Druid。配置示例:
spring: datasource: druid: stat-view-servlet: enabled: true login-username: admin login-password: admin filters: stat,wall filter: stat: slow-sql-millis: 1000 log-slow-sql: true2.3 多数据源基础配置
spring: datasource: dynamic: primary: master # 默认数据源 strict: false # 是否严格匹配数据源 datasource: master: url: jdbc:mysql://localhost:3306/master_db username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource slave1: url: jdbc:mysql://localhost:3306/slave_db1 username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource3. 核心实现与注解使用
3.1 @DS注解深度解析
@DS是动态数据源的核心注解,支持以下使用场景:
- 方法级注解(最常用):
@DS("slave1") public List<User> getUsers() { return userMapper.selectList(null); }- 类级注解(该类所有方法默认使用该数据源):
@DS("master") @Repository public class UserDaoImpl implements UserDao { // 所有方法默认使用master数据源 }- 注解继承规则:
- 方法注解优先于类注解
- 接口上的注解不生效(因为Spring AOP基于实现类)
3.2 动态数据源切换原理
核心流程:
- 通过AOP拦截被@DS注解的方法
- 从注解值或SpEL表达式中解析目标数据源名称
- 将数据源key设置到DynamicDataSourceContextHolder
- 执行SQL前通过AbstractRoutingDataSource确定实际数据源
- 执行完成后清理线程上下文
踩坑记录:我曾遇到@DS注解在同类方法调用时不生效的问题,这是因为Spring AOP的代理机制导致的。解决方案是注入自身代理或通过ApplicationContext获取bean。
4. 高级特性与实战技巧
4.1 分页插件集成
MyBatis-Plus的分页在多数据源环境下需要特殊处理:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }使用示例:
@DS("slave1") public Page<User> getUsersByPage(int pageNum, int pageSize) { Page<User> page = new Page<>(pageNum, pageSize); return userMapper.selectPage(page, null); }4.2 多数据源事务管理
默认情况下,@Transactional和@DS注解组合使用时会有问题。解决方案:
- 方案一:使用分布式事务框架(如Seata)
- 方案二:避免跨数据源事务
- 方案三:自定义事务管理器
@DS("master") @Transactional(rollbackFor = Exception.class) public void updateMaster(User user) { userMapper.updateById(user); // 调用其他数据源方法需要特殊处理 ((UserService)AopContext.currentProxy()).updateSlave(user); } @DS("slave1") public void updateSlave(User user) { userMapper.updateById(user); }4.3 动态数据源扩展
实现动态数据源路由:
public class DynamicDataSourceStrategy implements DynamicDataSourceStrategy { @Override public String determineDataSourceKey( DynamicDataSourceContextHolder contextHolder) { // 从请求参数或线程上下文获取路由key String tenantId = TenantContext.getCurrentTenant(); return "ds_" + tenantId; } }配置:
spring: datasource: dynamic: strategy: com.example.config.DynamicDataSourceStrategy5. 性能优化与问题排查
5.1 连接池配置建议
spring: datasource: druid: # 初始连接数 initial-size: 5 # 最小空闲连接数 min-idle: 5 # 最大活跃连接数 max-active: 20 # 获取连接等待超时时间 max-wait: 60000 # 配置间隔多久检测需要关闭的空闲连接 time-between-eviction-runs-millis: 60000 # 连接最小生存时间 min-evictable-idle-time-millis: 3000005.2 常见问题排查指南
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 数据源切换不生效 | 1. 注解位置错误 2. 同类方法调用 | 1. 检查注解位置 2. 使用AopContext |
| 事务回滚失效 | 跨数据源事务 | 使用分布式事务或重构代码 |
| 连接泄漏 | 连接未正确关闭 | 检查事务边界,配置Druid监控 |
| 性能下降 | 连接池配置不当 | 调整连接池参数,监控使用情况 |
5.3 监控与调优
- 启用Druid监控:
@Bean public ServletRegistrationBean<StatViewServlet> druidStatViewServlet() { ServletRegistrationBean<StatViewServlet> reg = new ServletRegistrationBean<>(); reg.setServlet(new StatViewServlet()); reg.addUrlMappings("/druid/*"); return reg; }- 关键监控指标:
- 活跃连接数
- 等待线程数
- SQL执行时间
- 慢SQL记录
6. 项目实战:若依框架改造案例
以若依4.8.3非分离版改造为例:
- 移除原有Mybatis依赖,添加Mybatis-Plus:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency>修改application.yml配置多数据源
重构原有Mapper接口:
// 原接口 public interface UserMapper { @Select("SELECT * FROM sys_user WHERE user_id = #{userId}") SysUser selectUserById(Long userId); } // 改为 public interface UserMapper extends BaseMapper<SysUser> { @DS("slave") // 添加数据源注解 @Select("SELECT * FROM sys_user WHERE user_id = #{userId}") SysUser selectUserById(Long userId); }- 分页改造:
// 原代码 PageHelper.startPage(pageNum, pageSize); List<SysUser> list = userMapper.selectUserList(user); PageInfo<SysUser> pageInfo = new PageInfo<>(list); // 改为 Page<SysUser> page = new Page<>(pageNum, pageSize); userMapper.selectPage(page, Wrappers.<SysUser>lambdaQuery() .eq(SysUser::getStatus, "0"));改造过程中的经验:
- 先从小模块开始试点
- 注意原有XML中特殊SQL的兼容处理
- 分页参数传递方式的变化需要统一调整