MyBatis数据源切换全面解析

前言

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

一、原理架构

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

1.1 核心架构组件

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

MyBatis数据源架构

Configuration对象

Environment配置

DataSourceFactory

DynamicDataSource

全局配置管理

数据源注册

事务工厂配置

环境ID

TransactionManager

DataSource配置

UNPOOLED工厂

POOLED工厂

JNDI工厂

AbstractRoutingDataSource

determineCurrentLookupKey

路由策略

核心原理说明:

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

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

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

2.1 工作流程时序图

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

数据库 实际数据源 DynamicDataSource Transaction事务 Executor执行器 MyBatis SqlSession 应用程序 数据库 实际数据源 DynamicDataSource Transaction事务 Executor执行器 MyBatis SqlSession 应用程序 读取ThreadLocal中的Key 执行SQL操作 创建执行器 开启事务 getConnection() determineCurrentLookupKey() 根据Key查找并获取连接 建立物理连接 返回连接对象 返回连接 返回连接 执行SQL 发送SQL指令 返回结果 返回结果

2.2 关键代码实现

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

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

使用 ThreadLocal 保证线程隔离。

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,实现路由逻辑。

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

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

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

读取Key

DataSourceContextHolder

-ThreadLocal<String> contextHolder

+setDataSourceKey(String key)

+getDataSourceKey() : String

+clearDataSourceKey()

DynamicDataSource

-Map<Object,Object> targetDataSources

-Object defaultTargetDataSource

+setTargetDataSources(Map)

+setDefaultTargetDataSource(Object)

+determineCurrentLookupKey() : Object

«interface»

DataSourceFactory

+getDataSource() : DataSource

UnpooledDataSourceFactory

+getDataSource() : DataSource

PooledDataSourceFactory

+getDataSource() : DataSource

JndiDataSourceFactory

+getDataSource() : DataSource

3.1 Spring Boot 配置示例

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

@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(推荐)

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

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

第二步:定义 AOP 切面

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

第三步:业务使用

@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 策略二:手动编程式切换

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

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 发送至主库。
下图展示了经典的读写分离架构:

数据库层

数据源层

应用层

写操作

读操作

读操作负载均衡

同步

同步

业务应用

Service层

DAO/Mapper层

DynamicDataSource

主数据源Master

从数据源Slave1

从数据源Slave2

主数据库

从数据库1

从数据库2

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

六、优势与劣势分析

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

数据源切换技术

优势

劣势

🟢 灵活性高

🟢 解耦业务逻辑

🟢 性能优化

🟢 可扩展性强

🔴 事务管理复杂

🔴 连接池资源占用

🔴 调试难度增加

🔴 缓存一致性问题

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

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

@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数据源切换

基础应用

读写分离

主从切换

数据源配置

进阶场景

多租户数据隔离

分库分表

读写负载均衡

高级特性

动态路由策略

数据源健康检查

自动故障转移

事务管理

本地事务

分布式事务

JTA集成

性能优化

连接池调优

缓存策略

SQL优化

监控运维

数据源监控

性能指标收集

告警机制

技术演进时间线:

早期阶段 2013-2015 手动数据源切换 基于ThreadLocal实现 简单的读写分离 发展阶段 2016-2018 注解驱动切换 AOP集成 多数据源事务管理 成熟阶段 2019-2021 动态路由策略 自动故障转移 分布式事务集成 现代阶段 2022-2024 云原生适配 Service Mesh集成 智能数据路由 MyBatis数据源切换技术演进

九、总结

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

  1. ThreadLocal 是灵魂:它保证了在多线程环境下,每个请求的数据源选择互不干扰。
  2. AOP 是翅膀:通过切面将数据源选择逻辑从业务代码中剥离,保持代码整洁。
  3. 事务是双刃剑:在使用多数据源时,务必清醒地认识事务边界的限制,避免事务失效。
    在实际项目中,建议从简单的读写分离开始,逐步根据业务需求引入更复杂的路由策略,并做好充分的监控与容错准备。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木易 士心

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值