在实际项目中,单一数据源往往无法满足复杂的业务需求:
-
有些系统需要同时连接多个数据库(例如一个主业务库、一个日志库)。
-
在高并发场景下,常见的优化方式是 读写分离(主库负责写,从库负责读)。
-
还有些场景需要 动态切库(例如 SaaS 系统中不同租户对应不同数据库)。
Spring Boot 为我们提供了灵活的扩展点,但在实际开发中,动态数据源与读写分离的配置并非“一次性搞定”,往往伴随着一系列踩坑经验。本文将带你从 基础实现 → 实际应用 → 常见坑点,全面梳理 Spring Boot 动态数据源的正确姿势。
1. 为什么需要动态数据源?
在单体应用阶段,单库完全够用。但随着业务发展,可能遇到以下需求:
-
多租户(Multi-Tenant)系统
-
每个租户一个数据库,系统需要根据请求动态切换数据库。
-
常见于 SaaS 平台。
-
-
读写分离(Read/Write Splitting)
-
写操作走主库,读操作走从库,减轻主库压力。
-
适用于高并发的电商、金融系统。
-
-
分库分表(Sharding)
-
按用户 ID 或订单号分库分表。
-
动态选择库成为必然。
-
因此,Spring Boot 中的数据源切换,已经是进阶开发绕不开的一道坎。
2. Spring Boot 自带的多数据源方案
Spring Boot 并没有直接提供“动态切库”的功能,但可以借助 AbstractRoutingDataSource 来实现。
2.1 基础实现思路
-
定义多个数据源(主库、从库、其他业务库)。
-
定义一个路由类继承
AbstractRoutingDataSource,通过上下文(ThreadLocal)决定使用哪个数据源。 -
使用 AOP 或注解,在方法执行前决定切换哪个库。
2.2 代码示例
// 1. 定义一个数据源上下文工具类
public class DataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
public static void set(String datasource) {
CONTEXT.set(datasource);
}
public static String get() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}
// 2. 路由类
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.get();
}
}
在配置类里将主库、从库注入,并设置默认数据源。
@Configuration
public class DataSourceConfig {
@Bean
@Primary
public DataSource dataSource(
@Qualifier("masterDataSource") DataSource master,
@Qualifier("slaveDataSource") DataSource slave) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", master);
targetDataSources.put("slave", slave);
DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
dynamicDataSource.setDefaultTargetDataSource(master);
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}
}
最后,通过 AOP 注解实现自动切换:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DS {
String value() default "master";
}
@Aspect
@Component
public class DynamicDataSourceAspect {
@Around("@annotation(ds)")
public Object switchDataSource(ProceedingJoinPoint point, DS ds) throws Throwable {
try {
DataSourceContextHolder.set(ds.value());
return point.proceed();
} finally {
DataSourceContextHolder.clear();
}
}
}
使用时只需要在方法上加注解:
@Service
public class UserService {
@DS("slave")
public List<User> getUsers() {
return userMapper.selectList(null);
}
}
3. 实战:读写分离的实现
3.1 常见思路
-
写操作 → 主库
-
读操作 → 从库
可以在 @DS 注解中手动指定,或者结合 MyBatis 插件自动识别 SQL 类型。
3.2 自动识别 SQL(常见做法)
很多公司会在 MyBatis 的拦截器里,根据 SQL 判断是 SELECT 还是 INSERT/UPDATE/DELETE,然后动态切换数据源。
@Intercepts({@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class}
)})
public class ReadWriteInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
DataSourceContextHolder.set("slave");
} else {
DataSourceContextHolder.set("master");
}
return invocation.proceed();
}
}
这样可以减少手动写 @DS("slave") 的麻烦。
4. 常见坑点总结
坑 1:事务与数据源切换冲突
-
Spring 的事务默认基于线程绑定。
-
如果事务开启后切换数据源,可能仍然使用第一个数据源。
解决办法:
-
保证在事务开始前就决定数据源。
-
对读写分离的场景,尽量在 DAO 层切换,而不是 Service 层。
坑 2:连接池配置不当
-
有些同学直接复用一个连接池,但换库。
-
结果导致连接还是老的库,出现“数据乱写”的问题。
解决办法:
-
每个数据源单独配置一个连接池(HikariCP/Druid)。
-
不要混用连接池。
坑 3:懒加载与多数据源
-
MyBatis 懒加载在二次查询时,可能切错库。
-
例如主库事务里,懒加载跑到了从库。
解决办法:
-
关闭懒加载,或者确保上下文数据源不被清除。
坑 4:从库延迟导致“脏读”
-
主库写完后,从库同步存在延迟。
-
读操作立即走从库,可能读不到最新数据。
解决办法:
-
核心业务查询强制走主库(如下单后查询订单)。
-
非核心查询走从库(如报表统计)。
坑 5:多租户与缓存一致性
-
多租户场景下,Redis 也需要区分租户。
-
否则可能出现 A 租户查到 B 租户的数据。
解决办法:
-
在缓存 key 中增加租户前缀。
5. 现成的轮子推荐
如果不想自己实现,可以直接用现成框架:
-
dynamic-datasource-spring-boot-starter
-
MyBatis-Plus 官方出品。
-
支持注解切换、多库、读写分离。
-
生产级项目常用。
-
使用示例:
spring:
datasource:
dynamic:
primary: master
datasource:
master:
url: jdbc:mysql://127.0.0.1:3306/master
username: root
password: 123456
slave:
url: jdbc:mysql://127.0.0.1:3306/slave
username: root
password: 123456
然后在代码里:
@DS("slave")
List<User> selectAll();
完全避免了自己造轮子。
6. 总结
Spring Boot 动态数据源在 多租户、读写分离、分库分表 等场景中非常常见,但坑点也不少:
-
要用
AbstractRoutingDataSource或现成的dynamic-datasource-starter实现。 -
注意事务与数据源切换的时机。
-
连接池一定要分开配置。
-
读写分离要避免脏读,核心业务强制走主库。
-
多租户场景还要考虑缓存一致性。
对于中小项目,推荐直接用 MyBatis-Plus 提供的 dynamic-datasource,避免重复造轮子;对于高并发场景,还需要进一步结合分库分表、分布式事务等手段,才能真正支撑复杂业务。

2万+

被折叠的 条评论
为什么被折叠?



