MyBatis动态数据源:多环境切换的“黑科技”实战指南

🔥关注墨瑾轩,带你探索编程的奥秘!🚀
🔥超萌技术攻略,轻松晋级编程高手🚀
🔥技术宝库已备好,就等你来挖掘🚀
🔥订阅墨瑾轩,智趣学习不孤单🚀
🔥即刻启航,编程之旅更有趣🚀

在这里插入图片描述在这里插入图片描述

当单库架构遇上多租户时代

“一个系统,如何同时连接10个数据库?MyBatis动态数据源让你轻松应对!”
在现代微服务架构和多租户系统中,多数据库切换已成为刚需。无论是读写分离、多租户隔离,还是跨地域部署,传统单数据源架构都显得力不从心。而MyBatis动态数据源通过运行时动态切换连接,实现了对多个数据库的灵活控制。本文将深入解析其实现原理,并通过完整代码示例带你掌握从零到一的实战技巧。


第一章:动态数据源的核心原理

1.1 什么是动态数据源?

动态数据源(Dynamic DataSource)是指在程序运行时,根据业务需求动态选择不同的数据库连接。其核心在于:

  • 多数据源配置:预定义多个数据库连接(主库、从库、租户库等)。
  • 运行时决策:通过上下文标识(如线程变量)决定当前操作使用哪个数据源。
  • 无缝切换:无需修改SQL语句或业务代码,实现数据库连接的透明切换。

1.2 技术选型:为何选择MyBatis?

优势维度传统JDBCMyBatis动态数据源
开发效率手动管理连接池注解+自动切换
灵活性静态配置,难以扩展运行时动态决策
性能长连接复用通过AbstractRoutingDataSource优化切换成本
生态支持无框架支持与Spring/Spring Boot深度集成

第二章:环境准备与依赖配置

2.1 项目结构与依赖

推荐技术栈

  • Spring Boot 3.0+
  • MyBatis Plus 3.5+
  • MySQL 8.0+

pom.xml依赖配置

<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <!-- MyBatis Plus -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.3.1</version>
    </dependency>

    <!-- MySQL驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <version>8.0.33</version>
    </dependency>

    <!-- AOP支持(用于数据源切换) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
</dependencies>

第三章:核心实现:从配置到切换

3.1 多数据源配置

application.yml配置示例

spring:
  datasource:
    dynamic:
      primary: master # 默认主数据源
      datasource:
        master:
          url: jdbc:mysql://localhost:3306/master_db?useSSL=false&serverTimezone=UTC
          username: root
          password: root123
        slave1:
          url: jdbc:mysql://localhost:3306/slave1_db?useSSL=false&serverTimezone=UTC
          username: root
          password: root123
        tenant1:
          url: jdbc:mysql://localhost:3306/tenant1_db?useSSL=false&serverTimezone=UTC
          username: root
          password: root123

3.2 动态数据源类实现

DynamicDataSource.java

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

import javax.sql.DataSource;
import java.util.Map;

/**
 * 动态数据源核心类:继承AbstractRoutingDataSource,实现数据源切换逻辑
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    // 存储所有数据源的映射关系
    private final Map<String, DataSource> targetDataSources;

    public DynamicDataSource(Map<String, DataSource> targetDataSources) {
        this.targetDataSources = targetDataSources;
    }

    /**
     * 核心方法:确定当前线程应使用的数据源Key
     * @return 数据源标识(如master/slave1/tenant1)
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }

    /**
     * 初始化方法:设置数据源映射和默认数据源
     */
    public void init() {
        setTargetDataSources(targetDataSources);
        setDefaultTargetDataSource(targetDataSources.get("master"));
        afterPropertiesSet(); // 触发初始化
    }
}

关键注释

  • determineCurrentLookupKey():Spring会调用此方法获取当前线程应使用的数据源标识。
  • DataSourceContextHolder:通过ThreadLocal保存线程级的数据源上下文(见下一节)。

3.3 上下文持有器:线程安全的数据源标识

DataSourceContextHolder.java

public class DataSourceContextHolder {

    // 使用ThreadLocal保证线程安全
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    /**
     * 设置当前线程的数据源标识
     * @param dataSource 数据源名称(如master/slave1)
     */
    public static void setDataSource(String dataSource) {
        contextHolder.set(dataSource);
    }

    /**
     * 获取当前线程的数据源标识
     * @return 数据源名称
     */
    public static String getDataSource() {
        return contextHolder.get();
    }

    /**
     * 清除当前线程的数据源标识(防止内存泄漏)
     */
    public static void clearDataSource() {
        contextHolder.remove();
    }
}

关键注释

  • ThreadLocal:确保每个线程独立存储数据源标识,避免并发冲突。
  • clearDataSource():务必在操作结束时调用,防止内存泄漏(尤其在Web请求中)。

3.4 数据源配置类:整合动态数据源

DataSourceConfig.java

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class DataSourceConfig {

    /**
     * 主数据源配置
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    /**
     * 从数据源1配置
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.slave1")
    public DataSource slave1DataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    /**
     * 租户数据源1配置
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.tenant1")
    public DataSource tenant1DataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    /**
     * 动态数据源Bean:整合所有数据源
     */
    @Bean
    public DataSource dynamicDataSource(
            @Qualifier("masterDataSource") DataSource master,
            @Qualifier("slave1DataSource") DataSource slave1,
            @Qualifier("tenant1DataSource") DataSource tenant1) {

        Map<String, DataSource> targetDataSources = new HashMap<>();
        targetDataSources.put("master", master);
        targetDataSources.put("slave1", slave1);
        targetDataSources.put("tenant1", tenant1);

        DynamicDataSource dynamicDataSource = new DynamicDataSource(targetDataSources);
        dynamicDataSource.init(); // 初始化数据源映射
        return dynamicDataSource;
    }
}

