Spring Boot 动态数据源实战:多库切换与读写分离的坑

在实际项目中,单一数据源往往无法满足复杂的业务需求:

  • 有些系统需要同时连接多个数据库(例如一个主业务库、一个日志库)。

  • 在高并发场景下,常见的优化方式是 读写分离(主库负责写,从库负责读)。

  • 还有些场景需要 动态切库(例如 SaaS 系统中不同租户对应不同数据库)。

Spring Boot 为我们提供了灵活的扩展点,但在实际开发中,动态数据源与读写分离的配置并非“一次性搞定”,往往伴随着一系列踩坑经验。本文将带你从 基础实现 → 实际应用 → 常见坑点,全面梳理 Spring Boot 动态数据源的正确姿势。


1. 为什么需要动态数据源?

在单体应用阶段,单库完全够用。但随着业务发展,可能遇到以下需求:

  1. 多租户(Multi-Tenant)系统

    • 每个租户一个数据库,系统需要根据请求动态切换数据库。

    • 常见于 SaaS 平台。

  2. 读写分离(Read/Write Splitting)

    • 写操作走主库,读操作走从库,减轻主库压力。

    • 适用于高并发的电商、金融系统。

  3. 分库分表(Sharding)

    • 按用户 ID 或订单号分库分表。

    • 动态选择库成为必然。

因此,Spring Boot 中的数据源切换,已经是进阶开发绕不开的一道坎。

 

 

2. Spring Boot 自带的多数据源方案

Spring Boot 并没有直接提供“动态切库”的功能,但可以借助 AbstractRoutingDataSource 来实现。

2.1 基础实现思路

  1. 定义多个数据源(主库、从库、其他业务库)。

  2. 定义一个路由类继承 AbstractRoutingDataSource,通过上下文(ThreadLocal)决定使用哪个数据源。

  3. 使用 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 动态数据源在 多租户、读写分离、分库分表 等场景中非常常见,但坑点也不少:

  1. 要用 AbstractRoutingDataSource 或现成的 dynamic-datasource-starter 实现。

  2. 注意事务与数据源切换的时机。

  3. 连接池一定要分开配置。

  4. 读写分离要避免脏读,核心业务强制走主库。

  5. 多租户场景还要考虑缓存一致性。

对于中小项目,推荐直接用 MyBatis-Plus 提供的 dynamic-datasource,避免重复造轮子;对于高并发场景,还需要进一步结合分库分表、分布式事务等手段,才能真正支撑复杂业务。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值