在实际企业项目中,单数据源往往很难满足复杂业务需求。比如:

电商系统中,订单库、用户库、商品库拆分在不同数据库;
报表系统需要同时读取业务库和数仓库;
银行、政务、数据治理项目中,需要适配 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_1slave_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_dborder_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、本地消息表和补偿任务。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