一套代码连接多个数据库,轻松实现读写分离、多租户等高级功能!

目录

🌟 为什么需要多数据源?

🚀 多数据源的实现方式

动态数据源

多数据源配置

⚙️ 使用dynamic-datasource-spring-boot-starter

步骤1:添加依赖

步骤2:配置多数据源

步骤3:使用@DS注解指定数据源

🔄 多数据源的实际应用

读写分离

业务分库

多租户

🔧 动态创建数据源

🛡️ 数据源事务管理

🔄 数据源切换原理

🔍 数据源切换的注意事项

💡 高级配置

数据源分组

自定义数据源选择策略

🎯 实战案例

📝 小结

⏭️ 下一步学习


🌟 为什么需要多数据源?

在企业级应用中,我们经常需要连接多个数据库,常见场景包括:

应用场景 图标 描述 优势
读写分离 🔄 读操作和写操作使用不同的数据库 提高系统性能和吞吐量
业务分库 🏢 不同业务模块使用不同的数据库 实现业务隔离,降低耦合
多租户 👥 不同租户使用不同的数据库 实现数据隔离,提高安全性
异构数据库 🔄 同时连接不同类型的数据库 满足不同业务场景的需求

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

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

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

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

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

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

🔍 数据源切换的注意事项

  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

使用分组:

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

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

🔥 最佳实践

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

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

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

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

⏭️ 下一步学习

Logo

一站式 AI 云服务平台

更多推荐