SpringBoot 多数据源简单实现

SpringBoot 多数据源简单实现

今天大致看了dynamic-datasource的源码,简单来说就是通过获取配置文件,加载自定义数据源,放入到Map中,当需要使用数据源时,通过getconnet()方法,从ThreadLocal的获取线程需要的数据源标识Key,在从Map中返回。

不过由于集成太多东西了,导致看的眼花缭乱的,根据自己的理解,简单实现下

首先是maven POM

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
    </parent>

    <properties>
        <java.version>8</java.version>
    </properties>
    
    <dependencies>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.0.33</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.yaml</groupId>
            <artifactId>snakeyaml</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter-test</artifactId>
            <version>3.0.3</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.20</version>
        </dependency>
    </dependencies>

环境搭建好了,编写yml文件,具体怎么个格式,看个人喜欢,反正只要能获取到就可以了

server:
  port: 8890
spring:
  datasource:
    custom:
      configs:
        - databaseName: master
          url: jdbc:mysql://192.168.1.10:3306/travel?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
          username: root
          password: root
          driver-class-name: com.mysql.cj.jdbc.Driver
          primary: true --默认数据库
        - databaseName: slave
          url: jdbc:mysql://192.168.1.10:3306/travel_0?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
          username: root
          password: root
          driver-class-name: com.mysql.cj.jdbc.Driver

  application:
    name: TravelOrder

mybatis-plus:
  mapper-locations: classpath:/mapper/*.xml
  type-aliases-package: com.db.entity
  configuration:
    map-underscore-to-camel-case: true


logging:
  level:
    com.order.mapper: debug


创建配置对应的model,

@Data
public class DatasourceProperties{
    private String databaseName; //数据源名称
    private String url; //连接url
    private String username;
    private String password;
    private String driverClassName;
    private Boolean primary = false;
    private int initialSize;
    private int minIdle;
    private int maxActive;
    private long maxWait;
}

然后通过**@ConfigurationProperties(prefix = “spring.datasource.custom”)**读取到configs集合中

@Configuration
@ConfigurationProperties(prefix = "spring.datasource.custom")
public class DataSourceConfig{
    private List<DatasourceProperties> configs;

    public List<DatasourceProperties> getConfigs() {
        return configs;
    }

    public void setConfigs(List<DatasourceProperties> configs) {
        this.configs = configs;
    }
}

既然文件已经读取进来了,就要开始实例化数据源,在DataSourceConfig中添加

    @Bean
    public DataSource dataSource() {
        Map<Object, Object> targetDataSources = new HashMap<>();
        AbstractRoutingDataSource routingDataSource = new AbstractRoutingDataSource() {
            @Override
            protected Object determineCurrentLookupKey() {
                // 根据实际情况返回当前的数据源标识
                return DataSourceContextHolder.getDataSourceType();
            }
        };
		
        //根据配置实例化全部的数据源
        for (DatasourceProperties config : configs) {
            DruidDataSource dataSource = new DruidDataSource();
            dataSource.setUrl(config.getUrl());
            dataSource.setUsername(config.getUsername());
            dataSource.setPassword(config.getPassword());
            dataSource.setDriverClassName(config.getDriverClassName());
            targetDataSources.put(config.getDatabaseName(), dataSource);
            if (config.getPrimary()){
                routingDataSource.setDefaultTargetDataSource(dataSource); // 设置默认数据源
            }
        }

        routingDataSource.setTargetDataSources(targetDataSources);
        return routingDataSource;
    }

这里就是核心实现了,一步一步说首先是AbstractRoutingDataSource,这个类就是自定义获取数据源的关键,复制了一些关键的做下说明

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
	@Nullable
    private Map<Object, DataSource> resolvedDataSources;
    @Nullable
    private Map<Object, Object> targetDataSources;
    
    
    //获取连接
	public Connection getConnection() throws SQLException {
        return this.determineTargetDataSource().getConnection();
    }
	
    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        //获取对应的key,这里是通过对抽象方法的实现,我们直接从ThreadLocal中获取
        Object lookupKey = this.determineCurrentLookupKey();
        //从map里面获取对应的数据源
        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();
    
    //通过在DataSourceConfig,我们把全部的数据源对象都set了
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        this.targetDataSources = targetDataSources;
    }

    //把targetDataSources赋值给resolvedDataSources,resolvedDataSources就是上面determineTargetDataSource()的map
   public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        } else {
            this.resolvedDataSources = new HashMap(this.targetDataSources.size());
            this.targetDataSources.forEach((key, value) -> {
                Object lookupKey = this.resolveSpecifiedLookupKey(key);
                DataSource dataSource = this.resolveSpecifiedDataSource(value);
                this.resolvedDataSources.put(lookupKey, dataSource);
            });
            if (this.defaultTargetDataSource != null) {
                this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
            }

        }
    }
}

所以现在有一个问题就是,有那么多线程,用什么方式把线程隔离开,并且每个线程都能获取到自己需要的数据源呢,所以用到了ThreadLocal(有可能涉及到多个方法多次切换,所以也可以用栈来替换String)

public class DataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public static void setDataSourceType(String dataSourceType) {
        contextHolder.set(dataSourceType);
    }

    public static String getDataSourceType() {
        return contextHolder.get();
    }

    public static void clearDataSourceType() {
        contextHolder.remove();
    }
}

到这一步基本就可以了简单使用下

    @Test
    void contextLoads() throws SQLException {
        DataSourceContextHolder.setDataSourceType("slave");
        Stock stock = stockMapper.queryById(1);
    }

返回

2024-06-26 20:22:02.327  INFO 20656 --- [           main] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
Stock(id=1, orderId=1, num=22)

基本完成了,后面就是通过Aop来简化使用了,直接对Mapper类进行Aop操作,这样就没必要每个方法都设置了

不过如果一个类里面切了多次数据源那么可以先把ThreadLocal里面的值清掉,在根据业务要求切换数据源

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

@Configuration
@Aspect
public class DataSouceAop {

    @Around("@within(db)")
    public Object toChangeDataBase(ProceedingJoinPoint proceedingJoinPoint,DB db) throws Throwable {
        String dbName = db.value();
        DataSourceContextHolder.setDataSourceType(dbName);
        Object proceed = proceedingJoinPoint.proceed();
        DataSourceContextHolder.clearDataSourceType();
        return proceed;
    }
}
@DB("slave") //使用注解
public interface StockMapper {

    /**
     * 通过ID查询单条数据
     *
     * @param id 主键
     * @return 实例对象
     */
    Stock queryById(Integer id);
}
//默认
public interface TorderMapper {

    /**
     * 通过ID查询单条数据
     *
     * @param id 主键
     * @return 实例对象
     */
    Torder queryById(Integer id);
}


如果帮助到,点个赞-。-

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值