动态数据源,多数据源,读写分离

本文最后更新于:5 个月前

使用springboot给你带来多大便利,你就要接受方便使用带来的相应的复杂度。直线正常跑谁不会给油啊,弯道快,才是真的快。说一说,数据源,就是个DataSource,多数据源就是多个DataSource,读写分离,就是读用一个数据源,写入一个数据源。一般增删改使用主库,查使用从库。配置数据库主从复制。

在springboot中对数据源怎么操作嘞。说思路,springboot中的自动配置用的很舒服,我们要做的就是把springboot这个我们用的很舒服的自动配置给去掉。这个自动配置会往运行环境里添加一个默认的数据源。这个自动配置这块东西不少,各种花里胡哨的注解,我见都是第一次见,你让我知道它的作用,是有一点扯的。害,没有开发springbooot-stater的经验,不宜多说。

手动注入数据源

以前的XML写法

<!--2. 配置数据源-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

JavaConfig写法

@Configuration
public class DataContext {
    @Bean
    public DataSource dataSource(){
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("xxxx");
        dataSource.setUsername("xxxx");
        dataSource.setPassword("xxxx");
        return dataSource;
    }
}

springboot相当于是集JavaConfig于大成者,springboot的stater,大多数就是配置上面的内容,使用springboot的规范就能在启动的时候加载配置类。一般手动注入数据源,需要先把springboot自动加载的配置类给排除掉,改一下启动类的注解@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) 改成这样就可以,为什么我要拿出来说呢,因为在下面实现动态数据源的时候也是需要排除这个的。

动态数据源

浅聊一下,动态数据源是对数据源的高级操作,所以就不要有这样的疑问,动态数据源能不能做到读写分离,能不能多数据源这样的问题。这种问题就和知乎上的提问:请问我拿了诺奖后可不可以保研?

引入依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>${dynamic.verion}</version><!-- maven官网上用最新的版本即可 -->
</dependency>

yml配置

spring:
  # 数据源配置
  datasource:
    # 动态数据源配置
    dynamic:
      # 设置默认的库,默认master
      primary: master
      # 是否启用严格模式,默认不启动. 严格模式下未匹配到数据源直接报错, 非严格模式下则使用默认数据源primary所设置的数据源
      strict: false
      # 是否使用p6spy输出,默认不输出
      p6spy: false
      # 数据源列表
      datasource:
        # 主库
        master:
          url: jdbc:mysql://127.0.0.1:3306/xxxx
          username: root
          password: root
        # 从库1
        second1:
          url: jdbc:mysql://127.0.0.1:3306/xxxx
          username: root
          password: root
        # 从库2
        second2:
          url: jdbc:mysql://127.0.0.1:3306/xxxx
          username: root
          password: root

使用@DS切换数据源

@Service
public class NameServer {
    @Resource
    JdbcTemplate jdbcTemplate;
    @DS(ConstSource.SECOND2)
    public String getName(){
        List<Map<String, Object>> maps = jdbcTemplate.queryForList("select name from wlf");
        return maps.get(0).get("name")+"";
    }
}

// 这里一般对应配置文件中的数据源新建常量。

/**
 * 数据源常量
 */
public class ConstSource {
    public static final String MASTER = "master";
    public static final String SECOND1 = "second1";
    public static final String SECOND2 = "second2";
}

@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解。

注解 结果
没有@DS 默认数据源
@DS(“dsName”) dsName可以为组名也可以为具体某个库的名称

编程式切换数据源

框架提供了编程式切换数据源。稍加封装改造一下。不建议手动修改数据源。

@Component
public class DataContext {

    @Resource
    protected DataSource dataSource;

    @Resource
    protected DefaultDataSourceCreator dataSourceCreator;

    /**
     * 新增数据源
     *
     * @param name 名称
     * @param username 账户
     * @param password 密码
     * @param url 连接
     * @param driver 驱动
     * */
    public void createDataSource(String name, String username, String password, String url, String driver){
        DynamicRoutingDataSource dynamicRoutingDataSource = (DynamicRoutingDataSource) dataSource;

        DataSourceProperty dsp = new DataSourceProperty();
        dsp.setPoolName(name);
        dsp.setUrl(url);
        dsp.setUsername(username);
        dsp.setPassword(password);
        dsp.setDriverClassName(driver);

        DataSource dataSource = dataSourceCreator.createDataSource(dsp);
        dynamicRoutingDataSource.addDataSource(name, dataSource);
    }

    /**
     * 不建议手动调用
     * 切换数据源,调用过后使用完毕后请务必调用一次cleanDataSource()方法。
     *
     * @param name 名称
     * */
    public void changeDataSource(String name){
        DynamicDataSourceContextHolder.push(name);
    }

    /**
     * 清空数据源
     *
     * @param name 名称
     * */
    public void cleanDataSource(String name) {
        DynamicDataSourceContextHolder.poll();
    }

     /**
     * 清空所有数据源
     *
     * */
    public void cleanDataSource() {
        DynamicDataSourceContextHolder.clear();
    }

