MyBatis数据源切换全面解析

@TOC

前言

MyBatis 的数据源切换是构建高可用、高性能数据库应用的关键技术。本文将从原理架构、核心代码实现、用法详解、初级进阶以及优势劣势等多个维度,结合具体代码示例,深入剖析这一技术。

一、原理架构

MyBatis 的数据源管理核心在于 DataSource 接口的抽象与路由。在单数据源场景下,配置直接指向一个具体的数据库连接池。而在多数据源场景下,我们需要引入一个"路由层",根据业务规则动态选择真正的数据源。

1.1 核心架构组件

下图展示了多数据源切换的整体架构,包括上下文管理、路由数据源以及实际的数据源工厂:

核心原理说明:

  • Configuration/Environment:MyBatis 的全局配置,传统方式下这里只能配置一个 Environment。
  • DynamicDataSource :这是实现切换的关键。在 Spring 生态中,我们通常继承 AbstractRoutingDataSource。它内部维护了一个 Map<Object, Object>,存储了 Key(如 "master", "slave")到真实 DataSource 的映射。
  • DataSourceContextHolder:一个 ThreadLocal 工具类,用于在当前线程中存储数据源的 Key,确保数据源选择在同一个线程内是透传的。

二、核心工作流程与代码实现

数据源切换并不是发生在 SQL 解析阶段,而是发生在获取数据库连接(Connection)的时刻。

2.1 工作流程时序图

下图展示了从 Service 层调用到获取数据库连接的完整时序:

2.2 关键代码实现

要实现上述流程,我们需要编写三个核心部分:上下文管理器、动态数据源类和配置类。

(1) 上下文管理器 (DataSourceContextHolder)

使用 ThreadLocal 保证线程隔离。

java 复制代码
public class DataSourceContextHolder {
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
    /**
     * 设置当前线程的数据源Key
     */
    public static void setDataSourceKey(String key) {
        CONTEXT_HOLDER.set(key);
    }
    /**
     * 获取当前线程的数据源Key
     */
    public static String getDataSourceKey() {
        return CONTEXT_HOLDER.get();
    }
    /**
     * 清除当前线程的数据源Key(非常重要,防止内存泄漏和污染)
     */
    public static void clearDataSourceKey() {
        CONTEXT_HOLDER.remove();
    }
}
(2) 动态数据源

继承 Spring 的 AbstractRoutingDataSource,实现路由逻辑。

java 复制代码
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        // 直接从上下文中获取Key
        return DataSourceContextHolder.getDataSourceKey();
    }
}

三、动态数据源实现架构与配置

下图展示了我们在代码中需要构建的类体系及其依赖关系:

3.1 Spring Boot 配置示例

我们需要配置多个真实的数据源 Bean,并将它们注入到 DynamicDataSource 中。

java 复制代码
@Configuration
public class DataSourceConfig {
    // 1. 配置主数据源
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }
    // 2. 配置从数据源
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }
    // 3. 配置动态数据源(Primary)
    @Bean
    @Primary
    public DynamicDataSource dynamicDataSource(
            @Qualifier("masterDataSource") DataSource masterDataSource,
            @Qualifier("slaveDataSource") DataSource slaveDataSource) {
        
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", masterDataSource);
        targetDataSources.put("slave", slaveDataSource);
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        // 设置目标数据源映射
        dynamicDataSource.setTargetDataSources(targetDataSources);
        // 设置默认数据源(通常为主库)
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
        
        return dynamicDataSource;
    }
}

四、数据源切换策略与用法

如何决定使用哪个数据源?常见的策略包括注解驱动、方法名约定和手动切换。 下图梳理了常见的路由策略:

4.1 策略一:自定义注解 + AOP(推荐)

这是最优雅的方式,对业务代码侵入性最小。 第一步:定义注解

java 复制代码
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
    String value() default "master"; // 默认主库
}

第二步:定义 AOP 切面

java 复制代码
@Aspect
@Component
public class DataSourceAspect {
    // 拦截带有 @TargetDataSource 注解的方法
    @Before("@annotation(targetDataSource)")
    public void before(JoinPoint point, TargetDataSource targetDataSource) {
        String dataSourceKey = targetDataSource.value();
        if (StringUtils.isNotBlank(dataSourceKey)) {
            DataSourceContextHolder.setDataSourceKey(dataSourceKey);
            System.out.println("切换数据源到: " + dataSourceKey);
        }
    }
    // 执行完毕后清除上下文
    @After("@annotation(targetDataSource)")
    public void after(TargetDataSource targetDataSource) {
        DataSourceContextHolder.clearDataSourceKey();
    }
}

第三步:业务使用

