SpringBoot 动态数据源

解决多租户,多数据源的问题
本demo的技术栈,SpringBoot,SpringCloud Alibaba(dubbo, nacos),hikaricp,druid,mysql

项目地址:https://gitee.com/xuwenjingrencai/dynamic-datasource-spring-boot-starter

1、动态数据源原理

利用aop做的动态数据源的切换,每个线程绑定一个数据源

2、动态数据源基础类

DynamicDatasource

public class DynamicDatasource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDatasourceHandler.getDatasource();
    }

    @Override
    protected DataSource determineTargetDataSource() {
        Object o = determineCurrentLookupKey();
        if (Objects.nonNull(o) && DynamicDatasourceUtil.DATASOURCES.containsKey(o)) {
            return DynamicDatasourceUtil.DATASOURCES.get(o);
        }
        throw new RuntimeException("数据源不存在");
    }
}

DynamicDatasourceHandler

@Slf4j
public class DynamicDatasourceHandler {
    private static ThreadLocal<String> threadDatasource = new InheritableThreadLocal<>();

    public static void setDatasource(String dataSource) {
        threadDatasource.set(dataSource);
    }

    public static String getDatasource() {
        return threadDatasource.get();
    }

    public static void remove() {
        threadDatasource.remove();
    }

    public static void close(DataSource dataSource) {
        Class<? extends DataSource> clazz = dataSource.getClass();
        try {
            Method close = clazz.getDeclaredMethod("close");
            close.invoke(clazz);
            log.debug("该数据源已关闭");
        } catch (Exception e) {
            log.warn("动态数据源关闭错误, 数据源名称 [{}]", dataSource, e);
        }
    }
}

DynamicDatasourceUtil ,对连接池做了简单适配,代码有点长

@Slf4j
public class DynamicDatasourceUtil {

    public static final String MASTER = "master";

    public static final Map<String, DataSource> DATASOURCES = new ConcurrentHashMap<>();

    public static void createDatasource(DynamicDatasourceProperty property) {
        if (DynamicDatasourceUtil.DATASOURCES.containsKey(property.getKey())) {
//            return DATASOURCES.get(property.getKey());
            return;
        }
        DataSource dataSource = null;
        if (Objects.nonNull(property.getType()) && ("com.zaxxer.hikari.HikariDataSource".equals(property.getType()) || "hikari".equals(property.getType()))) {
            dataSource = createHikariDataSource(property);
        } else if (Objects.nonNull(property.getType()) && ("com.alibaba.druid.pool.DruidDataSource".equals(property.getType()) || "druid".equals(property.getType()))) {
            dataSource = createDruidDataSource(property);
        } else {
            dataSource = createDefaultDataSource(property);
        }
        log.info("创建数据源: key: {}, type: {}", property.getKey(), property.getType());
        DynamicDatasourceUtil.DATASOURCES.putIfAbsent(property.getKey(), dataSource);
    }

    private static DataSource createDruidDataSource(DynamicDatasourceProperty property) {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(property.getDriverClassName());
        dataSource.setUrl(property.getUrl());
        dataSource.setUsername(property.getUsername());
        dataSource.setPassword(property.getPassword());
        if (Objects.nonNull(property.getMaxPoolSize())) {
            dataSource.setMaxActive(property.getMaxPoolSize());
        }
        if (Objects.nonNull(property.getInitSize())) {
            dataSource.setInitialSize(property.getInitSize());
        }
        if (Objects.nonNull(property.getMinIdle())) {
            dataSource.setMinIdle(property.getMinIdle());
        }
        if (Objects.nonNull(property.getPoolName())) {
            dataSource.setName(property.getPoolName());
        }
        if (Objects.nonNull(property.getMaxWait())) {
            dataSource.setMaxWait(property.getMaxWait());
        }
        if (Objects.nonNull(property.getValidationQuery())) {
            dataSource.setValidationQuery(property.getValidationQuery());
        }
        return dataSource;
    }

    private static DataSource createDefaultDataSource(DynamicDatasourceProperty property) {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(property.getDriverClassName());
        dataSource.setUrl(property.getUrl());
        dataSource.setUsername(property.getUsername());
        dataSource.setPassword(property.getPassword());
        return dataSource;
    }

