动态数据源的那些事

      没事刷博客,看到动态数据源,发现网上的代码啦,配置啦,千篇一律,你抄我来我抄你,没啥意义,网上开源的轮子也有dynamic-datasource-spring-boot-starter,不过我看了下他那边的源码,不支持spring事务,这个我个人还是觉得比较不喜欢的,而且在他自己实现的事务功能里面,貌似对事务嵌套也没做什么处理,它DSTransactional注解标记的方法最后直接就提交事务了。
      整个项目将使用springboot + mybatis + mysql,简单点配置也少点,代码地址也在最后放出来了。
      PS:前情提示,提前复习下ThreadLocal以及aop的用法,以及dynamic-datasource-spring-boot-starter为什么要使用栈的数据结构来存储事务内的所有连接,mybatis的一级缓存带来的问题。
      spring其实已经提供了一个很好的数据源,帮我们实现运行时动态切换数据源,它就是AbstractRoutingDataSource
动态数据源的那些事_第1张图片
简单来说,AbstractRoutingDataSource内部维护了一个默认数据源以及一个map(value为DataSource),它的getConnection方法,在运行时只要我们实现determineCurrentLookupKey方法,返回一个key,它就会去map中根据key找到对应的DataSource(找不到则使用默认数据源),从而实现动态切换的功能。
      如果所有获取connection的方式,都是直接从DataSource来的话,此时我们已经完美实现了动态数据源的功能,而且并没有和orm框架强绑定,你以前是啥配置,现在依旧啥配置,只是需要你注入的数据源是AbstractRoutingDataSource即可。不过,这是不是意味着我们一劳永逸了?
      不管你是否自己研究过spring事务源码,最起码我们都知道一个事实:一个事务内会复用connection。究其原因,spring事务aop在开启事务时,会直接获取connection,组装成ConnectionHolder,然后通过TransactionSynchronizationManager.bindResource(DataSource,ConnectionHolder),在其他需要获取connection的地方,则是优先通过TransactionSynchronizationManager.getResource(dataSource)去获取connection,这就导致了我们的AbstractRoutingDataSource,在一个事务内只会触发一次getConnection,从而导致数据源切换失败!
      这时候有意思的来了,如果存在事务的情况下,即使会复用connection,但是我们让这个connection也具有动态能力,是不是就解决了事务下面切换数据源的问题?
       直接上核心改写的类

package com.routing.datasource;

import com.routing.constant.DataSourceKeyConstant;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;

import javax.sql.DataSource;
import java.lang.reflect.Constructor;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Map;

/**
 * 动态数据源
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    private Map targetDataSource ;


    public DynamicDataSource(DataSource defaultDataSource, Map targetDataSource){
        setDefaultTargetDataSource(defaultDataSource);
        setTargetDataSources(targetDataSource);
        this.targetDataSource = targetDataSource;
    }

    public Map getTargetDataSource() {
        return targetDataSource;
    }

    /**
     * 获取真实的物理数据源信息
     * @return
     */
    public DataSource getPhysicsDataSource(){
        return determineTargetDataSource();
    }

    @Override
    public Connection getConnection() throws SQLException {
        Connection connection = super.getConnection();
        return getConnectionProxy(connection);
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        Connection connection = super.getConnection(username, password);
        return getConnectionProxy(connection);
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceHolder.getDbKey();
    }

    /**
     * 生成代理对象,connection存在事务的情况下,复用connection
     * @return
     */
    public Connection getConnectionProxy(Connection target){

        boolean existingTransaction = DataSourceHolder.isExistingTransaction();
        if(existingTransaction){
            Object dbKey = DataSourceHolder.getDbKey();
            if(dbKey == null){
                dbKey = DataSourceKeyConstant.PRIMARY;
            }
            ConnectionContext.initStack(dbKey,target);
        }
        // 生成代理类
        Class[] interfaces = {Connection.class};
        Connection connection = (Connection) Proxy.newProxyInstance(ClassUtils.getDefaultClassLoader(),interfaces,new DynamicConnectionProxy(target));
        return connection;
    }


}

在获取到的connection上,我们使用jdk代理的方式,如果存在事务,代理类DynamicConnectionProxy会动态真实物理库连接,然后通过反射去执行真实物理库连接的方法(一个事务内一个库的连接重复使用)
然后看看jdk代理类的相关改写部分

package com.routing.datasource;

import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.sql.Connection;

/**
 * 动态数据源
 */
@Slf4j
public class DynamicConnectionProxy implements InvocationHandler {

    // 默认连接
    private Connection target;

    // 是否已提交
    private boolean commit;

    // 是否已回滚
    private boolean rollback;

    public DynamicConnectionProxy(Connection target){
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        if("toString".equals(methodName)){
            return this.toString();
        }
        // 重写提交和回滚方法
        if("commit".equals(methodName)){
            log.info("connection 动态代理 commit方法....");
            ConnectionContext.commitAllConnectionInTransaction();
            commit = true;
            return null;
        }
        if("rollback".equals(methodName)){
            log.info("connection 动态代理 rollback方法....");
            ConnectionContext.rollbackAllConnectionInTransaction();
            rollback = true;
            return null;
        }
        if("close".equals(methodName)){
            // 当前连接已被提交或者回滚
            if(commit || rollback){
                ConnectionContext.removeAllConnectionInTransaction(true);
                DataSourceHolder.cleanDbKey();
                return null;
            }
        }
        // 判断是否存在事务,如果存在事务,则提供动态切换connection的功能
        if(DataSourceHolder.isExistingTransaction()){
            Connection connection = ConnectionContext.getPhysicsConnection(target);
            return method.invoke(connection,args);
        }
        return method.invoke(target,args);
    }
}

代理类中维护了一个target,主要是处理没有事物的情况,这个target本就是真实物理库的连接,此时不需要connection有动态能力,全部由target直接执行即可;当存在事务的情况下,才需要我们提供动态能力去解决这个问题。
这两个类已经帮我们解决了绝大部分动态数据源的问题,其他就是些aop以及ThreadLocal的应用了,就不丢出来了。
整个代码在https://codeup.aliyun.com/620...,有需要的人自己去拉吧( QQ 1767028198)

你可能感兴趣的:(动态数据源的那些事)