MultiDatasource源码解析

1. AbstractRoutingDataSource

Spring提供了数据源切换抽线实现--AbstractRoutingDatasource类,通过继承该类可以实现动态数据源切换。

AbstractRoutingDataSource的多数据源动态切换的核心逻辑是:在程序运行时,通过spring的InitializingBean接口实现的afterPropertiesSet(),将数据源以key-value形式织入到内存中,然后通过AbstractRoutingDataSource实现的determineCurrentLookupKey()方法动态地获取数据源key,灵活的进行数据源切换。

实现逻辑:

  • 把配置的多个数据源会放在AbstractRoutingDataSource的 targetDataSources(所有数据源)和defaultTargetDataSource(默认的数据源)中,然后通过afterPropertiesSet()方法将数据源分别进行复制到resolvedDataSources和resolvedDefaultDataSource中。
  • 定义MultiDataSource类继承抽象类AbstractRoutingDataSource,并实现了determineCurrentLookupKey()方法,动态获取不同数据源的key(一般是通过ThreadLocal处理),为切换数据源做准备。
  • 最后AbstractRoutingDataSource会根据内存的数据源信息以及动态key,动态地获取具体数据源,并且通过调用底层datasource.getConnection()方法从数据库连接池中获取某一个连接(具体Connection如何得到不用关心),有了连接和数据源后,datasource底层会自动实现sql的crud。

2.Multi-datasource框架多数据源切换实现逻辑

基于Spring的AbstractRoutingDataSource,我们只需要

  • 将默认数据源以及所有需要动态切换的数据源添加到AbstractRoutingDataSource类的

defaultTargetDataSource以及targetDataSources,格式为Map<数据源key,数据源>

  • 实现AbstractRoutingDataSource的determineCurrentLookupKey()方法,动态获取数据源key

由于事务切换肯定需要提供注解(定为@MS)做到业务隔离的,故需要借助AOP切面功能,所以底层框架实现伪代码如下:

①:从配置文件中读取所有数据源配置,并封装为具体数据源对象,添加到defaultTargetDataSource以及targetDataSources中.

源码如下:

    @PostConstruct

    public void init() {

        setPrimary(multiDataSourceProperties.getPrimary());

        // 添加并分组数据源

        Map<String, DataSource> dataSources = new HashMap<>(16);

        // 从配置文件中添加数据源(也提供了从其他地方添加数据源的抽象类)

        for (MultiDataSourceProvider provider : providers) {

            dataSources.putAll(provider.loadDataSources());

        }

        Map<Object, Object> targetDataSources = new HashMap<>(dataSources);

        // 添加到dataSourceMap中

        for (Map.Entry<String, DataSource> dsItem : dataSources.entrySet()) {

            addDataSource(dsItem.getKey(), dsItem.getValue());

        }

        // 检测默认数据源是否设置

        if (dataSourceMap.containsKey(primary)) {

            log.info("dynamic-datasource initial loaded [{}] datasource,primary datasource named [{}]", dataSources.size(), primary);

        } else {

            log.warn("dynamic-datasource initial loaded [{}] datasource,Please add your primary datasource or check your configuration", dataSources.size());

        }

        super.setDefaultTargetDataSource(dataSourceMap.get(primary));

        super.setTargetDataSources(targetDataSources);

    }

由于数据源加载方式有多种,包括但不限于yml、文件等,所以在这里提供了MultiDataSourceProvider接口的loadDataSources()方法,同时将公共创建方法通过模板模式AbstractDataSourceProvider提取出来,最好将所有数据源加载的方式添加进来,以yml加载方式为例:

	@Slf4j
	@AllArgsConstructor
	public class YmlMultiDataSourceProvider extends AbstractDataSourceProvider {
	
	    /**
	     * 所有数据源
	     */
	    private final Map<String, DataSourceProperty> dataSourcePropertiesMap;
	
	    /** 创建key为数据源名称,value为具体数据源的map信息
	     *
	     * @return
	     */
	    @Override
	    public Map<String, DataSource> loadDataSources() {
	        return createDataSourceMap(dataSourcePropertiesMap);
	    }
	}
	
	@Slf4j
	public abstract class AbstractDataSourceProvider implements MultiDataSourceProvider {
	
	    @Autowired
	    private DefaultDataSourceCreator defaultDataSourceCreator;
	
	    protected Map<String, DataSource> createDataSourceMap(
	            Map<String, DataSourceProperty> dataSourcePropertiesMap) {
	        Map<String, DataSource> dataSourceMap = new HashMap<>(dataSourcePropertiesMap.size() * 2);
	        // 获取yml配置的所有数据源,并创建
	      for (Map.Entry<String, DataSourceProperty> item : dataSourcePropertiesMap.entrySet()) {
	            String dsName = item.getKey();
	            DataSourceProperty dataSourceProperty = item.getValue();
	            String poolName = dataSourceProperty.getPoolName();
	            if (poolName == null || "".equals(poolName)) {
	                poolName = dsName;
	            }
	            dataSourceProperty.setPoolName(poolName);
	            dataSourceMap.put(dsName, defaultDataSourceCreator.createDataSource(dataSourceProperty));
	        }
	        return dataSourceMap;
	    }
	}
	