    private static HikariDataSource createHikariDataSource(DynamicDatasourceProperty property) {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setDriverClassName(property.getDriverClassName());
        dataSource.setJdbcUrl(property.getUrl());
        dataSource.setUsername(property.getUsername());
        dataSource.setPassword(property.getPassword());
        if (Objects.nonNull(property.getMaxPoolSize())) {
            dataSource.setMaximumPoolSize(property.getMaxPoolSize());
        }
        if (Objects.nonNull(property.getMinIdle())) {
            dataSource.setMinimumIdle(property.getMinIdle());
        }
        if (Objects.nonNull(property.getPoolName())) {
            dataSource.setPoolName(property.getPoolName());
        }
        if (Objects.nonNull(property.getIdleTimeout())) {
            dataSource.setIdleTimeout(property.getIdleTimeout());
        }
        if (Objects.nonNull(property.getMaxLifetime())) {
            dataSource.setMaxLifetime(property.getMaxLifetime());
        }
        if (Objects.nonNull(property.getConnectionTimeout())) {
            dataSource.setConnectionTimeout(property.getConnectionTimeout());
        }
        return dataSource;
    }
}

DynamicDatasourceProperty ,配置类

@Data
public class DynamicDatasourceProperty {

    private String key;

    private String type;

    private String url;

    private String driverClassName;

    private String username;

    private String password;

    //可选参数
    private Integer maxPoolSize;

    private Integer minIdle;

    private String poolName;

    private Long idleTimeout;

    private Long maxLifetime;

    private Long connectionTimeout;

    //druid
    private Integer initSize;

    private Long maxWait;

    private String validationQuery;
}

DataSourceConfig ,注册动态数据源,这里为了和springboot的固定数据源做兼容,用上了@ConditionalOnProperty,当属性为true的时候,切换至动态数据源,否则走springboot的固定数据源

@Data
@Component
@ConditionalOnProperty(name = "spring.dynamicdatasource.enabled", havingValue = "true")
@ConfigurationProperties(prefix = "spring.dynamicdatasource")
public class DataSourceConfig implements InitializingBean {

    private List<DynamicDatasourceProperty> ds = new ArrayList<>();

    private Map<String, DynamicDatasourceProperty> dsMap;

    @Override
    public void afterPropertiesSet() throws Exception {
        dsMap = ds.stream().collect(Collectors.toConcurrentMap(DynamicDatasourceProperty::getKey, x->x));
    }

    @Primary
    @Bean
    public DataSource dataSource() {
        DynamicDatasourceProperty master = dsMap.get(DynamicDatasourceUtil.MASTER);
        DynamicDatasourceUtil.createDatasource(master);
        DynamicDatasource dynamicDatasource = new DynamicDatasource();
        dynamicDatasource.setDefaultTargetDataSource(DynamicDatasourceUtil.DATASOURCES.get(DynamicDatasourceUtil.MASTER));
        dynamicDatasource.setTargetDataSources(new HashMap<>());
        return dynamicDatasource;
    }
}

3、切面

切面注解,DynamicDatasourceAop ,为什么这么写,下面会讲解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Transactional
public @interface DynamicDatasourceAop {
    @AliasFor(annotation = Transactional.class)
    String value() default "";

    @AliasFor(annotation = Transactional.class)
    String transactionManager() default "";

    @AliasFor(annotation = Transactional.class)
    Propagation propagation() default Propagation.REQUIRED;

    @AliasFor(annotation = Transactional.class)
    Isolation isolation() default Isolation.DEFAULT;

    @AliasFor(annotation = Transactional.class)
    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

    @AliasFor(annotation = Transactional.class)
    boolean readOnly() default false;

    @AliasFor(annotation = Transactional.class)
    Class<? extends Throwable>[] rollbackFor() default {};

    @AliasFor(annotation = Transactional.class)
    String[] rollbackForClassName() default {};

    @AliasFor(annotation = Transactional.class)
    Class<? extends Throwable>[] noRollbackFor() default {};

    @AliasFor(annotation = Transactional.class)
    String[] noRollbackForClassName() default {};
}

切面类,DynamicDatasourceAspect

@Configuration
@Slf4j
@Aspect
@Order(value = Ordered.HIGHEST_PRECEDENCE + 10)
@RequiredArgsConstructor
@ConditionalOnProperty(name = "spring.dynamicdatasource.enabled", havingValue = "true")
public class DynamicDatasourceAspect {

    @Autowired
    private DataSourceConfig dataSourceConfig;

    @Before("@annotation(DynamicDatasourceAop)")
    public void before(JoinPoint joinpoint) {
        BaseParam baseParam = null;
        try {
            Object[] args = joinpoint.getArgs();
            for (Object o : args) {
                if (o instanceof BaseParam) {
                    baseParam = (BaseParam) o;
                }
            }
            if (baseParam == null) {
                throw new RuntimeException("没有数据源");
            }
            if (DynamicDatasourceUtil.DATASOURCES.containsKey(baseParam.getDataKey())) {
                DynamicDatasourceHandler.setDatasource(baseParam.getDataKey());
            } else {
                if (dataSourceConfig.getDsMap().containsKey(baseParam.getDataKey())) {
                    DynamicDatasourceUtil.createDatasource(dataSourceConfig.getDsMap().get(baseParam.getDataKey()));
                    DynamicDatasourceHandler.setDatasource(baseParam.getDataKey());
                } else {
                    throw new RuntimeException("数据源不存在");
                }
            }
        } catch (Throwable e) {
            log.error(e.getMessage(), e);
        }

    }