java 复制代码
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
    // 写操作走主库
    @TargetDataSource("master")
    public void addUser(User user) {
        userMapper.insert(user);
    }
    // 读操作走从库
    @TargetDataSource("slave")
    public User getUserById(Long id) {
        return userMapper.selectById(id);
    }
}

4.2 策略二:手动编程式切换

在某些复杂的动态逻辑中(例如根据前端传入的参数决定租户库),可能需要手动切换。

java 复制代码
public void processOrder(Long userId, Order order) {
    try {
        // 1. 根据用户ID计算分片key,切换到对应的用户库
        String shardKey = "shard_" + (userId % 4);
        DataSourceContextHolder.setDataSourceKey(shardKey);
        // 2. 执行业务操作(此时会路由到指定分片)
        orderMapper.insert(order);
        
    } finally {
        // 3. 务必在finally中清理,避免影响后续操作
        DataSourceContextHolder.clearDataSourceKey();
    }
}

五、进阶场景:读写分离架构

读写分离是多数据源最经典的应用场景。应用层通过 AOP 或拦截器,自动将 SELECT 请求发送至从库,INSERT/UPDATE/DELETE 发送至主库。 下图展示了经典的读写分离架构:

实现思路: 可以结合 MyBatis 的 Plugin(插件)机制,在 SQL 执行前解析 SQL 语句类型。如果以 SELECT 开头,则调用 DataSourceContextHolder.setDataSourceKey("slave"),否则设为 master

六、优势与劣势分析

任何技术方案都有其适用场景。下图详细对比了该技术的优势与劣势:

6.1 劣势详解:事务边界问题

这是多数据源最大的坑。Spring 的 @Transactional 注解是基于线程绑定的 Connection 来管理的。 如果在一个事务方法中切换了数据源,Spring 的事务管理器可能无法感知,或者导致事务失效。 错误示例:

java 复制代码
@Transactional // 开启事务,此时获取了 Master 的连接
public void updateAndRead() {
    userMapper.updateUser(user); // 使用 Master 连接
    
    // 尝试切换数据源
    DataSourceContextHolder.setDataSourceKey("slave"); 
    
    // 问题:事务管理器仍持有 Master 的连接,这里可能并不会真的切换,
    // 或者抛出异常,因为 Connection 已经绑定在事务中了。
    userMapper.selectUser(id); 
}

解决方案:

  1. 避免事务内切换:将读写操作拆分到不同的事务方法中。
  2. 编程式事务 :使用 TransactionTemplate 精确控制事务边界。
  3. 分布式事务:如果必须跨库操作(如 Master 写,Slave 读并校验),需要引入 Seata 等分布式事务框架,但这通常用于跨库写,读写分离场景下通常允许最终一致性。

七、配置实现全流程

下图描述了从零开始搭建动态数据源的完整配置步骤:

八、进阶应用场景与演进

随着业务复杂度的提升,数据源切换技术也衍生出了多种高级应用场景。 下图展示了多数据源技术在不同阶段的应用场景及演进: 技术演进时间线:

九、总结

MyBatis 的数据源切换机制通过 AbstractRoutingDataSourceThreadLocal 的巧妙结合,为我们提供了一种轻量级、灵活的数据路由方案。 核心要点回顾:

  1. ThreadLocal 是灵魂:它保证了在多线程环境下,每个请求的数据源选择互不干扰。
  2. AOP 是翅膀:通过切面将数据源选择逻辑从业务代码中剥离,保持代码整洁。
  3. 事务是双刃剑:在使用多数据源时,务必清醒地认识事务边界的限制,避免事务失效。 在实际项目中,建议从简单的读写分离开始,逐步根据业务需求引入更复杂的路由策略,并做好充分的监控与容错准备。
相关推荐
新中地GIS开发老师1 小时前
地信职业百科④:GIS开发工程师
前端·数据库·gis·webgis·三维gis开发
云和恩墨1 小时前
从“第二存储”到多智能体协作,云和恩墨充实数据基础设施产品矩阵
数据库
ClouGence2 小时前
SAP HANA 到 Doris 数据迁移:4 种方案对比与迁移教程
数据库·后端·dba
霸道流氓气质2 小时前
ApiPost 中配置自动获取 Token 并调用业务接口完整指南
java·服务器·数据库
我不想名字重复2 小时前
redis缓存和数据库数据保持一致
数据库·redis·缓存
宇宙第一小趴菜2 小时前
三、Oracle 核心原理
数据库·oracle
码出钞能力3 小时前
apache-shardingsphere-5.5.3自定义分片算法
数据库
兜有米啦3 小时前
数据库第二次作业
数据库
nVisual4 小时前
机柜PDU安装位置与空间建模方案
大数据·网络·数据库·信息可视化·数据中心基础设施管理