spring项目中多数据源使用

1、多数据源使用场景

  业务复杂,涉及到多个库;读写分离(缓解数据库的读性能瓶颈)

2、实现原理(基于Spring)

  spring-jdbc 模块提供了 AbstractRoutingDataSource,其内部持有一个DataSource集合,可以在运行时动态切换数据源。

  应用直接操作的是AbstractRoutingDataSource的实现类,告诉AbstractRoutingDataSource访问哪个数据库,然后由

  AbstractRoutingDataSource从事先配置好的数据源(ds1、ds2)选择一个,来访问对应的数据库。

2.1、通过 AbstractRoutingDataSource 实现动态数据源

  通过这个类可以实现动态数据源切换,如下是其部分成员变量:

@Nullable
private Map<Object, Object> targetDataSources;
@Nullable
private Object defaultTargetDataSource;
@Nullable
private Map<Object, DataSource> resolvedDataSources;
  • targetDataSources保存了key和数据库连接的映射关系

  • defaultTargetDataSource 标识默认的连接

  • resolvedDataSources这个数据结构是通过targetDataSources构建而来,存储结构也是数据库标识和数据源的映射关系

  AbstractRoutingDataSource 通过实现 DataSource 的 getConnection() 方法在动态调用时获取数据库连接

   getConnection()方法如下:

public Connection getConnection() throws SQLException {
    return this.determineTargetDataSource().getConnection();
}
protected DataSource determineTargetDataSource() {
    Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
    Object lookupKey = this.determineCurrentLookupKey();
    DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
    if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
        dataSource = this.resolvedDefaultDataSource;
    }

    if (dataSource == null) {
        throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
    } else {
        return dataSource;
    }
}
@Nullable
protected abstract Object determineCurrentLookupKey();

  AbstractRoutingDataSource 定义了一个模板方法供我们定制化实现,用来获取 DataSource 标识(可以是字符串、对象等)

  所以到这里,我们只需实现 DynamicDataSource 继承 AbstractRoutingDataSource ,实现其 determineCurrentLookupKey()

总结:实现动态数据源只需做如下4步

  1、定义AbstractRoutingDataSource实现类DynamicDataSource

  2、初始化时为targetDataSources设置 不同数据源的DataSource和标识、及defaultTargetDataSource

  3、在determineTargetDataSource中提供对应的数据源标识即可

  4、切换数据源识即

2.2、示例

  1、数据源配置

spring:
  datasource:
   type: com.alibaba.druid.pool.DruidDataSource
  datasource1:
   jdbc‐url: jdbc:mysql://127.0.0.1:3306/datasource1?serverTimezone=UTC&useUnicode=true&characterEnco
ding=UTF8&useSSL=false
   username: root
   password: 123456
   initial‐size: 1
   min‐idle: 1
   max‐active: 20
   test‐on‐borrow: true
   driver‐class‐name: com.mysql.cj.jdbc.Driver
  datasource2:
   jdbc‐url: jdbc:mysql://127.0.0.1:3306/datasource2?serverTimezone=UTC&useUnicode=true&characterEnco
ding=UTF8&useSSL=false
   username: root
   password: 123456
   initial‐size: 1
   min‐idle: 1
   max‐active: 20
   test‐on‐borrow: true
   driver‐class‐name: com.mysql.cj.jdbc.Driver

  2、动态数据源设置

@Configuration
public class DynamicDataSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.datasource1")
    public DataSource firstDataSource(){
       return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.datasource2")
    public DataSource secondDataSource(){
      return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @Primary
    public DynamicDataSource dataSource(DataSource firstDataSource, DataSource secondDataSource) {
      Map<Object, Object> targetDataSources = new HashMap<>(5);
      targetDataSources.put(DataSourceNames.FIRST, firstDataSource);
      targetDataSources.put(DataSourceNames.SECOND, secondDataSource);
      return new DynamicDataSource(firstDataSource, targetDataSources);
    }
}

  3、DynamicDataSource 类实现

public class DynamicDataSource extends AbstractRoutingDataSource {

  /**
   * ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量。
   * 也就是说 ThreadLocal 可以为每个线程创建一个【单独的变量副本】,相当于线程的 private static 类型变量。
   */
  private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

 /**
  * 决定使用哪个数据源之前需要把多个数据源的信息以及默认数据源信息配置好
  *
  * @param defaultTargetDataSource 默认数据源
  * @param targetDataSources 目标数据源
  */
 public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object>
targetDataSources) {
    super.setDefaultTargetDataSource(defaultTargetDataSource);
    super.setTargetDataSources(targetDataSources);
    super.afterPropertiesSet();
  }

 @Override
 protected Object determineCurrentLookupKey() {
      return getDataSource();
 }

 public static void setDataSource(String dataSource) {
    CONTEXT_HOLDER.set(dataSource);
 }

 public static String getDataSource() {
    return CONTEXT_HOLDER.get();
 }

 public static void clearDataSource() {
    CONTEXT_HOLDER.remove();
 }
}

  4、动态数据源切换具体实现方式

(1)直接通过 setDataSource 在 threadLocal 中设置数据源标识(灵活,但需要在代码处手动设置)