同时因为数据源包括但不限于druid、jndi、c3p0,所以需要提供创建数据源的接口,供适配创建不同数据源。

public interface DataSourceCreator {



    /**

     * 通过属性创建数据源

     *

     * @param dataSourceProperty 数据源属性

     * @return 被创建的数据源

     */

    DataSource createDataSource(DataSourceProperty dataSourceProperty);



    /**

     * 当前创建器是否支持根据此属性创建

     *

     * @param dataSourceProperty 数据源属性

     * @return 是否支持

     */

    boolean support(DataSourceProperty dataSourceProperty);

}





public class DruidDataSourceCreator extends AbstractDataSourceCreator implements DataSourceCreator, InitializingBean {



    @Override

    public boolean support(DataSourceProperty dataSourceProperty) {

        Class<? extends DataSource> type = dataSourceProperty.getType();

        // 通过yml文件中配置的type决定是使用哪个数据源,没配置默认使用druid数据源

        return type == null || "com.alibaba.druid.pool.DruidDataSource".equals(type.getName());

    }

    @Override

Datasource createDataSource(DataSourceProperty dataSourceProperty) {

 // 创建druid数据源

}



}

②:借助AOP切面,编写@MS注解,动态获取数据源

	@Aspect
	public class MultiDatasourceAspect {
	    @Autowired
	    private MultiDataSourceProperties multiDataSourceProperties;
	
	    private static final Logger LOG = LogManager.getLogger(MultiDatasourceAspect.class);
	
	    @Pointcut("@annotation(com.jenkin.multi.datasource.annotation.MS)")
	    public void datasourcePointCut() {
	
	    }
	
	    @Around("datasourcePointCut()")
	    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
	        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
	        Method method = signature.getMethod();
	        String msKey = null;
	        try {
	            if (method.isAnnotationPresent(MS.class)) {
	                MS ms = method.getAnnotation(MS.class);
	                // 获取当前方法进行的操作
	                msKey = ms.value();
	                if (StringUtils.isEmpty(msKey)) {
	                    msKey = multiDataSourceProperties.getPrimary();
	                }
	            }
	            // 放入当前线程中
	            MultiDataSourceContextHolder.push(msKey);
	            return joinPoint.proceed();
	        } catch (Exception e) {
	            LOG.error("Multi datasource exception for ",e);
	            throw e;
	        } finally {
	            MultiDataSourceContextHolder.poll();
	        }
	
	    }
	}

通过ThreadLocal将数据源的key放入线程上线文中,注意当未在方法上使用@MS注解时,是不会进入该切面逻辑的,也就是说程序需要判断ThreadLocal获取到的key为空的情况。

③:实现AbstractRoutingDataSource抽象类的determineCurrentLookupKey()方法,动态获取key

@Slf4j
@Component
public class MultiRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        String msKey = MultiDataSourceContextHolder.peek();
        // 如果不添加注解时,就使用默认master数据源
        if (StringUtils.isEmpty(msKey)) {
            msKey = getPrimary();
        }
        return msKey;
    }
    ……
}

这样就可以借助AbstractRoutingDataSource进行动态数据源的切换了。

3. 实现分布式事务(多数据源)

  不同于多系统间的分布式事务,多数据源导致的分布式事务问题就无需引用springcloud的这种seata重量级框架来实现了。

对于传统单库事务的回滚操作,一般是通过try{ sql}  catch {记录状态} finally {如果status异常了则获取Connection回滚,如果status正常则获取Connection执行sql}。

  对于多库事务,也可以类似处理,所以重点在于如何获取所有需要执行sql库的所有connection

①:记录\获取所有Connection

进行数据库crud操作时,比如orderMapper.insert(order),就会执行AbstractRoutingDataSource类中的getConnection(),它是实现与Datasource接口,所以可以重写该方法,用以记录所有数据源的连接。

