MyBatis-Plus多数据源配置:轻松实现数据库切换
/ 设置权重// 70%的概率// 30%的概率@Override.sum();return ds;
一套代码连接多个数据库,轻松实现读写分离、多租户等高级功能!
目录
⚙️ 使用dynamic-datasource-spring-boot-starter
🌟 为什么需要多数据源?
在企业级应用中,我们经常需要连接多个数据库,常见场景包括:
| 应用场景 | 图标 | 描述 | 优势 |
|---|---|---|---|
| 读写分离 | 🔄 | 读操作和写操作使用不同的数据库 | 提高系统性能和吞吐量 |
| 业务分库 | 🏢 | 不同业务模块使用不同的数据库 | 实现业务隔离,降低耦合 |
| 多租户 | 👥 | 不同租户使用不同的数据库 | 实现数据隔离,提高安全性 |
| 异构数据库 | 🔄 | 同时连接不同类型的数据库 | 满足不同业务场景的需求 |
💡 提示:MyBatis-Plus提供了强大的多数据源支持,让你轻松应对各种复杂场景!
🚀 多数据源的实现方式
MyBatis-Plus提供了两种多数据源的实现方式:
动态数据源
-
通过AOP和ThreadLocal实现
-
运行时动态切换数据源
-
使用注解或代码控制
-
官方推荐方式
多数据源配置
-
通过配置多个SqlSessionFactory
-
每个数据源单独配置
-
编译时确定数据源
-
传统实现方式
本文将重点介绍第一种方式:动态数据源。
⚙️ 使用dynamic-datasource-spring-boot-starter
步骤1:添加依赖
<!-- 动态数据源 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
步骤2:配置多数据源
spring:
datasource:
dynamic:
primary: master # 设置默认的数据源
strict: false # 严格匹配数据源,未匹配到时使用默认数据源
datasource:
master: # 主数据源
url: jdbc:mysql://localhost:3306/master_db?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
slave: # 从数据源
url: jdbc:mysql://localhost:3306/slave_db?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
business: # 业务数据源
url: jdbc:mysql://localhost:3306/business_db?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
⚠️ 注意:
primary属性指定了默认数据源,当没有明确指定数据源时,会使用这个默认数据源。
步骤3:使用@DS注解指定数据源
@Service
@DS("master") // 类级别的数据源指定
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
public User getById(Long id) {
return baseMapper.selectById(id);
}
@Override
@DS("slave") // 方法级别的数据源指定,优先级高于类级别
public User getByIdFromSlave(Long id) {
return baseMapper.selectById(id);
}
}
💡 提示:
@DS注解可以用在类上,也可以用在方法上,方法级别的注解优先级高于类级别的注解。
🔄 多数据源的实际应用
读写分离
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
@DS("master") // 写操作使用主库
public boolean save(User user) {
return super.save(user);
}
@Override
@DS("master") // 写操作使用主库
public boolean update(User user) {
return updateById(user);
}
@Override
@DS("slave") // 读操作使用从库
public User getById(Long id) {
return baseMapper.selectById(id);
}
@Override
@DS("slave") // 读操作使用从库
public List<User> list() {
return baseMapper.selectList(null);
}
}
读写分离的优势:
-
✅ 提高系统吞吐量
-
✅ 减轻主库压力
-
✅ 提高系统可用性
业务分库
@Service
@DS("user") // 用户相关操作使用user数据源
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
// ...
}
@Service
@DS("order") // 订单相关操作使用order数据源
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
// ...
}
@Service
@DS("product") // 商品相关操作使用product数据源
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
// ...
}
业务分库的优势:
-
✅ 业务隔离,降低耦合
-
✅ 提高系统扩展性
-
✅ 便于团队协作开发
方式一:使用@DS注解
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
public User getById(Long id) {
// 获取当前租户ID
String tenantId = TenantContext.getTenantId();
// 动态切换数据源
DynamicDataSourceContextHolder.push("tenant_" + tenantId);
try {
return baseMapper.selectById(id);
} finally {
// 恢复数据源
DynamicDataSourceContextHolder.poll();
}
}
}
方式二:使用AOP实现
@Aspect
@Component
public class TenantDataSourceAspect {
@Pointcut("execution(* com.example.service.*.*(..))")
public void servicePointcut() {}
@Before("servicePointcut()")
public void switchDataSource(JoinPoint point) {
// 获取当前租户ID
String tenantId = TenantContext.getTenantId();
if (tenantId != null) {
// 动态切换数据源
DynamicDataSourceContextHolder.push("tenant_" + tenantId);
}
}
@After("servicePointcut()")
public void restoreDataSource(JoinPoint point) {
// 恢复数据源
DynamicDataSourceContextHolder.poll();
}
}
🔧 动态创建数据源
在某些场景下,我们可能需要在运行时动态创建数据源,例如多租户场景下,每个租户使用一个独立的数据库:
@Component
public class DynamicDataSourceCreator {
@Autowired
private DynamicRoutingDataSource dynamicRoutingDataSource;
/**
* 创建数据源
*/
public void createDataSource(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");
// 添加数据源
dynamicRoutingDataSource.addDataSource(name, dataSource);
}
/**
* 移除数据源
*/
public void removeDataSource(String name) {
dynamicRoutingDataSource.removeDataSource(name);
}
}
使用示例:
@Service
public class TenantServiceImpl implements TenantService {
@Autowired
private DynamicDataSourceCreator dataSourceCreator;
@Override
public void registerTenant(String tenantId, String dbUrl, String username, String password) {
// 创建租户数据源
dataSourceCreator.createDataSource("tenant_" + tenantId, dbUrl, username, password);
}
@Override
public void removeTenant(String tenantId) {
// 移除租户数据源
dataSourceCreator.removeDataSource("tenant_" + tenantId);
}
}
🛡️ 数据源事务管理
在使用多数据源时,事务管理需要特别注意:
@Configuration
public class DataSourceTransactionConfig {
@Bean
public PlatformTransactionManager transactionManager(DynamicRoutingDataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
return new TransactionTemplate(transactionManager);
}
}
⚠️ 注意:默认情况下,Spring的事务只能在单个数据源中生效。如果需要跨多个数据源事务,需要使用分布式事务解决方案,如Seata。
🔄 数据源切换原理
动态数据源的核心原理是基于Spring的AbstractRoutingDataSource和ThreadLocal实现的:
-
数据源注册:将多个数据源注册到
DynamicRoutingDataSource中 -
上下文存储:使用
ThreadLocal存储当前线程的数据源标识 -
动态路由:在SQL执行前,根据上下文中的数据源标识选择对应的数据源
-
透明切换:对业务代码透明,无需关心底层实现
public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void push(String dataSourceName) {
CONTEXT_HOLDER.set(dataSourceName);
}
public static String peek() {
return CONTEXT_HOLDER.get();
}
public static void poll() {
CONTEXT_HOLDER.remove();
}
}
🔍 数据源切换的注意事项
-
数据源切换的作用域:数据源切换是基于ThreadLocal实现的,只在当前线程有效
-
事务的影响:在一个事务中切换数据源可能会导致事务失效
-
嵌套调用:在嵌套调用中,内层方法的数据源注解会覆盖外层方法的数据源注解
-
异步调用:在异步调用中,ThreadLocal无法传递,需要特殊处理
💡 高级配置
数据源分组
spring:
datasource:
dynamic:
primary: master
datasource:
master:
url: jdbc:mysql://localhost:3306/master?useUnicode=true&characterEncoding=utf8
username: root
password: 123456
slave_1:
url: jdbc:mysql://localhost:3307/master?useUnicode=true&characterEncoding=utf8
username: root
password: 123456
slave_2:
url: jdbc:mysql://localhost:3308/master?useUnicode=true&characterEncoding=utf8
username: root
password: 123456
# 配置数据源分组
strategy: com.baomidou.dynamic.datasource.strategy.RandomDynamicDataSourceStrategy # 随机策略
group:
slave:
- slave_1
- slave_2
使用分组:
@Service
public class UserServiceImpl implements UserService {
@DS("master") // 使用master数据源
public void write() {
// 写操作
}
@DS("slave") // 使用slave分组,会从slave_1和slave_2中随机选择一个
public void read() {
// 读操作
}
}
自定义数据源选择策略
public class WeightDynamicDataSourceStrategy implements DynamicDataSourceStrategy {
private final Map<String, Integer> weightMap = new HashMap<>();
private final Random random = new Random();
public WeightDynamicDataSourceStrategy() {
// 设置权重
weightMap.put("slave_1", 7); // 70%的概率
weightMap.put("slave_2", 3); // 30%的概率
}
@Override
public String determineDataSource(List<String> dataSources) {
int totalWeight = dataSources.stream()
.mapToInt(ds -> weightMap.getOrDefault(ds, 1))
.sum();
int randomWeight = random.nextInt(totalWeight) + 1;
int current = 0;
for (String ds : dataSources) {
current += weightMap.getOrDefault(ds, 1);
if (randomWeight <= current) {
return ds;
}
}
return dataSources.get(0);
}
}
配置自定义策略:
spring:
datasource:
dynamic:
strategy: com.example.config.WeightDynamicDataSourceStrategy
🎯 实战案例
案例:电商系统的读写分离实现
需求:实现一个电商系统,写操作使用主库,读操作使用从库,提高系统性能。
配置数据源:
spring:
datasource:
dynamic:
primary: master
datasource:
master: # 主库
url: jdbc:mysql://master-db:3306/mall
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
slave_1: # 从库1
url: jdbc:mysql://slave1-db:3306/mall
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
slave_2: # 从库2
url: jdbc:mysql://slave2-db:3306/mall
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
group:
slave:
- slave_1
- slave_2
创建切面自动切换数据源:
@Aspect
@Component
public class DataSourceAspect {
@Pointcut("execution(* com.example.service..*.select*(..))")
public void readPointcut() {}
@Pointcut("execution(* com.example.service..*.get*(..))")
public void readPointcut2() {}
@Pointcut("execution(* com.example.service..*.list*(..))")
public void readPointcut3() {}
@Pointcut("execution(* com.example.service..*.count*(..))")
public void readPointcut4() {}
@Pointcut("execution(* com.example.service..*.save*(..))")
public void writePointcut() {}
@Pointcut("execution(* com.example.service..*.update*(..))")
public void writePointcut2() {}
@Pointcut("execution(* com.example.service..*.delete*(..))")
public void writePointcut3() {}
@Before("readPointcut() || readPointcut2() || readPointcut3() || readPointcut4()")
public void setReadDataSource() {
DynamicDataSourceContextHolder.push("slave");
}
@Before("writePointcut() || writePointcut2() || writePointcut3()")
public void setWriteDataSource() {
DynamicDataSourceContextHolder.push("master");
}
@After("readPointcut() || readPointcut2() || readPointcut3() || readPointcut4() || writePointcut() || writePointcut2() || writePointcut3()")
public void clearDataSource() {
DynamicDataSourceContextHolder.poll();
}
}
业务代码无需关心数据源切换:
@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
// 读操作自动路由到从库
@Override
public Product getById(Long id) {
return baseMapper.selectById(id);
}
// 写操作自动路由到主库
@Override
public boolean updateStock(Long id, Integer stock) {
Product product = new Product();
product.setId(id);
product.setStock(stock);
return updateById(product);
}
}
📝 小结
MyBatis-Plus的动态数据源功能为我们提供了强大而灵活的多数据源支持,主要优势包括:
| 优势 | 描述 |
|---|---|
| 🔹 简单易用 | 通过简单的注解即可实现数据源切换 |
| 🔹 灵活配置 | 支持多种数据源配置方式和策略 |
| 🔹 运行时动态 | 支持运行时动态创建和切换数据源 |
| 🔹 性能优化 | 通过读写分离等策略提高系统性能 |
🔥 最佳实践:
合理规划数据源,避免过多数据源导致管理复杂
注意事务边界,避免跨数据源事务问题
使用AOP自动切换数据源,减少代码侵入性
定期检查数据源健康状态,确保系统稳定性
⏭️ 下一步学习
更多推荐




所有评论(0)