(2)AOP + 自定义注解

  不同业务的数据源,一般通过 AOP + 自定义注解动态切换数据源

  自定义注解:

@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface WR {
    String value() default "W";
}

  切面类:

@Component
@Aspect
public class DynamicDataSourceAspect {
  // 前置通知
  @Before("within(com.tuling.dynamic.datasource.service.impl.*) && @annotation(wr)")
  public void before(JoinPoint joinPoint, WR wr){
      System.out.println(wr.value());
  }
}

  使用注解:

@Service
public class FrendImplService implements FrendService {

  @Autowired
  FrendMapper frendMapper;

  @Override
  @WR("R") // 库2
  public List<Frend> list() {
    return frendMapper.list();
  }

  @Override
  @WR("W") // 库1
  public void save(Frend frend) {
    frendMapper.save(frend);
}

(3)mybatis 插件

  读写分离的数据源,一般可以通过mybatis插件实现。

@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, R
owBounds.class,ResultHandler.class})})
public class DynamicDataSourcePlugin implements Interceptor {

  @Override
  public Object intercept(Invocation invocation) throws Throwable {

    Object[] objects = invocation.getArgs();
    MappedStatement ms = (MappedStatement) objects[0];
    // 读方法
    if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
      DynamicDataSource.name.set("R");
    } else {
    // 写方法
      DynamicDataSource.name.set("W");
    }
   // 修改当前线程要选择的数据源的key
    return invocation.proceed();
  }

  @Override
  public Object plugin(Object target) {
    if (target instanceof Executor) {
       return Plugin.wrap(target, this);
    } else {
       return target;
    }
  }

  @Override
  public void setProperties(Properties properties) {
  }
}

3、多数据源事务控制

(1)spring编程式事务

@Override
public void saveAll(Frend frend) {
    wtransactionTemplate.execute(wstatus ‐> {
    rtransactionTemplate.execute(rstatus ‐> {
       try{
         saveW(frend);
         saveR(frend);
         return true;
       } catch (Exception e){
         wstatus.setRollbackOnly();
         rstatus.setRollbackOnly();
         return false;
       }
     });
     return true;
 });
}

(2)spring声明式事务

@Transactional(transactionManager = "wTransactionManager")
public void saveAll(Frend frend) throws Exception {
     FrendService frendService = (FrendService) AopContext.currentProxy();
     frendService.saveAllR(frend);
}

@Transactional(transactionManager = "rTransactionManager",propagation = Propagation.REQUIRES_NEW )
public void saveAllR(Frend frend) {
     saveW(frend);
     saveR(frend);
}

4、dynamic-datasource多数源组件

  基于 SpringBoot 的多数据源组件,功能强悍,支持 Seata 分布式事务。

  • 支持 数据源分组 ,适用于多种场景 纯粹多库 读写分离 一主多从 混合模式。

  • 支持数据库敏感配置信息 加密 ENC()。

  • 支持每个数据库独立初始化表结构schema和数据库database。

  • 支持无数据源启动,支持懒加载数据源(需要的时候再创建连接)。

  • 支持 自定义注解 ,需继承DS(3.2.0+)。

  • 提供并简化对Druid,HikariCp,BeeCp,Dbcp2的快速集成。

  • 提供对Mybatis­Plus,Quartz,ShardingJdbc,P6sy,Jndi等组件的集成方案。

  • 提供 自定义数据源来源 方案(如全从数据库加载)。

  • 提供项目启动后 动态增加移除数据源 方案。

  • 提供Mybatis环境下的 纯读写分离 方案。

  • 提供使用 spel动态参数 解析数据源方案。内置spel,session,header,支持自定义。

  • 支持 多层数据源嵌套切换 。(ServiceA >>> ServiceB >>> ServiceC)。

  • 提供 基于seata的分布式事务方案。

  • 提供 本地多数据源事务方案。 附:不能和原生spring事务混用。

(1)引入依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic‐datasource‐spring‐boot‐starter</artifactId>
   <version>${version}</version>
</dependency>

(2)使用@DS

  @DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解。没有@DS则使用默认数据源。

@Service
@DS("slave")
public class UserServiceImpl implements UserService {

     @Autowired
     private JdbcTemplate jdbcTemplate;

     public List selectAll() {
         return jdbcTemplate.queryForList("select * from user");
     }

     @Override
     @DS("slave_1")
     public List selectByCondition() {
         return jdbcTemplate.queryForList("select * from user where age >10");
     }
}

(3)本地事务

  使用@DSTransactional即可, 不能和Spring@Transactional混用!

public class AService {

     @DS("a")//如果a是默认数据源则不需要DS注解。
     @DSTransactional
     public void dosomething(){
         BService.dosomething();
         CService.dosomething();
     }
}

public class BService {

     @DS("b")
     public void dosomething(){
         //dosomething
     }
}

public class CService {

     @DS("c")
     public void dosomething(){
         //dosomething
     }
}

  只要@DSTransactional注解下任一环节发生异常,则全局多数据源事务回滚。

  如果BC上也有@DSTransactional会有影响吗?答:没有影响的。

posted @ 2025-07-16 20:59  jingyi_up  阅读(51)  评论(0)    收藏  举报