    /**
     * 修改数据源
     *
     * @param name 名称
     * @param username 账户
     * @param password 密码
     * @param url 连接
     * @param driver 驱动
     * */
    public void updateDataSource(String name, String username, String password, String url, String driver) {
        removeDataSource(name);
        createDataSource(name, username, password, url, driver);
    }

    /**
     * 删除数据源
     *
     * @param name 名称
     * */
    public void removeDataSource(String name) {
        DynamicRoutingDataSource dynamicRoutingDataSource = (DynamicRoutingDataSource) dataSource;
        dynamicRoutingDataSource.removeDataSource(name);
    }

    /**
     * 数据源列表
     * */
    public Map<String, DataSource> getDataSources() {
        DynamicRoutingDataSource dynamicRoutingDataSource = (DynamicRoutingDataSource) dataSource;
        return dynamicRoutingDataSource.getDataSources();
    }

    /**
     * 获取数据源
     *
     * @param name 名称
     * */
    public DataSource getDataSource(String name) {
        DynamicRoutingDataSource dynamicRoutingDataSource = (DynamicRoutingDataSource) dataSource;
        return dynamicRoutingDataSource.getDataSource(name);
    }

    // init dataSource
    @PostConstruct
    public void loadDataSource() {
        // 这里是伪码,正常情况是从数据库中获取数据库连接信息
        List<SysDataSource> sysDataSources = sysDataSourceService.list();
        // 循环调用添加方法即可
        sysDataSources.forEach(sysDataSource -> {
            createDataSource(
                    sysDataSource.getName(),
                    sysDataSource.getUsername(),
                    sysDataSource.getPassword(),
                    sysDataSource.getUrl(),
                    sysDataSource.getDriver());
        });
    }
}

PS: 这种编程运行时操作数据源已经有点十分方便了,核心就是这个类DefaultDataSourceCreator,如果想深入了解可以看一看源码,可以以此种模式操作多租户的模式,根据不同用户连接不同的数据源。

使用MyBatis插件读写分离

添加配置

@Configuration
public class CoreConfig {
    @Bean
    public ConfigurationCustomizer mybatisConfigurationCustomizer() {
        return configuration -> configuration.addInterceptor(new MasterSlaveAutoRoutingPlugin());
    }
}

支持MybatisPlus。及MyBatis.
PS:主从数据源配置应为

spring:
  datasource:
    dynamic:
      primary: master
      datasource:
        # 主库
        master:
        # 从库1
        slave_1:
        # 从库2
        slave_2:

slave_1,slave_2这样表示这两个数据源为一组,注解切换或者手动切换是数据源名应为slave,而不是slave_1,

切换数据源失败

使用了spring的事务,则切换数据源会有几率会失效,或者百分百失效

开发者原文:

原因: spring开启事务后会维护一个ConnectionHolder,保证在整个事务下,都是用同一个数据库连接。请检查整个调用链路涉及的类的方法和类本身还有继承的抽象类上是否有@Transactional注解。

方法内部调用

查看以下示例 回答 外部调用 userservice.test1() 能在执行到 test2() 切换到second数据源吗?

public UserService {

    @DS("first")
    public void test1() {
        // do something
         test2();
    }

    @DS("second")
    public void test2() {
        // do something
    }
}

答案:!!!不能不能不能!!!! 数据源核心原理是基于aop代理实现切换,内部方法调用不会使用aop。

解决方法:

把test2()方法提到另外一个service,单独调用。

使用了Shiro

建议不用,可以使用sa-token来代替。

事务控制

上面说到了使用spring的事务,会导致切换数据源失败。框架开发者也了解这个情况,写了一个本地的事务工具类LocalTxUtil来解决此问题

public final class LocalTxUtil {

    /**
     * 手动开启事务
     */
    public static void startTransaction() {
        if (!StringUtils.isEmpty(TransactionContext.getXID())) {
            log.debug("dynamic-datasource exist local tx [{}]", TransactionContext.getXID());
        } else {
            String xid = UUID.randomUUID().toString();
            TransactionContext.bind(xid);
            log.debug("dynamic-datasource start local tx [{}]", xid);
        }
    }

    /**
     * 手动提交事务
     */
    public static void commit() {
        ConnectionFactory.notify(true);
        log.debug("dynamic-datasource commit local tx [{}]", TransactionContext.getXID());
        TransactionContext.remove();
    }

    /**
     * 手动回滚事务
     */
    public static void rollback() {
        ConnectionFactory.notify(false);
        log.debug("dynamic-datasource rollback local tx [{}]", TransactionContext.getXID());
        TransactionContext.remove();
    }
}

后话

在如今各种持久层框架中,我们越来越感知不到数据源的存在,好处是,配置简单了,坏处就是对数据源的处理时就有些麻烦。在古老的JDBC中,其实没有这个问题,万物queryForList(),换个数据源继续queryForList()。这个框架理论上支持任何持久层,因为人根本没把目光放在持久层上,专注于数据源,它只操作数据源的。

在springboot2.6.x的版本的不允许依赖循环,我属实不能理解。我不理解,

封面

cosplay原神 珊瑚宫心海


动态数据源,多数据源,读写分离
https://wangijun.com/2022/03/11/java-11/
作者
无良芳
发布于
2022年3月11日
许可协议