Spring Boot 多数据源管理实战:dynamic-datasource 从入门到架构落地
在实际企业项目中,单数据源往往很难满足复杂业务需求。比如:
电商系统中,订单库、用户库、商品库拆分在不同数据库;
报表系统需要同时读取业务库和数仓库;
银行、政务、数据治理项目中,需要适配 MySQL、Oracle、PostgreSQL、达梦、GaussDB 等多种数据库;
高并发系统中,需要做主从读写分离,写走 master,读走 slave。
如果我们手动维护多个 DataSource、多个 SqlSessionFactory、多个 TransactionManager,项目会变得非常臃肿。这个时候,dynamic-datasource 就是一个非常实用的多数据源管理框架。
一、为什么项目需要多数据源?
1. 业务拆库
比如一个电商系统:
user_db 用户库
order_db 订单库
product_db 商品库
pay_db 支付库
不同业务模块的数据存储在不同数据库中,一个 Spring Boot 项目可能需要同时访问多个数据库。
2. 读写分离
高并发项目中,数据库一般会采用主从架构:
master:负责写入
slave_1:负责读取
slave_2:负责读取
二、传统多数据源方案的问题
没有 dynamic-datasource 之前,常见写法是手动配置多个数据源:
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
然后还要配置:
SqlSessionFactory
SqlSessionTemplate
TransactionManager
MapperScan
如果有 2 个数据源还能接受,但如果有 5 个、10 个甚至更多数据源,配置会非常复杂。
主要问题有:
1. 配置类太多,维护成本高。
2. Mapper 和数据源强绑定,不够灵活。
3. 业务代码切换数据源不方便。
4. 读写分离、分组负载均衡需要自己实现。
5. 动态新增、删除数据源比较麻烦。
而 dynamic-datasource 的价值就在于:把多数据源管理这件事封装起来,让开发者只需要关心配置和注解。
三、dynamic-datasource 是什么?
dynamic-datasource 是一个 Spring Boot 多数据源快速集成组件。它支持 Spring Boot 1.5.x、2.x、3.x、4.x,不同 Spring Boot 版本需要选择不同 starter。比如 Spring Boot 3.x 使用 dynamic-datasource-spring-boot3-starter,Spring Boot 4.x 使用 dynamic-datasource-spring-boot4-starter。
它主要解决几个问题:
1. 多数据源统一配置。
2. 通过 @DS 注解切换数据源。
3. 支持主从分组和负载均衡。
4. 支持 HikariCP、Druid 等连接池。
5. 支持数据源动态增删。
6. 支持自定义数据源选择策略。
7. 支持 Seata 分布式事务方案。
四、核心原理:dynamic-datasource 是怎么切换数据源的?
先记住一句话:
dynamic-datasource 的本质是基于 Spring 的 AbstractRoutingDataSource 思想,实现运行时动态路由数据源。
简单理解:
业务方法上标注 @DS("order")
↓
AOP 拦截方法调用
↓
把 "order" 放入 ThreadLocal
↓
执行 SQL 前,从 ThreadLocal 获取当前数据源 key
↓
路由到真正的 DataSource
↓
SQL 执行完成后清理 ThreadLocal
整体流程可以理解为:
Controller
↓
Service 方法 @DS("slave")
↓
DynamicDataSourceAnnotationInterceptor
↓
DynamicDataSourceContextHolder
↓
DynamicRoutingDataSource
↓
真实数据源 master / slave / order / user
1. @DS 注解
@DS 是 dynamic-datasource 提供的核心注解。
@DS("order")
public List<Order> listOrders() {
return orderMapper.selectList(null);
}
表示当前方法执行时,切换到 order 数据源。
2. ThreadLocal 保存当前数据源
数据源标识一般会保存在线程上下文中。为什么用 ThreadLocal?
因为一次请求通常由一个线程处理,这个线程中执行 Service、Mapper、SQL 时,都可以通过 ThreadLocal 获取当前应该使用哪个数据源。
类似这样:
public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSource(String dataSourceName) {
CONTEXT_HOLDER.set(dataSourceName);
}
public static String getDataSource() {
return CONTEXT_HOLDER.get();
}
public static void clear() {
CONTEXT_HOLDER.remove();
}
}
当然,框架内部实现比这个更完善,支持嵌套切换。
3. 动态路由数据源
真正执行 SQL 时,不是直接使用某个固定数据源,而是先进入动态路由数据源。
伪代码如下:
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSource();
}
}
它会根据当前线程上下文中的数据源 key,决定使用哪个真实数据源。
五、Spring Boot 项目实战配置
下面以 Spring Boot 3.x 为例。
官方说明中,Spring Boot 3.x 对应的 starter 是 dynamic-datasource-spring-boot3-starter,并要求 JDK 17+。
1. 引入依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
<version>4.5.0</version>
</dependency>
如果是 Spring Boot 2.x,一般使用:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>4.5.0</version>
</dependency>
如果是 Spring Boot 4.x,使用:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot4-starter</artifactId>
<version>4.5.0</version>
</dependency>
Sonatype Central 当前也能查到 dynamic-datasource-spring-boot4-starter 的 4.5.0 版本
2. 配置 application.yml
spring:
datasource:
dynamic:
primary: master
strict: false
datasource:
master:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/master_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
slave:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/slave_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
order:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/order_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
几个关键配置说明:
primary: master
表示默认数据源是 master。如果某个方法没有写 @DS,默认走 master。
strict: false
表示没有匹配到指定数据源时,是否严格报错。
如果设置为:
strict: true
当你写了:
@DS("xxx")
但配置里没有 xxx 数据源,就会直接报错。
六、@DS 注解的使用方式
1. 标在 Service 方法上
最推荐的写法是标在 Service 层方法上。
@Service
public class OrderService {
@Resource
private OrderMapper orderMapper;
@DS("order")
public List<Order> listOrders() {
return orderMapper.selectList(null);
}
}
这样当前方法里的 SQL 都会走 order 数据源。
2. 标在 Service 类上
如果整个 Service 都访问同一个数据源,可以直接标在类上:
@Service
@DS("order")
public class OrderService {
@Resource
private OrderMapper orderMapper;
public List<Order> listOrders() {
return orderMapper.selectList(null);
}
public Order getById(Long id) {
return orderMapper.selectById(id);
}
}
这个类中的所有方法默认走 order 数据源。
3. 方法上的 @DS 优先级更高
@Service
@DS("master")
public class UserService {
@Resource
private UserMapper userMapper;
public User getFromMaster(Long id) {
return userMapper.selectById(id);
}
@DS("slave")
public User getFromSlave(Long id) {
return userMapper.selectById(id);
}
}
这里:
getFromMaster() 走 master
getFromSlave() 走 slave
dynamic-datasource 官方约定中也提到,就近原则下,代码块主动切换优先于方法注解,方法注解优先于类注解。
七、Mapper 层需要加 @DS 吗?
般不建议把 @DS 加在 Mapper 层。
推荐加在 Service 层。
原因是:
1. Service 是业务边界,更适合决定访问哪个库。
2. 一个 Service 方法里可能调用多个 Mapper。
3. Mapper 只负责 SQL 映射,不应该关心业务数据源路由。
4. 事务通常也是加在 Service 层,数据源切换和事务边界放一起更清晰。
八、读写分离配置
dynamic-datasource 支持数据源分组。官方约定中提到,配置文件里以下划线 _ 分割的数据源,首部就是组名,相同组名的数据源会放到同一个组下;切换数据源时可以切换具体数据源名称,也可以切换组名。
例如:
spring:
datasource:
dynamic:
primary: master
datasource:
master:
url: jdbc:mysql://localhost:3306/shop_master
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
slave_1:
url: jdbc:mysql://localhost:3306/shop_slave_1
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
slave_2:
url: jdbc:mysql://localhost:3306/shop_slave_2
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
这里:
master 是一个数据源
slave_1 和 slave_2 属于 slave 分组
查询时可以这样写:
@DS("slave")
public List<Product> listProducts() {
return productMapper.selectList(null);
}
写入时:
@DS("master")
public void saveProduct(Product product) {
productMapper.insert(product);
}
如果指定的是 slave 组,框架会在 slave_1 和 slave_2 中选择一个数据源。
九、事务怎么处理?
这是多数据源里最容易踩坑的地方。
1. 单数据源事务
如果一个方法只操作一个数据源,可以直接使用 @Transactional。
@Service
public class OrderService {
@Resource
private OrderInfoMapper orderInfoMapper;
@DS("order")
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderInfo orderInfo) {
orderInfoMapper.insert(orderInfo);
}
}
这种情况没有问题。
因为当前事务里只使用了 order 一个数据源。
2. 多数据源事务的问题
如果一个方法里同时操作两个数据源:
@Transactional(rollbackFor = Exception.class)
public void createUserAndOrder(UserInfo user, OrderInfo order) {
userInfoService.saveUser(user); // user 数据源
orderInfoService.saveOrder(order); // order 数据源
int i = 1 / 0;
}
这时候要小心。
普通 Spring 本地事务通常只能保证一个数据源的事务一致性。因为一个 DataSourceTransactionManager 一般管理的是一个数据源连接。
如果在一个业务方法里同时操作 user_db 和 order_db,就不是普通本地事务能天然解决的问题了。
你可能会遇到:
user_db 插入成功
order_db 插入失败
或者 order_db 回滚了,但 user_db 没有回滚
3. 多数据源事务的解决方案
方案一:避免跨库强事务
这是最推荐的架构思路。
能不跨库事务,就不要跨库事务。
例如:
用户服务只操作用户库
订单服务只操作订单库
跨服务一致性通过 MQ、最终一致性、补偿任务解决
这也是微服务架构里常见的做法。
方案二:使用 Seata 分布式事务
dynamic-datasource 官方能力中包含“基于 Seata 的分布式事务方案”。
适合场景:
1. 多个数据库必须强一致。
2. 一个业务操作必须同时成功或同时失败。
3. 可以接受 Seata 带来的复杂度和性能损耗。
例如:
@GlobalTransactional(rollbackFor = Exception.class)
public void createUserAndOrder(UserInfo user, OrderInfo order) {
userInfoService.saveUser(user);
orderInfoService.saveOrder(order);
}
不过生产环境是否引入 Seata,需要根据业务场景评估。
方案三:使用 @DSTransactional
dynamic-datasource 也提供了自己的事务注解,一些版本中支持 @DSTransactional。
它适合处理框架内部多数据源事务场景,但你要注意:
1. 它不是万能分布式事务。
2. 它更适合同一个应用内多个数据源的事务协调。
3. 如果涉及多个服务、多个系统,还是要考虑 Seata、MQ 最终一致性、TCC 等方案。
简单示例:
@DSTransactional
public void createUserAndOrder(UserInfo user, OrderInfo order) {
userInfoService.saveUser(user);
orderInfoService.saveOrder(order);
}
理解它时可以这样想:
普通 @Transactional:主要管理一个数据源连接的事务。
@DSTransactional:尝试协调 dynamic-datasource 管理下的多个数据源连接。
Seata @GlobalTransactional:分布式事务框架,适合更复杂的跨库、跨服务事务。
总结:
dynamic-datasource 是一个 Spring Boot 多数据源管理框架,核心是基于动态路由数据源实现的。它通过 @DS 注解配合 AOP 拦截,在方法执行前把数据源标识放入 ThreadLocal,执行 SQL 时由动态路由数据源根据当前线程上下文选择真实 DataSource,方法执行完成后再清理上下文。
它支持主从读写分离、数据源分组、动态增删数据源、敏感配置加密、Hikari/Druid 等连接池集成,也可以结合 Seata 处理分布式事务。实际项目中我一般会把 @DS 放在 Service 层,而不是 Mapper 层,因为 Service 才是业务边界,也更方便和事务控制结合。
需要注意的是,多数据源下普通 @Transactional 不能天然保证多个数据库同时回滚。如果涉及跨库强一致,要考虑 Seata、XA、TCC;如果业务允许最终一致,可以使用 MQ、本地消息表和补偿任务。
更多推荐



所有评论(0)