除了AOP切面,还有哪些更灵活的数据源切换策略?比如基于注解或自定义路由规则

在Spring Boot中实现数据源切换,除了AOP切面,还有几种非常灵活的策略。下面我用一个表格对比各种方案,然后重点介绍两种最实用的方法:

策略类型 实现方式 灵活性 代码侵入性 适用场景
注解驱动 使用@DS等注解标记方法/类 精确到方法级别的数据源控制
手动编程 在代码中调用API动态切换 最高 复杂业务逻辑、同一方法内多数据源
请求特征路由 基于HTTP头、参数等自动路由 中高 多租户、按客户分库等场景
方法规则路由 基于方法名约定自动切换 有固定命名规范的读写分离

🔁 注解驱动切换

这是目前最流行且优雅的方案,MyBatis-Plus的动态数据源组件提供了开箱即用的支持。

配置依赖

xml 复制代码
<dependency>
    <groupId>com.baomidou</groupId>
-   <artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
-   <version>3.5.0</version>
</dependency>

配置文件

yaml 复制代码
spring:
  datasource:
    dynamic:
      primary: master  # 默认数据源
      strict: false    # 是否严格匹配数据源
      datasource:
        master:
          url: jdbc:mysql://localhost:3306/master
          username: root
          password: master_password
        slave:
          url: jdbc:mysql://localhost:3306/slave
          username: root
          password: slave_password

使用@DS注解

typescript 复制代码
@Service
public class UserService {
    
    // 默认使用主库(写操作)
    public void createUser(User user) {
        userMapper.insert(user);
    }
    
    // 显式指定从库(读操作)
    @DS("slave")
    public User getUserById(Long id) {
        return userMapper.selectById(id);
    }
    
    // 在Mapper层直接指定数据源
    @DS("slave")
    public List<User> findActiveUsers() {
        return userMapper.selectActiveUsers();
    }
}

优点:声明式配置,代码侵入性低,支持类级别和方法级别的精细控制。

⚙️ 手动编程切换

对于需要在同一方法内使用多个数据源的复杂场景,手动控制提供了最大灵活性。

基本API使用

java 复制代码
@Service
public class OrderService {
    
    public void processOrder(Long orderId) {
        // 第一阶段:从主库读取订单
        DynamicDataSourceContextHolder.push("master");
        try {
            Order order = orderMapper.selectById(orderId);
            // 业务处理...
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
        
        // 第二阶段:写入从库进行数据分析
        DynamicDataSourceContextHolder.push("slave");
        try {
            analysisMapper.insertOrderAnalysis(order);
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
    }
}

基于HTTP请求的自动切换

通过过滤器或拦截器实现基于请求特征的路由:

typescript 复制代码
@Component
@WebFilter(urlPatterns = "/*")
public class DataSourceFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                         FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        
        // 从请求头获取数据源标识(如多租户场景)
        String tenantId = httpRequest.getHeader("X-Tenant-Id");
        // 或从请求参数判断业务类型
        String businessType = httpRequest.getParameter("bizType");
        
        String dataSourceKey = determineDataSource(tenantId, businessType);
        
        DynamicDataSourceContextHolder.push(dataSourceKey);
        try {
            chain.doFilter(request, response);
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
    }
    
    private String determineDataSource(String tenantId, String businessType) {
        // 复杂的路由逻辑
        if ("report".equals(businessType)) {
            return "report_db";
        } else if (tenantId != null) {
            return "tenant_" + tenantId;
        }
        return "master"; // 默认数据源
    }
}

🧠 自定义路由规则

通过继承AbstractRoutingDataSource并重写determineCurrentLookupKey()方法,可以实现基于业务逻辑的复杂路由

基于方法名的路由示例

scala 复制代码
public class BusinessRoutingDataSource extends AbstractRoutingDataSource {
    
    @Override
    protected Object determineCurrentLookupKey() {
        // 获取当前方法名进行路由决策
        String methodName = getCurrentMethodName();
        
        if (methodName.startsWith("find") || methodName.startsWith("query") 
            || methodName.startsWith("select") || methodName.startsWith("get")) {
            return "slave"; // 读操作路由到从库
        } else if (methodName.startsWith("insert") || methodName.startsWith("update") 
                 || methodName.startsWith("delete") || methodName.startsWith("save")) {
            return "master"; // 写操作路由到主库
        }
        
        // 默认路由策略
        return isReadOperation(methodName) ? "slave" : "master";
    }
}

基于参数值的路由

scala 复制代码
public class ParameterBasedRouter extends AbstractRoutingDataSource {
    
    @Override
    protected Object determineCurrentLookupKey() {
        // 从线程上下文或参数中获取路由信息
        String businessUnit = RequestContextHolder.getBusinessUnit();
        String dataType = RequestContextHolder.getDataType();
        
        // 组合路由逻辑
        if ("finance".equals(businessUnit) && "sensitive".equals(dataType)) {
            return "finance_secure_db";
        } else if ("report".equals(dataType)) {
            return "reporting_db";
        }
        
        return "default_db";
    }
}

⚠️ 实战注意事项

  1. 事务管理 :在手动切换数据源时,确保同一个事务内使用同一数据源,避免跨数据源事务导致的数据不一致问题。
  2. 资源清理 :使用try-finally确保每次数据源切换后都能正确清理上下文,防止内存泄漏数据源污染
  3. 性能考量 :频繁的数据源切换会带来性能开销,在高性能场景下应考虑批量操作连接复用
  4. 降级策略 :始终配置合理的主从延迟处理故障降级机制,确保主库不可用时系统的可用性。

💎 总结

选择合适的数据源切换策略需要权衡灵活性和复杂性。对于大多数场景,注解驱动方案 提供了最佳实践;对于复杂业务逻辑,手动编程控制 不可或缺;而对于有固定路由规则的场景,自定义路由策略能够提供最优雅的解决方案。

相关推荐
弥生赞歌2 小时前
Mysql作业四
数据库·mysql
间彧2 小时前
🏗️ Spring Boot 3实现MySQL读写分离完整指南
数据库
已黑化的小白3 小时前
Rust 的所有权系统,是一场对“共享即混乱”的编程革命
开发语言·后端·rust
PawSQL3 小时前
智能SQL优化工具 PawSQL 月度更新 | 2025年10月
数据库·人工智能·sql·sql优化·pawsql
Ace_31750887763 小时前
淘宝店铺全量商品接口实战:分类穿透采集与增量同步的技术方案
大数据·数据库·python
Gavin_9153 小时前
【Ruby】Mixins扩展方式之include,extend和prepend
数据库·ruby
瀚高PG实验室3 小时前
pg_pdr的生成方式
数据库·瀚高数据库
烤麻辣烫3 小时前
黑马程序员苍穹外卖(新手)Day1
java·数据库·spring boot·学习·mybatis
llxxyy卢4 小时前
SQL注入之堆叠及waf绕过注入(安全狗)
数据库·sql·安全