1. MyBatis-Plus多数据源配置实战指南
在企业级应用开发中,多数据源配置是一个常见需求。无论是读写分离、业务分库还是多租户架构,都需要我们掌握高效的数据源切换方案。MyBatis-Plus作为MyBatis的增强工具,提供了简洁而强大的多数据源支持,本文将深入解析其实现原理和最佳实践。
1.1 多数据源的典型应用场景
在实际项目中,我们通常会遇到以下几种需要多数据源的场景:
-
读写分离:将数据库的读操作和写操作分离到不同的数据库实例,这是提升系统性能的经典方案。主库负责处理所有写操作(INSERT/UPDATE/DELETE),而从库则处理读操作(SELECT)。这种架构可以有效减轻主库压力,提高系统整体吞吐量。
-
业务分库:随着业务规模扩大,单一数据库可能无法满足性能需求。按照业务模块将数据分散到不同的数据库,比如用户数据、订单数据、商品数据分别存储,可以降低单库压力,提高系统扩展性。
-
多租户架构:在SaaS应用中,不同租户的数据需要严格隔离。为每个租户分配独立的数据库是最彻底的隔离方案,既能保证数据安全,又便于单独备份和恢复。
-
异构数据库集成:某些特殊场景下,我们可能需要同时访问不同类型的数据库,比如MySQL和PostgreSQL,甚至是非关系型数据库。多数据源配置让这种混合架构成为可能。
1.2 MyBatis-Plus多数据源方案选型
MyBatis-Plus提供了两种主要的多数据源实现方式:
动态数据源方案:
- 基于Spring的AbstractRoutingDataSource实现
- 通过ThreadLocal保存当前线程的数据源标识
- 支持运行时动态切换数据源
- 使用@DS注解或编程方式控制数据源选择
- 官方推荐方案,配置简单,灵活性高
多SqlSessionFactory方案:
- 为每个数据源配置独立的SqlSessionFactory
- 数据源在编译时确定,缺乏灵活性
- 需要手动管理不同数据源对应的Mapper
- 适合数据源固定不变的简单场景
对于大多数现代应用,动态数据源方案是更好的选择。它不仅配置简单,还能满足运行时动态切换的需求,特别适合需要弹性扩展的场景。
2. 动态数据源配置详解
2.1 环境准备与依赖配置
首先需要在项目中引入dynamic-datasource-spring-boot-starter依赖。这是一个基于Spring Boot的starter,可以简化配置过程。
xml复制<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
注意:版本号应根据实际需求选择,建议使用最新稳定版。同时确保项目中已经正确配置了Spring Boot和MyBatis-Plus的基础依赖。
2.2 多数据源基础配置
在application.yml中配置多数据源的基本信息:
yaml复制spring:
datasource:
dynamic:
primary: master # 设置默认数据源
strict: false # 是否严格匹配数据源,未匹配到时使用默认数据源
datasource:
master: # 主数据源配置
url: jdbc:mysql://localhost:3306/master_db?useSSL=false&characterEncoding=utf8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
slave1: # 从数据源1
url: jdbc:mysql://localhost:3307/slave_db?useSSL=false&characterEncoding=utf8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
slave2: # 从数据源2
url: jdbc:mysql://localhost:3308/slave_db?useSSL=false&characterEncoding=utf8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
配置说明:
primary指定了默认数据源,当没有明确指定数据源时,系统会使用这个默认值strict模式控制当指定数据源不存在时的行为:true表示抛出异常,false表示回退到默认数据源- 每个数据源需要配置完整的连接信息,包括url、用户名、密码和驱动类
2.3 @DS注解的使用方法
@DS是动态数据源的核心注解,可以用在类或方法上,用于指定数据源。
类级别使用:
java复制@Service
@DS("slave") // 该类所有方法默认使用slave数据源
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
// 方法实现...
}
方法级别使用:
java复制@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
@DS("master") // 此方法使用master数据源
public boolean save(Order order) {
return super.save(order);
}
@DS("slave") // 此方法使用slave数据源
public Order getById(Long id) {
return baseMapper.selectById(id);
}
}
重要规则:方法级别的注解优先级高于类级别。如果一个方法上有@DS注解,则以方法上的为准;如果没有,则使用类上的注解;如果都没有,则使用默认数据源。
3. 高级应用场景实现
3.1 读写分离实战
读写分离是提升数据库性能的经典方案。下面是一个完整的实现示例:
java复制@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
// 写操作使用主库
@DS("master")
public boolean saveProduct(Product product) {
return save(product);
}
// 读操作使用从库
@DS("slave")
public Product getProductById(Long id) {
return getById(id);
}
// 批量查询也使用从库
@DS("slave")
public List<Product> listProducts(Condition condition) {
return list(condition);
}
}
为了进一步简化代码,可以使用AOP自动切换数据源:
java复制@Aspect
@Component
public class DataSourceAspect {
// 定义读操作的切点
@Pointcut("execution(* com..service..*.get*(..)) || " +
"execution(* com..service..*.list*(..)) || " +
"execution(* com..service..*.select*(..))")
public void readPointcut() {}
// 定义写操作的切点
@Pointcut("execution(* com..service..*.save*(..)) || " +
"execution(* com..service..*.update*(..)) || " +
"execution(* com..service..*.delete*(..))")
public void writePointcut() {}
@Before("readPointcut()")
public void beforeRead() {
DynamicDataSourceContextHolder.push("slave");
}
@Before("writePointcut()")
public void beforeWrite() {
DynamicDataSourceContextHolder.push("master");
}
@After("readPointcut() || writePointcut()")
public void afterMethod() {
DynamicDataSourceContextHolder.poll();
}
}
3.2 多租户数据隔离方案
在多租户系统中,每个租户可能需要独立的数据库。下面演示如何动态管理租户数据源:
java复制@Component
public class TenantDataSourceManager {
@Autowired
private DataSource dataSource;
/**
* 添加租户数据源
*/
public void addTenantDataSource(String tenantId, String url, String username, String password) {
HikariDataSource tenantDataSource = new HikariDataSource();
tenantDataSource.setJdbcUrl(url);
tenantDataSource.setUsername(username);
tenantDataSource.setPassword(password);
tenantDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
// 将数据源添加到动态数据源路由
((DynamicRoutingDataSource) dataSource).addDataSource("tenant_" + tenantId, tenantDataSource);
}
/**
* 移除租户数据源
*/
public void removeTenantDataSource(String tenantId) {
((DynamicRoutingDataSource) dataSource).removeDataSource("tenant_" + tenantId);
}
}
使用租户上下文和AOP自动切换数据源:
java复制public class TenantContext {
private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>();
public static void setTenantId(String tenantId) {
CURRENT_TENANT.set(tenantId);
}
public static String getTenantId() {
return CURRENT_TENANT.get();
}
public static void clear() {
CURRENT_TENANT.remove();
}
}
@Aspect
@Component
public class TenantDataSourceAspect {
@Pointcut("@within(org.springframework.stereotype.Service)")
public void servicePointcut() {}
@Before("servicePointcut()")
public void beforeService(JoinPoint joinPoint) {
String tenantId = TenantContext.getTenantId();
if (tenantId != null) {
DynamicDataSourceContextHolder.push("tenant_" + tenantId);
}
}
@After("servicePointcut()")
public void afterService(JoinPoint joinPoint) {
DynamicDataSourceContextHolder.poll();
}
}
4. 核心原理与性能优化
4.1 动态数据源工作原理
MyBatis-Plus的动态数据源功能基于以下几个核心组件:
-
DynamicRoutingDataSource:继承自Spring的AbstractRoutingDataSource,负责实际的数据源路由。它维护了一个数据源映射表,根据当前数据源标识(key)返回对应的真实数据源。
-
DynamicDataSourceContextHolder:使用ThreadLocal保存当前线程的数据源标识。这个类提供了push/poll/peek等静态方法,用于操作当前线程的数据源标识。
-
@DS注解处理器:通过AOP拦截带有@DS注解的方法,在执行前设置数据源标识,执行后清除标识。
工作流程:
- 方法调用被AOP拦截
- 解析@DS注解确定目标数据源
- 将数据源标识存入ThreadLocal
- 执行实际方法
- 方法返回后清除ThreadLocal中的数据源标识
- DynamicRoutingDataSource根据当前标识选择真实数据源
4.2 数据源分组与负载均衡
对于读多写少的场景,我们通常会有多个从库。dynamic-datasource支持将多个数据源分为一组,并提供多种负载均衡策略:
yaml复制spring:
datasource:
dynamic:
primary: master
datasource:
master:
url: jdbc:mysql://master-host:3306/db
username: root
password: 123456
slave1:
url: jdbc:mysql://slave1-host:3306/db
username: root
password: 123456
slave2:
url: jdbc:mysql://slave2-host:3306/db
username: root
password: 123456
strategy: com.baomidou.dynamic.datasource.strategy.RoundRobinDynamicDataSourceStrategy
group:
slave:
- slave1
- slave2
支持的策略包括:
- 随机策略(RandomDynamicDataSourceStrategy)
- 轮询策略(RoundRobinDynamicDataSourceStrategy)
- 自定义策略(实现DynamicDataSourceStrategy接口)
使用分组数据源:
java复制@Service
public class UserServiceImpl implements UserService {
@DS("master") // 写操作使用主库
public void updateUser(User user) {
// 更新逻辑
}
@DS("slave") // 读操作使用从库组
public User getUser(Long id) {
// 查询逻辑
}
}
4.3 事务管理注意事项
多数据源环境下的事务管理需要特别注意:
-
单数据源事务:默认情况下,Spring事务只能保证单个数据源上的原子性。如果一个事务中包含多个数据源的操作,无法保证跨数据源的一致性。
-
事务传播行为:在嵌套方法调用时,内层方法的数据源切换可能会被外层事务覆盖,导致实际使用了错误的数据源。
-
解决方案:
- 避免在事务方法中切换数据源
- 对于需要跨数据源事务的场景,考虑使用分布式事务框架如Seata
- 将不同数据源的操作拆分到不同方法中,避免混用
配置事务管理器:
java复制@Configuration
public class TransactionConfig {
@Bean
public PlatformTransactionManager transactionManager(DynamicRoutingDataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
5. 性能优化与生产实践
5.1 连接池配置建议
正确的连接池配置对系统性能至关重要。dynamic-datasource默认使用HikariCP作为连接池实现,建议根据实际负载调整以下参数:
yaml复制spring:
datasource:
dynamic:
datasource:
master:
url: jdbc:mysql://master-host:3306/db
username: root
password: 123456
hikari:
maximum-pool-size: 20 # 最大连接数
minimum-idle: 10 # 最小空闲连接
connection-timeout: 30000 # 连接超时时间(ms)
idle-timeout: 600000 # 空闲连接超时时间(ms)
max-lifetime: 1800000 # 连接最大存活时间(ms)
配置建议:
- 最大连接数 = (核心数 * 2) + 有效磁盘数
- 对于IO密集型应用,可以适当增大连接数
- 监控连接池使用情况,避免连接泄漏
5.2 监控与健康检查
在生产环境中,应该对数据源进行监控和健康检查:
- 健康检查配置:
yaml复制spring:
datasource:
dynamic:
health:
enabled: true # 启用健康检查
default-valid-query: SELECT 1 # 健康检查SQL
timeout: 3000 # 检查超时时间(ms)
- 自定义健康检查器:
java复制@Component
public class DataSourceHealthChecker implements ApplicationRunner {
@Autowired
private DynamicRoutingDataSource routingDataSource;
@Override
public void run(ApplicationArguments args) {
Map<String, DataSource> dataSources = routingDataSource.getCurrentDataSources();
dataSources.forEach((name, ds) -> {
try (Connection conn = ds.getConnection()) {
conn.createStatement().execute("SELECT 1");
log.info("数据源 {} 健康检查通过", name);
} catch (SQLException e) {
log.error("数据源 {} 健康检查失败: {}", name, e.getMessage());
}
});
}
}
5.3 常见问题排查
问题1:数据源切换不生效
可能原因:
- 方法没有被Spring代理(如内部方法调用)
- 事务注解@Transactional影响了数据源切换
- 异步方法中ThreadLocal失效
解决方案:
- 确保方法是通过Spring代理调用的
- 避免在事务方法中切换数据源
- 对于异步调用,手动传递数据源标识
问题2:连接泄漏
可能原因:
- 没有正确关闭Connection
- 事务未正常结束
- 连接池配置不合理
解决方案:
- 使用try-with-resources确保Connection关闭
- 监控连接池使用情况
- 配置合理的连接超时和回收策略
问题3:性能下降
可能原因:
- 连接池配置不合理
- 数据源过多导致资源竞争
- 频繁的数据源切换开销
解决方案:
- 优化连接池参数
- 合并不必要的独立数据源
- 减少不必要的数据源切换
6. 扩展与定制开发
6.1 自定义数据源选择策略
除了内置的随机和轮询策略,我们可以实现自己的选择逻辑。例如,基于权重的选择策略:
java复制public class WeightedDataSourceStrategy implements DynamicDataSourceStrategy {
private final Map<String, Integer> weights;
private final Random random = new Random();
public WeightedDataSourceStrategy() {
weights = new HashMap<>();
weights.put("slave1", 7); // 70%的权重
weights.put("slave2", 3); // 30%的权重
}
@Override
public String determineDataSource(List<String> dataSources) {
int totalWeight = dataSources.stream()
.mapToInt(ds -> weights.getOrDefault(ds, 1))
.sum();
int randomValue = random.nextInt(totalWeight);
int current = 0;
for (String ds : dataSources) {
current += weights.getOrDefault(ds, 1);
if (randomValue < current) {
return ds;
}
}
return dataSources.get(0);
}
}
注册自定义策略:
yaml复制spring:
datasource:
dynamic:
strategy: com.example.config.WeightedDataSourceStrategy
6.2 动态添加/移除数据源
在某些场景下,我们需要在运行时动态管理数据源:
java复制@Service
public class DynamicDataSourceService {
@Autowired
private DynamicRoutingDataSource routingDataSource;
/**
* 添加新数据源
*/
public void addDataSource(String name, String url, String username, String password) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
routingDataSource.addDataSource(name, dataSource);
}
/**
* 移除数据源
*/
public void removeDataSource(String name) {
routingDataSource.removeDataSource(name);
}
/**
* 获取所有数据源
*/
public Set<String> getDataSourceNames() {
return routingDataSource.getCurrentDataSources().keySet();
}
}
6.3 多数据源与MyBatis-Plus插件集成
MyBatis-Plus提供了多种实用插件,在多数据源环境下也能正常工作:
java复制@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 动态表名插件
DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor();
dynamicTableNameInnerInterceptor.setTableNameHandler((sql, tableName) -> {
// 动态表名逻辑
return tableName;
});
interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);
return interceptor;
}
}
这些插件会在SQL执行前后进行拦截处理,与数据源路由机制协同工作。