游乐游手机版
首页/数据库/文章详情

MyBatis数据源切换详解与实现

时间:2026-07-24 06:20
MyBatis多数据源切换通过继承AbstractRoutingDataSource实现动态路由,结合ThreadLocal上下文管理器保证线程隔离。利用AOP自定义注解驱动切换,支持读写分离与分片场景。需注意事务边界问题,避免在事务内切换数据源导致失效,适用于高可用、高性能数据库应用。

前言

MyBatis多数据源切换是构建高可用、高性能数据库应用的关键技术之一。当应用程序需要同时与多个数据库交互时,如何让MyBatis在不同数据源之间灵活切换,同时保持代码整洁与高效,是开发者必须掌握的技能。本文将从原理架构、核心代码实现、具体用法、进阶场景及优劣分析等维度,结合代码示例,深入剖析这一技术要点。

一、原理架构

MyBatis的数据源管理核心在于DataSource接口的抽象与路由。在单数据源场景下,配置直接指向一个具体的数据库连接池,简单直接。但在多数据源场景中,需要引入一个“路由层”,由它根据业务规则动态决定使用哪个真实数据源。

1.1 核心架构组件

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

  • Configuration/Environment:MyBatis的全局配置。传统方式下,这里只能配置一个Environment,即只能绑定一个数据源。
  • DynamicDataSource:实现切换的关键组件。在Spring生态中,通常继承AbstractRoutingDataSource,内部维护一个Map,存储Key(如“master”、“slave”)到真实DataSource的映射关系。路由时通过该Map找到对应数据源。
  • DataSourceContextHolder:一个ThreadLocal工具类,用于在当前线程中存储数据源Key,确保数据源选择在同一线程内透传且互不干扰。

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

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

2.1 工作流程时序图

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

2.2 关键代码实现

要实现上述流程,需要编写三个核心组件:上下文管理器、动态数据源类以及配置类。

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

使用ThreadLocal保证线程隔离,这是最基础且有效的实现方式。

public class DataSourceContextHolder {
    private static final ThreadLocal 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,实现路由逻辑。核心是重写determineCurrentLookupKey方法,返回需要使用的数据源Key。

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

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

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

下面这张图展示了代码中需要构建的类体系及其依赖关系:

3.1 Spring Boot 配置示例

在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 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发送至主库。

实现思路可结合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的数据源切换机制,通过AbstractRoutingDataSourceThreadLocal的巧妙结合,提供了一种轻量级、灵活的数据路由方案。

回顾核心要点:

  1. ThreadLocal是灵魂:它保证了在多线程环境下,每个请求的数据源选择互不干扰,这是整个方案的基石。
  2. AOP是翅膀:通过切面将数据源选择逻辑从业务代码中剥离,让代码保持整洁,专注核心业务逻辑。
  3. 事务是双刃剑:使用多数据源时,务必清醒认识事务边界的限制,避免事务失效,这是实践中最大的陷阱。

实际项目中,建议从简单的读写分离入手,逐步引入更复杂的路由策略,同时做好充分的监控与容错准备。

来源:https://juejin.cn/post/7665367969493516342
上一篇SAP HANA到Doris迁移4种方案对比教程 下一篇PostgreSQL WAL CDC系统构建原理与工程实现
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
自增主键值从何而来?深入理解原理,告别只会auto_increment
数据库 · 2026-07-25

自增主键值从何而来?深入理解原理,告别只会auto_increment

KingbaseES推荐使用serial、bigserial、显式sequence或identity列实现自增主键。serial创建integer并关联序列,bigserial对应bigint;显式sequence可自定义起始值等参数;identity有generatedbydefault(允许指定值)与always(禁止)两种模式。

Linux下瀚高数据库授权文件过期及替换解决方案
数据库 · 2026-07-25

Linux下瀚高数据库授权文件过期及替换解决方案

在银河麒麟系统下,瀚高数据库hgdb-4 5试用授权20天到期后需替换正式授权文件。正确操作:停止服务,备份旧文件,将授权文件复制到 opt highgo hgdb-4 5 etc lic 并命名为hgdb lic,设置权限600和属主highgo:highgo,再启动服务。禁止直接修改data目录下的license info文件。

Oracle BLOB实时同步的5大技术挑战与难点解析
数据库 · 2026-07-25

Oracle BLOB实时同步的5大技术挑战与难点解析

OracleBLOB实时同步面临分片组装、多列隔离、长事务跨窗口、事务回滚及大对象资源控制等技术挑战,必须在日志中精确还原完整字段值,才能保证源端与目标端数据完全一致,这对同步系统的稳健性提出了高要求。

MySQL禁用redo日志导致全备失败
数据库 · 2026-07-25

MySQL禁用redo日志导致全备失败

MySQL全量备份失败是由于数据定义语言操作触发排序索引构建,禁用重做日志导致XtraBackup无法获取一致性备份。测试验证表明,优化表语句即使无数据也会触发该问题。根本原因在于排序索引构建过程跳过了重做日志记录,破坏了备份的一致性。

Kafka架构图优化与改进的全面详细步骤与实践指南
数据库 · 2026-07-25

Kafka架构图优化与改进的全面详细步骤与实践指南

Kafka作为实时数据流处理的核心中间件,其底层架构虽已相当成熟,但在实际生产环境中,要充分发挥其性能潜力,仍需落实到具体的调优与架构改造上。核心目标可归纳为三点:如何承载更高的吞吐量、如何保障数据不丢失、以及故障发生时如何快速恢复。本文将从这几个关键方向出发,深入探讨如何真正榨干Kafka集群的性