可以使用代理模式,创建Connection代理,用以处理Connection的所有操作

	@Slf4j
	public class ConnectionProxy implements Connection {
	
	    private Connection connection;
	
	    private String ds;
	
	    public ConnectionProxy(Connection connection, String ds) {
	        this.connection = connection;
	        this.ds = ds;
	    }
	
	    public void notify(Boolean commit) throws SQLException {
	        try {
	            if (commit) {
	                connection.commit();
	            } else {
	                connection.rollback();
	            }
	        } catch (SQLException e) {
	            log.error(e.getLocalizedMessage(), e);
	            throw e;
	        } finally {
	            try {
	                connection.close();
	            } catch (SQLException e2) {
	                log.error("db connection close failed", e2);
	            }
	        }
	    }
	
	    @Override
	    public void commit() throws SQLException {
	         //connection.commit();
	    }
	
	    @Override
	    public void rollback() throws SQLException {
	         //connection.rollback();
	    }
	
	    @Override
	    public void close() throws SQLException {
	         //connection.close();
	    }
	……
	}

有这个代理就可以通过notify方法对数据库crud操作进行commit或者rollback,从而到达事务管理。

对于Connection的创建和获取,可以通过工厂模式构建,通过putConnection()方法,将连接存放到 CONNECTION_HOLDER中;通过getConnection(String ds)方法获取具体数据源的连接。

	public class ConnectionFactory {
	
	    private static final ThreadLocal<Map<String, ConnectionProxy>> CONNECTION_HOLDER =
	            new ThreadLocal<Map<String, ConnectionProxy>>() {
	                @Override
	                protected Map<String, ConnectionProxy> initialValue() {
	                    return new ConcurrentHashMap<>(8);
	                }
	            };
	
	    public static void putConnection(String ds, ConnectionProxy connection) {
	        Map<String, ConnectionProxy> concurrentHashMap = CONNECTION_HOLDER.get();
	        if (!concurrentHashMap.containsKey(ds)) {
	            try {
	                connection.setAutoCommit(false);
	            } catch (SQLException e) {
	                e.printStackTrace();
	            }
	            concurrentHashMap.put(ds, connection);
	        }
	    }
	
	    public static ConnectionProxy getConnection(String ds) {
	        return CONNECTION_HOLDER.get().get(ds);
	    }
	
	    public static void notify(Boolean state) throws Exception {
	        Exception exception = null;
	        try {
	            Map<String, ConnectionProxy> concurrentHashMap = CONNECTION_HOLDER.get();
	            for (ConnectionProxy connectionProxy : concurrentHashMap.values()) {
	                try {
	                    connectionProxy.notify(state);
	                } catch (SQLException e) {
	                    exception = e;
	                }
	            }
	        } finally {
	            CONNECTION_HOLDER.remove();
	            if (exception != null) {
	                throw exception;
	            }
	        }
	    }
	
	}

有了以上准备,就可以实现AbstractRoutingDataSource类的getConnection方法,将Connection记录

	@Slf4j
	@Component
	public class MultiRoutingDataSource extends AbstractRoutingDataSource {
	   @Override
	    public Connection getConnection() throws SQLException {
	        String xid = TransactionContext.getXID();
	        if (StringUtils.isEmpty(xid)) {
	            // datasource.getConnection()底层通过从数据库连接池中获取某个Connection,具体哪一个不需要关关心
	            return determineDataSource().getConnection();
	        } else {
	            String ds = MultiDataSourceContextHolder.peek();
	            ds = StringUtils.isEmpty(ds) ? getPrimary() : ds;
	            ConnectionProxy connection = ConnectionFactory.getConnection(ds);
	            return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection()) : connection;
	        }
	    }
	
	    @Override
	    public Connection getConnection(String username, String password) throws SQLException {
	        String xid = TransactionContext.getXID();
	        if (StringUtils.isEmpty(xid)) {
	            return determineDataSource().getConnection(username, password);
	        } else {
	            String ds = MultiDataSourceContextHolder.peek();
	            ds = StringUtils.isEmpty(ds) ? getPrimary() : ds;
	            ConnectionProxy connection = ConnectionFactory.getConnection(ds);
	            return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection(username, password))
	                    : connection;
	        }
	    }
	…...
	}
	

这样在多数据源切换时,就会将所有Connection加入到CONNECTION_HOLDER中。

