> 技术文档 > MyBatis-Plus多数据源配置:轻松实现数据库切换_mybatisplus 多数据源

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实现的:​

  1. 数据源注册:将多个数据源注册到DynamicRoutingDataSource

  2. 上下文存储:使用ThreadLocal存储当前线程的数据源标识

  3. 动态路由:在SQL执行前,根据上下文中的数据源标识选择对应的数据源

  4. 透明切换:对业务代码透明,无需关心底层实现

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();   }}

🔍 数据源切换的注意事项

  1. 数据源切换的作用域:数据源切换是基于ThreadLocal实现的,只在当前线程有效

  2. 事务的影响:在一个事务中切换数据源可能会导致事务失效

  3. 嵌套调用:在嵌套调用中,内层方法的数据源注解会覆盖外层方法的数据源注解

  4. 异步调用:在异步调用中,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的动态数据源功能为我们提供了强大而灵活的多数据源支持,主要优势包括:

优势 描述 🔹 简单易用 通过简单的注解即可实现数据源切换 🔹 灵活配置 支持多种数据源配置方式和策略 🔹 运行时动态 支持运行时动态创建和切换数据源 🔹 性能优化 通过读写分离等策略提高系统性能

🔥 最佳实践

  1. 合理规划数据源,避免过多数据源导致管理复杂

  2. 注意事务边界,避免跨数据源事务问题

  3. 使用AOP自动切换数据源,减少代码侵入性

  4. 定期检查数据源健康状态,确保系统稳定性

⏭️ 下一步学习

  • 性能分析插件 - 分析SQL执行效率

  • 乐观锁插件 - 解决并发更新问题

  • 动态表名 - 实现分表操作