第四章:数据源切换策略:AOP与注解

4.1 自定义注解:标记数据源

@DataSource.java

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    String value() default "master"; // 默认使用主数据源
}

4.2 AOP切面:实现注解驱动的切换

DataSourceAspect.java

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class DataSourceAspect {

    // 切入点:所有带有@DataSource注解的方法
    @Pointcut("@annotation(com.example.demo.datasource.DataSource)")
    public void dataSourcePointCut() {}

    /**
     * 环绕通知:在方法执行前切换数据源,执行后清除上下文
     */
    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        try {
            // 获取方法上的@DataSource注解
            DataSource ds = getDataSourceAnnotation(point);
            if (ds != null) {
                // 设置当前线程的数据源标识
                DataSourceContextHolder.setDataSource(ds.value());
            }
            return point.proceed();
        } finally {
            // 无论成功与否,始终清除上下文
            DataSourceContextHolder.clearDataSource();
        }
    }

    /**
     * 获取@DataSource注解的值
     */
    private DataSource getDataSourceAnnotation(ProceedingJoinPoint point) {
        // 优先获取类级别的注解
        DataSource ds = point.getTarget().getClass().getAnnotation(DataSource.class);
        if (ds != null) {
            return ds;
        }
        // 获取方法级别的注解
        return point.getSignature().getMethod().getAnnotation(DataSource.class);
    }
}

关键注释

  • @Around:环绕通知确保在方法执行前后都能操作数据源上下文。
  • clearDataSource():防止线程复用导致的数据源污染。

第五章:实战案例:多租户系统实现

5.1 用户服务接口示例

UserService.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 查询主库用户(默认数据源)
     */
    public User getUserFromMaster(Long id) {
        return userMapper.selectById(id);
    }

    /**
     * 查询从库用户(通过注解切换数据源)
     */
    @DataSource("slave1")
    public User getUserFromSlave(Long id) {
        return userMapper.selectById(id);
    }

    /**
     * 查询租户1用户(通过注解切换数据源)
     */
    @DataSource("tenant1")
    public User getUserFromTenant1(Long id) {
        return userMapper.selectById(id);
    }
}

5.2 Mapper接口示例

UserMapper.java

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

第六章:性能优化与注意事项

6.1 性能优化策略

优化维度方法效果提升
连接池配置使用HikariCP(默认支持)吞吐量提升30%
AOP效率避免过度切面嵌套减少上下文切换开销
缓存策略对只读操作启用二级缓存减少数据库压力

代码示例:启用MyBatis二级缓存

<!-- UserMapper.xml -->
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

6.2 常见问题与解决方案

问题1:数据源切换失败

错误示例

No data source available for key 'slave1'

解决方案

  • 检查application.yml中是否正确定义了slave1数据源。
  • 确保DynamicDataSource初始化时已注册该数据源。
问题2:线程复用导致数据源污染

现象

  • 在异步任务中切换数据源后,主线程访问数据库异常。

解决方案

  • 在异步任务中显式传递数据源上下文(需自定义线程池)。
  • 或在异步任务中重新设置数据源标识。
问题3:事务管理失效

原因

  • 默认事务管理器绑定到主数据源,切换数据源后事务失效。

解决方案

  • 使用@Transactional(propagation = Propagation.REQUIRES_NEW)显式开启新事务。
  • 或配置多事务管理器(复杂场景)。

第七章:扩展与进阶

7.1 多租户架构:动态Schema切换

// 通过租户ID动态设置Schema
@Aspect
public class TenantAspect {
    @Around("@annotation(com.example.Tenant)")
    public Object switchSchema(ProceedingJoinPoint pjp) throws Throwable {
        String tenantId = getTenantIdFromRequest(); // 从请求头获取租户ID
        DataSourceContextHolder.setDataSource("tenant_" + tenantId);
        return pjp.proceed();
    }
}

7.2 读写分离:基于SQL类型的自动切换

// 在AOP中解析SQL类型(SELECT/INSERT)
@Around("execution(* com.example.mapper.*.*(..))")
public Object autoSwitch(ProceedingJoinPoint pjp) throws Throwable {
    MappedStatement ms = ...; // 获取当前SQL语句
    if (ms.getSqlCommandType() == SqlCommandType.SELECT) {
        DataSourceContextHolder.setDataSource("slave1");
    } else {
        DataSourceContextHolder.setDataSource("master");
    }
    return pjp.proceed();
}

动态数据源的“无限可能”

通过MyBatis动态数据源,我们实现了:

  • 多租户隔离:为每个租户分配独立数据库。
  • 读写分离:自动切换主从库,提升系统性能。
  • 多环境适配:同一套代码支持测试/生产/灰度环境。

最终建议

  • 优先使用注解驱动:通过@DataSource注解简化切换逻辑。
  • 严格清理上下文:每次切换后调用clearDataSource(),避免线程复用问题。
  • 监控与日志:记录数据源切换日志,便于排查问题。

“真正的动态,不是代码的灵活,而是架构的自由。”
—— 一位多租户系统架构师的感悟

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

墨瑾轩

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

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

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

打赏作者

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

抵扣说明:

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

余额充值