②:创建@MSTransaction注解,添加AOP切面

	@Aspect
	public class MultiDatasourceTransactionAspect {
	    @Autowired
	    private MultiDataSourceProperties multiDataSourceProperties;
	
	    private static final Logger LOG = LogManager.getLogger(MultiDatasourceTransactionAspect.class);
	
	    @Pointcut("@annotation(com.jenkin.multi.datasource.annotation.MSTransaction)")
	    public void multiDatasourceTransactionPointCut() {
	
	    }
	
	    @Around("multiDatasourceTransactionPointCut()")
	    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
	        // 在分布式事务或多数据源事务的场景中,可能存在已经启动的外部事务,
	        // 比如由某个全局事务管理器(如分布式事务框架或者 Spring 的事务管理器)创建的事务。此时,就不需要再次启动新的本地事务。
	        if (!StringUtils.isEmpty(TransactionContext.getXID())) {
	            return joinPoint.proceed();
	        }
	        boolean state = true;
	        Object o;
	        LocalTxUtil.startTransaction();
	        try {
	            o = joinPoint.proceed();
	        } catch (Exception e) {
	            state = false;
	            throw e;
	        } finally {
	            if (state) {
	                LocalTxUtil.commit();
	            } else {
	                LocalTxUtil.rollback();
	            }
	        }
	        return o;
	    }
	}
	@Slf4j
	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() throws Exception {
	        try {
	            ConnectionFactory.notify(true);
	        } finally {
	            log.debug("dynamic-datasource commit local tx [{}]", TransactionContext.getXID());
	            TransactionContext.remove();
	        }
	    }
	
	    /**
	     * 手动回滚事务
	     */
	    public static void rollback() throws Exception {
	        try {
	            ConnectionFactory.notify(false);
	        } finally {
	            log.debug("dynamic-datasource rollback local tx [{}]", TransactionContext.getXID());
	            TransactionContext.remove();
	        }
	    }
	}
以上就是MultiDataSource的源码解析,感兴趣的小伙伴麻烦帮去gitee上star以下呀~~
源码地址:https://gitee.com/jenkin1011/multi_datasource.git
### 多数据源配置的概念 多数据源配置是指在一个应用程序中能够访问多个数据库实例的能力。这种能力对于大型应用尤其重要,因为不同的业务模块可能依赖于不同的数据库系统或同一系统的不同实例。通过合理设计的数据源切换机制,可以有效地隔离各个子系统的数据操作逻辑。 ### Spring Boot 中基于 MyBatis 的多数据源实现方案 #### 1. 配置 `application.yml` 为了支持多数据源,在`application.yml`文件里需定义至少两个独立的数据源属性集。每个数据源都应指定其驱动程序名称、URL以及用户名密码等必要参数[^1]: ```yaml spring: datasource: master: url: jdbc:mysql://localhost:3306/db_master?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver slave: url: jdbc:mysql://localhost:3307/db_slave?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver ``` #### 2. 创建自定义的 DataSourceConfig 类 接下来创建一个名为`DataSourceConfig.java`的新类用于注册这些数据源并将其暴露给Spring容器管理。此过程涉及到构建具体的`DataSource`对象,并设置相应的事务管理和JDBC模板等功能组件[^4]: ```java @Configuration public class DataSourceConfig { @Bean(name = "masterDataSource") @ConfigurationProperties(prefix = "spring.datasource.master") public DataSource masterDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "slaveDataSource") @ConfigurationProperties(prefix = "spring.datasource.slave") public DataSource slaveDataSource() { return DataSourceBuilder.create().build(); } } ``` #### 3. 动态选择目标数据源 为了让应用程序能够在运行期间灵活地决定使用哪个具体的数据源执行SQL语句,通常会采用抽象路由数据源(AbstractRoutingDataSource)的方式。这种方式允许开发者编写简单的规则来判断当前请求应该关联哪一个物理上的数据源。 ```java public class DynamicDataSource extends AbstractRoutingDataSource { private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>(); public void setMasterTargetDataSource(DataSource masterTargetDataSource) { super.setDefaultTargetDataSource(masterTargetDataSource); } protected Object determineCurrentLookupKey() { return getContextHolder(); } public static void setContextHolder(String dataSourceType){ CONTEXT_HOLDER.set(dataSourceType); } public static String getContextHolder(){ return CONTEXT_HOLDER.get(); } public static void clearContextHolder(){ CONTEXT_HOLDER.remove(); } } ``` #### 4. 注册动态数据源至Spring上下文中 最后一步是在Spring环境中正式引入上述定制化的DynamicDataSource作为默认使用的数据源类型,并确保它能正确识别之前声明过的所有实际存在的数据源实体。 ```java @Bean(name="dynamicDataSource") public DynamicDataSource dynamicDataSource(@Qualifier("masterDataSource") DataSource master, @Qualifier("slaveDataSource") DataSource slave) { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("master", master); targetDataSources.put("slave", slave); DynamicDataSource dynamicDataSource = new DynamicDataSource(); dynamicDataSource.setTargetDataSources(targetDataSources); // 添加可选的目标数据源 dynamicDataSource.setDefaultTargetDataSource(master); // 设置默认的数据源 return dynamicDataSource; } // 将MyBatis所需的SqlSessionFactory绑定到动态数据源上 @Bean public SqlSessionFactory sqlSessionFactory(DynamicDataSource dynamicDataSource) throws Exception { SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); sessionFactory.setDataSource(dynamicDataSource); PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); sessionFactory.setMapperLocations(resolver.getResources("classpath*:mapper/*.xml")); return sessionFactory.getObject(); } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值