动态数据源

476 阅读3分钟

解决问题

  1. 多个数据源使用注解切换。
  2. 与连接池的集成
  3. 分布式事务

实现方式

  1. 基于abstractRoutingDataSource做拓展
  2. 不同操作类,固定数据源
  3. 分库分表中间件

使用dynamic-datasource-spring-boot-starter

采用的是方式一的实现方式

大面上的源码流程

  1. 自动装配类com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration。 首先将配置信息加载到DynamicDataSourceProvider中。

    @Bean
    @Order(0)
    public DynamicDataSourceProvider ymlDynamicDataSourceProvider() {
        return new YmlDynamicDataSourceProvider(properties.getDatasource());
    }
  1. 实例化DataSource实现类DynamicRoutingDataSource
        @Bean
        @ConditionalOnMissingBean
        public DataSource dataSource() {
            DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
            dataSource.setPrimary(properties.getPrimary());
            dataSource.setStrict(properties.getStrict());
            dataSource.setStrategy(properties.getStrategy());
            dataSource.setP6spy(properties.getP6spy());
            dataSource.setSeata(properties.getSeata());
            return dataSource;
        }
  1. DynamicRoutingDataSource实现了InitializingBean,在属性注入完成后调用AfterPropertiesSet()方法。主要是将dynamicDataSourceProvider的数据按照分组添加到DynamicRoutingDataSource的所有数据库实例变量Map<String,DataSource> dataSourceMap和分组数据库实例变量Map<String,DataSource> groupDataSourceMap

    // 添加并分组数据源
    Map<String, DataSource> dataSources = new HashMap<>(16);
    for (DynamicDataSourceProvider provider : providers) {
        dataSources.putAll(provider.loadDataSources());
    }
    for (Map.Entry<String, DataSource> dsItem : dataSources.entrySet()) {
        addDataSource(dsItem.getKey(), dsItem.getValue());
    }
  1. com.baomidou.dynamic.datasource.aop.DynamicDataSourceAnnotationInterceptor#invoke编程式切面,解析使用@DS注解的方法,获取注解的value值决定使用哪个数据源。将数据源名称存入DynamicDataSourceContextHolder中。
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        String dsKey = determineDatasourceKey(invocation);
        DynamicDataSourceContextHolder.push(dsKey);
        try {
            return invocation.proceed();
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
    }
  1. mybatis执行SQL语句要调用getConnection获取数据库连接对象com.baomidou.dynamic.datasource.ds.AbstractRoutingDataSource#getConnection()。调用实现类的com.baomidou.dynamic.datasource.DynamicRoutingDataSource#determineDataSource方法。从上下文取出数据源名称,根据名称获得数据源
@Override
public DataSource determineDataSource() {
    String dsKey = DynamicDataSourceContextHolder.peek();
    return getDataSource(dsKey);
}

通过数据源解析器解析要切换到哪个数据源,不是简单的获取注解的value值,要考虑与mybatis适配

         private String determineDatasourceKey(MethodInvocation invocation) {
             String key = dataSourceClassResolver.findKey(invocation.getMethod(), invocation.getThis());
             return key.startsWith(DYNAMIC_PREFIX) ? dsProcessor.determineDatasource(invocation, key) : key;
         }

本地多数据源事务解决方案

在很多数据库相关项目里,connection这个词是有歧义的,可能有的含义包括:事务、会话、数据库连接。 spring自带事务明显默认了一个事务会话就是一个数据库链接这种老思想。从而导致了数据源回滚只能回滚第一个,后面的都失效了。

  1. 注解@DSTransactioonal的增强类com.baomidou.dynamic.datasource.aop.DynamicLocalTransactionInterceptor#invoke生成xid绑定到上下文中
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
    if (!StringUtils.isEmpty(TransactionContext.getXID())) {
        return methodInvocation.proceed();
    }
    boolean state = true;
    Object o;
    LocalTxUtil.startTransaction();
    try {
        o = methodInvocation.proceed();
    } catch (Exception e) {
        state = false;
        throw e;
    } finally {
        if (state) {
            LocalTxUtil.commit();
        } else {
            LocalTxUtil.rollback();
        }
    }
    return o;
}
  1. sql执行完成,没有问题就要提交事务LocalTxUtil.commit();ConnectionFactory是记录了当前会话中所有用到的连接,也就是Connection的代理类ConnectionProxy。notify方法通知每一个当前会话的连接是提交还是回滚。
public static void commit() throws Exception {
    try {
        ConnectionFactory.notify(true);
    } finally {
        log.debug("dynamic-datasource commit local tx [{}]", TransactionContext.getXID());
        TransactionContext.remove();
    }
}
  1. ConnectionProxy是在什么时候加入到连接中的呢,是在getConnection的时候。执行SQL之前会执行getConnection方法,如果service层方法添加了@DSTransactional注解,TransactionContext。getXID不为空,如果上下文连接工厂ConnectionFactory没有这个数据源,就新创建一个ConnectionProxy并添加到当前上下文中。这样这个事务中就包括了所有相关联的连接
@Override
public Connection getConnection() throws SQLException {
    String xid = TransactionContext.getXID();
    if (StringUtils.isEmpty(xid)) {//没有使用注解
        return determineDataSource().getConnection();
    } else {//使用了@DSTransactinal注解
        String ds = DynamicDataSourceContextHolder.peek();
        ds = StringUtils.isEmpty(ds) ? getPrimary() : ds;
        ConnectionProxy connection = ConnectionFactory.getConnection(ds);
        return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection()) : connection;
    }
}

工具类

AnnotatedElementUtiles

org.springframework.core.annotation.AnnotatedElementUtils好像是个获取方法上注解的类,特殊作用目前不理解

获取类的代理对象

 private OrderService self() {
        return (OrderService) AopContext.currentProxy();
    }

参考链接

芋道 Spring Boot 多数据源(读写分离)入门
DSTransactional实现源码分析