1. 项目背景与核心需求
在现代企业级应用开发中,多数据源连接已经成为标配需求。一个典型的电商系统可能需要同时访问MySQL中的订单数据、SQL Server中的库存数据,甚至还需要连接Redis缓存。SpringBoot作为Java生态中最流行的微服务框架,其简化配置的特性让多数据源管理变得更加优雅。
我最近在金融行业的一个风控系统中就遇到了这样的场景:需要实时比对MySQL中的用户交易记录和SQL Server中的黑名单数据。通过整合MyBatisPlus这个强大的ORM框架,最终实现了高性能的多数据源查询方案。下面就把这套经过实战检验的方案完整分享给大家。
2. 技术选型与架构设计
2.1 核心组件说明
这套方案主要基于以下技术栈:
- SpringBoot 2.7.x(选择LTS版本保证稳定性)
- MyBatisPlus 3.5.x(简化了90%的CRUD操作)
- Druid 1.2.8(阿里开源的数据库连接池)
- MySQL 8.0 / SQL Server 2019
特别注意:MyBatisPlus从3.0开始支持lambda表达式写法,可以避免魔法值的硬编码问题,这是选择它的重要原因。
2.2 多数据源实现原理
SpringBoot默认使用自动配置的单数据源,要实现多数据源需要手动管理DataSource实例。核心思路是:
- 禁用spring-boot-starter-jdbc的自动配置
- 创建多个DataSource Bean并指定qualifier
- 为每个数据源配置独立的SqlSessionFactory
- 通过AOP实现动态数据源切换
java复制@Configuration
@MapperScan(basePackages = "com.example.mapper.mysql")
public class MySQLDataSourceConfig {
// 具体配置见下文
}
3. 详细实现步骤
3.1 基础环境准备
首先在pom.xml中添加必要依赖:
xml复制<dependencies>
<!-- 核心starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 数据库相关 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
3.2 MySQL数据源配置
创建MySQL专属配置类:
java复制@Configuration
@MapperScan(basePackages = "com.example.mapper.mysql",
sqlSessionFactoryRef = "mysqlSqlSessionFactory")
public class MySQLDataSourceConfig {
@Bean(name = "mysqlDataSource")
@ConfigurationProperties(prefix = "spring.datasource.mysql")
public DataSource mysqlDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "mysqlSqlSessionFactory")
public SqlSessionFactory mysqlSqlSessionFactory(
@Qualifier("mysqlDataSource") DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/mysql/*.xml"));
return sessionFactory.getObject();
}
@Bean(name = "mysqlTransactionManager")
public DataSourceTransactionManager mysqlTransactionManager(
@Qualifier("mysqlDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
对应的application.yml配置:
yaml复制spring:
datasource:
mysql:
url: jdbc:mysql://localhost:3306/db1?useSSL=false&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
initial-size: 5
max-active: 20
min-idle: 5
3.3 SQL Server数据源配置
SQL Server的配置类与MySQL类似,但需要注意几个关键差异点:
java复制@Configuration
@MapperScan(basePackages = "com.example.mapper.sqlserver",
sqlSessionFactoryRef = "sqlServerSqlSessionFactory")
public class SQLServerDataSourceConfig {
@Bean(name = "sqlServerDataSource")
@ConfigurationProperties(prefix = "spring.datasource.sqlserver")
public DataSource sqlServerDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "sqlServerSqlSessionFactory")
public SqlSessionFactory sqlServerSqlSessionFactory(
@Qualifier("sqlServerDataSource") DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/sqlserver/*.xml"));
// SQL Server需要特殊配置的类型处理器
sessionFactory.setTypeHandlers(new CustomSQLServerTypeHandler());
return sessionFactory.getObject();
}
}
对应的application.yml配置:
yaml复制spring:
datasource:
sqlserver:
url: jdbc:sqlserver://localhost:1433;databaseName=testdb
username: sa
password: yourStrongPassword
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
druid:
initial-size: 3 # SQL Server连接较昂贵,初始连接数可以少些
max-active: 15
validation-query: SELECT 1
3.4 动态数据源路由实现
创建AbstractRoutingDataSource的子类实现动态切换:
java复制public class DynamicDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSourceKey(String key) {
CONTEXT_HOLDER.set(key);
}
public static void clearDataSourceKey() {
CONTEXT_HOLDER.remove();
}
@Override
protected Object determineCurrentLookupKey() {
return CONTEXT_HOLDER.get();
}
}
配置主数据源(包含所有子数据源):
java复制@Primary
@Bean(name = "dynamicDataSource")
public DataSource dynamicDataSource(
@Qualifier("mysqlDataSource") DataSource mysqlDataSource,
@Qualifier("sqlServerDataSource") DataSource sqlServerDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("mysql", mysqlDataSource);
targetDataSources.put("sqlserver", sqlServerDataSource);
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(mysqlDataSource);
return dynamicDataSource;
}
4. 使用示例与最佳实践
4.1 基于注解的切换方式
创建自定义注解实现数据源切换:
java复制@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String value() default "mysql";
}
通过AOP实现注解解析:
java复制@Aspect
@Component
@Order(-1) // 确保在事务注解前执行
public class DataSourceAspect {
@Before("@annotation(ds)")
public void beforeSwitchDataSource(JoinPoint point, DataSource ds) {
String dsId = ds.value();
if (!DynamicDataSource.containsDataSource(dsId)) {
throw new IllegalArgumentException("数据源"+dsId+"不存在");
}
DynamicDataSource.setDataSourceKey(dsId);
}
@After("@annotation(ds)")
public void afterSwitchDataSource(JoinPoint point, DataSource ds) {
DynamicDataSource.clearDataSourceKey();
}
}
4.2 MyBatisPlus多数据源CRUD示例
MySQL数据源操作:
java复制@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper; // 对应mysql数据源的mapper
@DataSource("mysql")
public User getUserById(Long id) {
return userMapper.selectById(id);
}
}
SQL Server数据源操作:
java复制@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductMapper productMapper; // 对应sqlserver数据源的mapper
@DataSource("sqlserver")
public Product getProductByCode(String code) {
QueryWrapper<Product> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(Product::getProductCode, code);
return productMapper.selectOne(wrapper);
}
}
4.3 事务管理的特殊处理
多数据源环境下事务管理需要特别注意:
java复制@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private UserMapper userMapper;
@Autowired
private ProductMapper productMapper;
// 跨数据源事务需要分布式事务解决方案
// 这里演示单数据源事务
@DataSource("mysql")
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
// 操作mysql数据源
userMapper.updateBalance(dto.getUserId(), dto.getAmount().negate());
// 这里如果操作sqlserver数据源会导致事务不生效
// 需要引入Seata等分布式事务框架
}
}
5. 性能优化与问题排查
5.1 连接池配置建议
针对不同数据库的优化参数:
| 参数项 | MySQL建议值 | SQL Server建议值 | 说明 |
|---|---|---|---|
| initialSize | 5-10 | 3-5 | SQL Server连接创建成本更高 |
| maxActive | 20-50 | 15-30 | 根据服务器配置调整 |
| minIdle | 5-10 | 3-5 | 保持最小空闲连接 |
| maxWait | 60000 | 60000 | 获取连接超时时间(ms) |
| timeBetweenEvictionRunsMillis | 60000 | 60000 | 检测间隔 |
| minEvictableIdleTimeMillis | 300000 | 300000 | 最小空闲时间 |
5.2 常见问题解决方案
问题1:连接泄漏
症状:应用运行一段时间后出现连接不足
解决方案:
- 开启Druid的监控功能
- 配置removeAbandoned相关参数
- 定期检查连接使用情况
yaml复制spring:
datasource:
druid:
remove-abandoned: true
remove-abandoned-timeout: 1800
log-abandoned: true
问题2:SQL Server时区异常
症状:时间字段出现8小时偏差
解决方案:
- 在连接字符串中添加时区参数
- 使用Java 8的新的时间API
yaml复制url: jdbc:sqlserver://localhost:1433;databaseName=testdb;sendTimeAsDateTime=false
问题3:MyBatisPlus主键策略冲突
症状:插入数据时报主键冲突
解决方案:
- 为不同数据库配置不同的ID生成策略
- 在实体类上明确指定:
java复制@TableId(value = "id", type = IdType.AUTO) // MySQL自增
@TableId(value = "id", type = IdType.INPUT) // SQL Server可能需要
5.3 监控与调优建议
- 集成Druid监控界面:
java复制@Configuration
public class DruidConfig {
@Bean
public ServletRegistrationBean<StatViewServlet> druidServlet() {
ServletRegistrationBean<StatViewServlet> reg = new ServletRegistrationBean<>();
reg.setServlet(new StatViewServlet());
reg.addUrlMappings("/druid/*");
// 添加控制台管理用户
reg.addInitParameter("loginUsername", "admin");
reg.addInitParameter("loginPassword", "admin");
return reg;
}
}
- 监控关键指标:
- 活跃连接数
- 等待线程数
- 执行时间超过1秒的SQL
- 事务提交/回滚比例
- 定期进行压力测试:
使用JMeter模拟多线程场景,观察不同数据源的性能表现
