🔥关注墨瑾轩,带你探索编程的奥秘!🚀
🔥超萌技术攻略,轻松晋级编程高手🚀
🔥技术宝库已备好,就等你来挖掘🚀
🔥订阅墨瑾轩,智趣学习不孤单🚀
🔥即刻启航,编程之旅更有趣🚀
当单库架构遇上多租户时代
“一个系统,如何同时连接10个数据库?MyBatis动态数据源让你轻松应对!”
在现代微服务架构和多租户系统中,多数据库切换已成为刚需。无论是读写分离、多租户隔离,还是跨地域部署,传统单数据源架构都显得力不从心。而MyBatis动态数据源通过运行时动态切换连接,实现了对多个数据库的灵活控制。本文将深入解析其实现原理,并通过完整代码示例带你掌握从零到一的实战技巧。
第一章:动态数据源的核心原理
1.1 什么是动态数据源?
动态数据源(Dynamic DataSource)是指在程序运行时,根据业务需求动态选择不同的数据库连接。其核心在于:
- 多数据源配置:预定义多个数据库连接(主库、从库、租户库等)。
- 运行时决策:通过上下文标识(如线程变量)决定当前操作使用哪个数据源。
- 无缝切换:无需修改SQL语句或业务代码,实现数据库连接的透明切换。
1.2 技术选型:为何选择MyBatis?
优势维度 | 传统JDBC | MyBatis动态数据源 |
---|---|---|
开发效率 | 手动管理连接池 | 注解+自动切换 |
灵活性 | 静态配置,难以扩展 | 运行时动态决策 |
性能 | 长连接复用 | 通过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()
,避免线程复用问题。 - 监控与日志:记录数据源切换日志,便于排查问题。
“真正的动态,不是代码的灵活,而是架构的自由。”
—— 一位多租户系统架构师的感悟