引言
在Java企业级开发中,数据库操作可以说是最基础也最核心的需求之一。早期我们习惯用单数据源“打天下”——一个应用连一个数据库,写操作、读操作全走同一个连接。但随着业务规模扩大,单数据源的局限性逐渐暴露:主库压力过大却无法分流查询、多租户数据混存导致维护困难、跨库操作事务难以保证……这时候,MyBatis多数据源能力就成了破解困局的关键。
今天这篇文章,笔者将结合企业级开发的实际需求,从为什么需要多数据源、静态/动态配置实战、事务避坑指南到性能优化技巧,带你全面掌握这一核心技能。无论你是刚接触多数据源的新手,还是想优化现有架构的开发者,这篇都能帮你理清思路、避开陷阱,真正把多数据源用对、用好。
一、为什么需要多数据源?常见场景有哪些?
在实际开发中,单数据源往往无法满足复杂业务需求。我总结了3个最常用的场景,看看你有没有遇到过:
1. 主从读写分离
这是最常见的场景!主库负责写(增删改),从库负责读(查询)。主库写操作后立即同步到从库,但可能会有延迟。比如电商大促时,主库压力大,通过读写分离可以把查询压力分散到从库,提升系统吞吐量。
2. 多租户隔离(SaaS系统)
如果你做过SaaS系统,肯定见过“一个租户一个库”的设计。比如为公司A、B、C分别分配独立数据库,通过租户ID动态切换数据源,避免数据混在一起。
3. 异构数据库访问
有时候系统需要同时操作关系型数据库(如MySQL)和非关系型数据库(如Redis、MongoDB)。虽然MyBatis主要操作关系型数据库,但配合其他框架(如Spring Data Redis)也能实现多数据源访问。
二、静态多数据源:固定数据源的场景
如果你的业务中数据源数量固定(比如主库+1个从库),静态多数据源是最直接的选择。它的核心是为每个数据源创建独立的DataSource
、SqlSessionFactory
和事务管理器。
步骤1:环境准备(Spring Boot项目)
首先,在pom.xml
中添加依赖。注意:MyBatis核心库、数据库驱动(如MySQL)、连接池(推荐HikariCP)是必选的。
<!-- MyBatis 核心 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- HikariCP 连接池(Spring Boot默认) -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
步骤2:配置多数据源参数(application.yml)
在配置文件中定义主库和从库的连接信息。注意:每个数据源的jdbc-url
、username
、password
都要单独配置!
spring:
datasource:
# 主库配置(写操作)
master:
jdbc-url: jdbc:mysql://localhost:3306/master_db?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 20 # 最大连接数
connection-timeout: 30000 # 连接超时时间(毫秒)
# 从库配置(读操作)
slave:
jdbc-url: jdbc:mysql://localhost:3307/slave_db?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 15 # 从库压力小,连接数可以少点
步骤3:定义数据源Bean
通过@ConfigurationProperties
绑定配置,并创建两个独立的DataSource
Bean。这里要注意给每个Bean起不同的名字(如masterDataSource
、slaveDataSource
),后续要用到。
@Configuration
public class DataSourceConfig {
// 主库数据源(@Primary表示默认数据源,可选)
@Bean("masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
// 从库数据源
@Bean("slaveDataSource")
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
}
步骤4:配置SqlSessionFactory和事务管理器
每个数据源需要对应的SqlSessionFactory
(生成SqlSession)和事务管理器(控制本地事务)。这里要注意:
SqlSessionFactory
需要指定Mapper XML的路径(避免不同数据源的Mapper冲突)。- 事务管理器必须和
SqlSessionFactory
一一对应。
@Configuration
public class SqlSessionFactoryConfig {
// 主库 SqlSessionFactory
@Bean("masterSqlSessionFactory")
public SqlSessionFactory masterSqlSessionFactory(
@Qualifier("masterDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
// 指定主库 Mapper XML 路径(假设放在 resources/mapper/master 下)
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/master/*.xml"));
// 实体类包(可选,MyBatis自动扫描)
sessionFactory.setTypeAliasesPackage("com.example.entity");
return sessionFactory.getObject();
}
// 从库 SqlSessionFactory
@Bean("slaveSqlSessionFactory")
public SqlSessionFactory slaveSqlSessionFactory(
@Qualifier("slaveDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
// 从库 Mapper XML 路径
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/slave/*.xml"));
sessionFactory.setTypeAliasesPackage("com.example.entity");
return sessionFactory.getObject();
}
// 主库事务管理器(必须和主库 SqlSessionFactory 绑定)
@Bean("masterTransactionManager")
public PlatformTransactionManager masterTransactionManager(
@Qualifier("masterSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new DataSourceTransactionManager(sqlSessionFactory.getDataSource());
}
// 从库事务管理器
@Bean("slaveTransactionManager")
public PlatformTransactionManager slaveTransactionManager(
@Qualifier("slaveSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new DataSourceTransactionManager(sqlSessionFactory.getDataSource());
}
}
步骤5:指定Mapper使用的数据源
通过@MapperScan
注解,告诉MyBatis不同的Mapper接口使用哪个SqlSessionFactory
。例如:主库的Mapper放在com.example.mapper.master
包下,从库的放在com.example.mapper.slave
包下。
@SpringBootApplication
// 扫描主库 Mapper(使用 masterSqlSessionFactory)
@MapperScan(basePackages = "com.example.mapper.master",
sqlSessionFactoryRef = "masterSqlSessionFactory")
// 扫描从库 Mapper(使用 slaveSqlSessionFactory)
@MapperScan(basePackages = "com.example.mapper.slave",
sqlSessionFactoryRef = "slaveSqlSessionFactory")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
静态多数据源的优缺点
优点:配置简单,适合数据源数量固定、无需动态切换的场景(如主从读写分离)。
缺点:数据源数量固定,无法运行时动态调整;如果数据源很多(比如10个租户),配置会变得冗余。
三、动态多数据源:运行时切换的终极方案
如果你的业务需要根据不同的租户、业务类型动态切换数据源(比如SaaS系统),静态多数据源就不够用了。这时候需要用动态数据源路由,核心是继承AbstractRoutingDataSource
,重写determineCurrentLookupKey()
方法。
步骤1:定义动态数据源路由类
通过ThreadLocal
存储当前线程的数据源Key(比如master
或slave
),线程结束后清除Key,避免线程污染。
public class DynamicDataSource extends AbstractRoutingDataSource {
// ThreadLocal 存储当前数据源 Key(线程隔离)
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
// 设置当前数据源 Key(在AOP中调用)
public static void setDataSourceKey(String key) {
CONTEXT_HOLDER.set(key);
}
// 清除当前数据源 Key(避免线程复用导致Key残留)
public static void clearDataSourceKey() {
CONTEXT_HOLDER.remove();
}
// 重写方法,返回当前数据源 Key
@Override
protected Object determineCurrentLookupKey() {
return CONTEXT_HOLDER.get(); // 返回 null 会使用默认数据源
}
}
步骤2:配置动态数据源Bean
将多个数据源(主库、从库、租户库)放入targetDataSources
,并设置默认数据源(未指定Key时使用)。
@Configuration
public class DynamicDataSourceConfig {
@Bean
public DynamicDataSource dynamicDataSource(
@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 默认数据源(未指定Key时使用)
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource); // Key是"master"
targetDataSources.put("slave", slaveDataSource); // Key是"slave"
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(masterDataSource); // 默认主库
return dynamicDataSource;
}
}
步骤3:配置SqlSessionFactory和事务管理器
动态数据源的SqlSessionFactory
和事务管理器与静态类似,但不需要为每个数据源单独配置,统一使用动态数据源。
@Configuration
public class DynamicSqlSessionFactoryConfig {
@Bean("sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(DynamicDataSource dynamicDataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dynamicDataSource); // 使用动态数据源
// 统一 Mapper XML 路径(所有数据源的Mapper放在一起)
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/*.xml"));
sessionFactory.setTypeAliasesPackage("com.example.entity");
return sessionFactory.getObject();
}
@Bean
public PlatformTransactionManager transactionManager(DynamicDataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
}
步骤4:通过AOP动态切换数据源
在需要切换数据源的方法上添加自定义注解(如@SlaveDS
),通过AOP拦截并设置ThreadLocal
中的Key。
4.1 定义自定义注解
// 注解可以加在方法或类上
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SlaveDS {
}
4.2 编写AOP切面
@Aspect
@Component
public class DataSourceAspect {
// 切入点:标注了@SlaveDS的方法
@Pointcut("@annotation(com.example.annotation.SlaveDS)")
public void slaveDSPointcut() {}
// 方法执行前,切换为从库
@Before("slaveDSPointcut()")
public void before(JoinPoint joinPoint) {
DynamicDataSource.setDataSourceKey("slave"); // 切换为从库
}
// 方法执行后,清除Key(重要!避免线程污染)
@After("slaveDSPointcut()")
public void after(JoinPoint joinPoint) {
DynamicDataSource.clearDataSourceKey();
}
}
步骤5:在Service中使用动态数据源
在需要访问从库的方法上添加@SlaveDS
注解,MyBatis会自动使用从库的数据源。
@Service
public class UserService {
@Autowired
private UserMapper userMapper; // Mapper 接口(动态数据源)
// 主库操作(默认,无需注解)
public User getMasterUser(Long id) {
return userMapper.selectById(id);
}
// 从库操作(通过@SlaveDS切换)
@SlaveDS
public User getSlaveUser(Long id) {
return userMapper.selectById(id);
}
}
动态多数据源的注意事项
- 线程安全:必须在
@After
中清除ThreadLocal
的Key!否则线程池复用线程时,下一个请求可能错误地使用上一个请求的数据源。 - 事务限制:动态数据源切换后,本地事务只能作用于当前数据源。如果一个方法中同时操作主库和从库,事务无法保证原子性(需用分布式事务)。
- Mapper冲突:所有数据源的Mapper接口不能同名,否则
@MapperScan
会报错。建议按数据源分包(如master.mapper
、slave.mapper
)。
四、分布式事务:跨数据源的原子性
如果业务需要跨多个数据源操作(比如主库写+从库更新缓存),本地事务无法保证原子性,这时候需要分布式事务解决方案。
常见方案对比
方案 | 原理 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
XA协议 | 两阶段提交(2PC) | 强一致性 | 性能差,锁资源时间长 | 对一致性要求极高的场景 |
Seata AT模式 | 自动生成回滚日志 | 无侵入,性能较好 | 需要改造数据库(自增主键) | 微服务跨库场景 |
最终一致性 | 消息队列补偿 | 性能高,解耦 | 实现复杂,有延迟 | 对一致性要求不高的场景 |
五、实战踩坑指南(避坑!避坑!)
- 连接池配置错误:每个数据源必须独立配置连接池参数!如果主从库共用连接池,可能导致主库连接被从库耗尽。
- 事务失效:动态数据源切换后,如果在同一个方法中调用多个数据源的操作,必须使用分布式事务,否则本地事务无法生效。
- Mapper XML路径错误:静态多数据源中,不同数据源的Mapper XML路径必须严格区分,否则会报“重复映射”错误。
- ThreadLocal未清除:动态数据源中,如果忘记清除
ThreadLocal
的Key,线程池复用线程时会导致数据源切换错误(比如A用户的请求用了B用户的数据源)。
总结
MyBatis多数据源的核心是数据源的隔离与动态路由:
- 静态多数据源适合固定场景(如主从读写分离),配置简单但扩展性差;
- 动态多数据源通过
AbstractRoutingDataSource
和AOP实现运行时切换,适合多租户、SaaS等复杂场景; - 跨数据源操作需用分布式事务,根据业务需求选择XA、Seata或最终一致性方案。
最后,推荐大家试试MyBatis-Plus的dynamic-datasource
模块,它内置了动态数据源支持,通过@DS
注解就能轻松切换,能省去很多配置代码~
如果本文对你有帮助,欢迎点赞收藏,评论区一起交流! 😊