MyBatis-Plus多数据源配置:轻松实现数据库切换_mybatisplus 多数据源
一套代码连接多个数据库,轻松实现读写分离、多租户等高级功能!
目录
🌟 为什么需要多数据源?
🚀 多数据源的实现方式
动态数据源
多数据源配置
⚙️ 使用dynamic-datasource-spring-boot-starter
步骤1:添加依赖
步骤2:配置多数据源
步骤3:使用@DS注解指定数据源
🔄 多数据源的实际应用
读写分离
业务分库
多租户
🔧 动态创建数据源
🛡️ 数据源事务管理
🔄 数据源切换原理
🔍 数据源切换的注意事项
💡 高级配置
数据源分组
自定义数据源选择策略
🎯 实战案例
📝 小结
⏭️ 下一步学习
🌟 为什么需要多数据源?
在企业级应用中,我们经常需要连接多个数据库,常见场景包括:
💡 提示:MyBatis-Plus提供了强大的多数据源支持,让你轻松应对各种复杂场景!
🚀 多数据源的实现方式
MyBatis-Plus提供了两种多数据源的实现方式:
动态数据源
-
通过AOP和ThreadLocal实现
-
运行时动态切换数据源
-
使用注解或代码控制
-
官方推荐方式
多数据源配置
-
通过配置多个SqlSessionFactory
-
每个数据源单独配置
-
编译时确定数据源
-
传统实现方式
本文将重点介绍第一种方式:动态数据源。
⚙️ 使用dynamic-datasource-spring-boot-starter
步骤1:添加依赖
com.baomidou dynamic-datasource-spring-boot-starter 3.5.2
步骤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 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
注解可以用在类上,也可以用在方法上,方法级别的注解优先级高于类级别的注解。
🔄 多数据源的实际应用
读写分离
@Servicepublic class UserServiceImpl extends ServiceImpl 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 list() { return baseMapper.selectList(null); }}
读写分离的优势:
-
✅ 提高系统吞吐量
-
✅ 减轻主库压力
-
✅ 提高系统可用性
业务分库
@Service@DS(\"user\") // 用户相关操作使用user数据源public class UserServiceImpl extends ServiceImpl implements UserService { // ...}@Service@DS(\"order\") // 订单相关操作使用order数据源public class OrderServiceImpl extends ServiceImpl implements OrderService { // ...}@Service@DS(\"product\") // 商品相关操作使用product数据源public class ProductServiceImpl extends ServiceImpl implements ProductService { // ...}
业务分库的优势:
-
✅ 业务隔离,降低耦合
-
✅ 提高系统扩展性
-
✅ 便于团队协作开发
方式一:使用@DS注解
@Servicepublic class UserServiceImpl extends ServiceImpl 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@Componentpublic 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(); }}
🔧 动态创建数据源
在某些场景下,我们可能需要在运行时动态创建数据源,例如多租户场景下,每个租户使用一个独立的数据库:
@Componentpublic 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); }}
使用示例:
@Servicepublic 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); }}
🛡️ 数据源事务管理
在使用多数据源时,事务管理需要特别注意:
@Configurationpublic 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 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
使用分组:
@Servicepublic 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 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 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@Componentpublic 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(); }}
业务代码无需关心数据源切换:
@Servicepublic class ProductServiceImpl extends ServiceImpl 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自动切换数据源,减少代码侵入性
定期检查数据源健康状态,确保系统稳定性
⏭️ 下一步学习
-
性能分析插件 - 分析SQL执行效率
-
乐观锁插件 - 解决并发更新问题
-
动态表名 - 实现分表操作