SpringBoot配置多数据源, 动态切换数据源.

springBoot配置多个数据源, 自定义策略动态切换数据源.

本篇以mysql数据库主从同步,读写分离场景为例, 有两个数据源: 主数据源source,用于数据更新(update,insert,delete); 从数据源replica1用于数据查询

基于springBoot2.4.0, 使用默认数据库连接池hikari

国际运动的影响, 当前各开源项目已逐步清理master,salve,blacklist,whitelist等术语, mysql分别使用source,replica,blocklist,allowlist替换,所以本例中也同样如此

github: https://github.com/varyuan/awesome-java

  1. application.yml增加自定义数据源配置
dynamic:
  defaultDsKey: replica1
  datasources:
    source:
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3306/yuan?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
      #  连接池配置
      #  最大连接数,默认为10
      maximum-pool-size: 2
      #  连接池名字,默认以HikariPool-1,HikariPool-2形式增长
      pool-name: yuan-source
    replica1:
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3306/yuanbak?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
      #  连接池配置
      maximum-pool-size: 2
      pool-name: yuan-replica1
  1. 定义实体类接收上面的配置. 很多教程教你自定义一个数据源配置properties对应的实体类, 我觉得没有必要, 复用spring原本的就挺好( 在application.ymlspring.datasource 配置对应的实体类是org.springframework.boot.autoconfigure.jdbc.DataSourceProperties, spring.datasource.hikari池配置对应的实体类是 com.zaxxer.hikari.HikariDataSource)
import com.zaxxer.hikari.HikariDataSource;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.Map;

@ConfigurationProperties(prefix = "dynamic")
@Getter
@Setter
public class DynamicDataSourceProperties {
    private Map<String, HikariDataSource> datasources;
    // 默认数据源
    private String defaultDsKey;
}

  1. 重点: 继承AbstractRoutingDataSource抽象类并注册为bean
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Configuration
// 启用DynamicDataSourceProperties配置
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
public class DynamicDataSourceConfig {
    @Resource
    private DynamicDataSourceProperties dynamicDataSourceProperties;

    @Bean
    public AbstractRoutingDataSource dataSource() {
        // 实现AbstractRoutingDataSource的determineCurrentLookupKey方法, 该方法会返回当前要使用的数据源对应的dsKey
        AbstractRoutingDataSource abstractRoutingDataSource = new AbstractRoutingDataSource() {
            @Override
            protected Object determineCurrentLookupKey() {
                return DsKeyThreadLocal.getDsKey();
            }
        };
        Map<String, HikariDataSource> datasources = dynamicDataSourceProperties.getDatasources();
        log.info("all datasource key: {}", datasources.keySet());
        // 必须配置默认数据源
        String defaultDsKey = dynamicDataSourceProperties.getDefaultDsKey();
        if (!datasources.containsKey(defaultDsKey)) {
            throw new IllegalArgumentException("must config default datasource");
        }
        log.info("default datasource key: {}", defaultDsKey);
        // 设置所有数据源
        Map<Object, Object> dataSourceMap = new HashMap<>();
        datasources.forEach((dsKey, hikariDataSource) -> {
            dataSourceMap.put(dsKey, hikariDataSource);
        });
        abstractRoutingDataSource.setTargetDataSources(dataSourceMap);
        // 设置默认数据源 当dsKey找不到对应的数据源或没有设置数据源时, 使用默认数据源
        abstractRoutingDataSource.setDefaultTargetDataSource(dataSourceMap.get(defaultDsKey));
        // afterPropertiesSet()方法调用时用来将targetDataSources的属性写入resolvedDataSources中的
        abstractRoutingDataSource.afterPropertiesSet();
        return abstractRoutingDataSource;
    }
}
  1. DsKeyThreadLocal用于存储本次会话使用的数据源
// 使用threadLocal存储当前会话使用的数据源对应的dsKey
public final class DsKeyThreadLocal {

    private static ThreadLocal<String> DS_KEY = new ThreadLocal<>();

    private DsKeyThreadLocal() {
    }

    public static void setDsKey(String dsKey) {
        DS_KEY.set(dsKey);
    }

    public static String getDsKey() {
        return DS_KEY.get();
    }
}
  1. 测试: 在两个数据源中分别造一条id为1,其他字段不同的记录. 查询的结果与对应的数据源记录一致则证明成功
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

@SpringBootTest(classes = App.class)
@Slf4j
public class DictDaoTest {
    @Resource
    DictDao dictDao;

    @Test
    public void dynamicDataSourceTest() {
        // 使用主数据源
        DsKeyThreadLocal.setDsKey("source");
        Dict dict = dictDao.selectById(1);
        log.info("dict : {}", dict);

        // 使用从数据源
        DsKeyThreadLocal.setDsKey("replica1");
        dict = dictDao.selectById(1);
        log.info("dict : {}", dict);
    }
}
  1. 定义动态切换数据源的策略, 可以使用filter或者aop实现, 在业务代码之前设置本次会话要使用的数据源dsKey.

    下面是一种简单的策略: 众所周知, 在restful风格的接口设计中, GET请求用来获取资源, POST请求用来更新资源. 所以可以制定这样一种简单的策略: GET请求使用从数据源, POST请求使用主数据源

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@Slf4j
@Component
public class DsFilter extends OncePerRequestFilter {
    @Resource
    private DynamicDataSourceProperties dynamicDataSourceProperties;

    // 此方法能保证在单个请求线程中每个请求只调用一次
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String method = request.getMethod();
        // GET请求使用从库, POST请求使用主库, 应增加else覆盖所有情况
        if (method.equals("GET")) {
            DsKeyThreadLocal.setDsKey("replica1");
        } else if (method.equals("POST")) {
            DsKeyThreadLocal.setDsKey("source");
        } else {
            DsKeyThreadLocal.setDsKey(dynamicDataSourceProperties.getDefaultDsKey());
        }
        log.info("datasource route to {}", DsKeyThreadLocal.getDsKey());
        filterChain.doFilter(request, response);
    }
}

cook

  1. 较低版本的springBoot配置自定义多数据源应该先要排除数据源自动配置

  2. 动态切换数据源的策略应覆盖所有情况( 即给予当前会话的数据源一个初始值) ,否则会出现当前会话使用的数据源是处理线程在上次会话中使用的. 本来可以设计成获取数据源的DsKeyThreadLocal#getDsKey方法调用一次后自动清除DS_KEY, 但是考虑到一次会话有多个查询语句的情况,所以不能如此.

  3. 如果你看日志会发现每个数据源都会创建一个连接池

  4. 单数据源时配置参考如下:

    spring:
      datasource:
        #    type: com.zaxxer.hikari.HikariDataSource
        username: root
        password: root
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/yuan?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
        hikari:
          maximum-pool-size: 2
          pool-name: yuan-hikari
    
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值