    @After("@annotation(DynamicDatasourceAop)")
    public void after(JoinPoint joinPoint) {
        DynamicDatasourceHandler.remove();
    }
}

4、配置

配置文件,不同的profile加载不同的nacos配置

#application.yml
server:
  port: 0

dubbo:
  application:
    name: cloud-dubbo-provider
  scan:
    base-packages: com.xuwj.provider.dubboService
  protocols:
    dubbo:
      id: dubbo
      name: dubbo
      port: -1

#bootstrap.yml
spring:
  profiles:
#    active: dev
#    active: test
    active: dynamicshort
#    active: dynamiclong
  application:
    name: cloud-dubbo-provider
  cloud:
    nacos:
      discovery:
        enabled: true
        register-enabled: true
        server-addr: 127.0.0.1:8848
        ephemeral: false
      config:
        enabled: true
        server-addr: 127.0.0.1:8848
        file-extension: yml

profile: dev

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
    username: root
    password: root
    druid:
      initial-size: 8
      max-active: 8
      min-idle: 8
      max-wait: 1000
      validation-query: 'select 1'

mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    auto-mapping-behavior: full
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath*:mapper/**/*Mapper.xml
  global-config:
    db-config:
      logic-not-delete-value: 1
      logic-delete-value: 0

profile: test

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/testv?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
    username: root
    password: root
    druid:
      initial-size: 8
      max-active: 8
      min-idle: 8
      max-wait: 1000
      validation-query: 'select 1'

mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    auto-mapping-behavior: full
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath*:mapper/**/*Mapper.xml
  global-config:
    db-config:
      logic-not-delete-value: 1
      logic-delete-value: 0

profile: dynamicshot

spring:
  # datasource:
  #   # type: com.alibaba.druid.pool.DruidDataSource
  #   driverClassName: com.mysql.cj.jdbc.Driver
  #   url: jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
  #   username: root
  #   password: root

  dynamicdatasource:
    enabled: true
    ds:
      - key: master
        type: hikari
        driverClassName: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
        username: root
        password: root

      - key: test1
        type: hikari
        driverClassName: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/test1?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
        username: root
        password: root

      - key: test2
        # type: hikari
        driverClassName: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/test2?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
        username: root
        password: root

      - key: testv1
        type: hikari
        driverClassName: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/testv1?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
        username: root
        password: root

      - key: testv2
        # type: druid
        driverClassName: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/testv2?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
        username: root
        password: root

mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    auto-mapping-behavior: full
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath*:mapper/**/*Mapper.xml
  global-config:
    db-config:
      logic-not-delete-value: 1
      logic-delete-value: 0


5、总结

本案例可以支持根据配置文件的配置,走springboot的固定数据源或者动态数据源,动态数据源中根据key匹配不同的数据源,根据type开启对应的连接池,动态数据源是懒加载的方式,用到了,再去配置中寻找数据源的配置,初始化数据源。
如果仔细看,可以发现,本案例已经将事务和动态数据源的切面的注解合二为一了,具体的心路历程:
1、做动态数据源,切面注解 @DynamicDatasourceAop 中是空的,实现了动态切换数据源
2、支持事务,发现事务注解 @Transactional 只能放在 切面注解标的方法的内层方法上面,如果放在一起,会先执行事务,发现没有数据源,报错
3、将两个注解放在一起,思考事务和切面都是走的aop,应该可以规定一个顺序,此时发现开启事务的注解 @EnableTransactionManagement 里面的 int order() default Ordered.LOWEST_PRECEDENCE; 是最低优先级,于是在切面类上标注 @Order(value = Ordered.HIGHEST_PRECEDENCE),报错,

java.lang.IllegalStateException: No MethodInvocation found: Check that an AOP invocation is in progress and that the ExposeInvocationInterceptor is upfront in the interceptor chain. Specifically, note that advices with order HIGHEST_PRECEDENCE will execute before ExposeInvocationInterceptor! In addition, ExposeInvocationInterceptor and ExposeInvocationInterceptor.currentInvocation() must be invoked from the same thread.

,寻找原因,是order的问题,于是修改order:@Order(value = Ordered.HIGHEST_PRECEDENCE + 10),正常启动,事务和动态数据源兼容
4、合并注解,参考RestController的注解获得现在的注解

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值