多数据源管理——@DS和@DSTranscation使用介绍和原理分析

前言

在日常的后端开发过程中,由于业务的变更或者系统的切换,我们经常会遇到需要配置多个数据源的情况。为了解决数据源的动态切换问题,可以引入@DS注解。@DS注解是一个用于切换数据源的注解,可以标注在方法或者类上。在使用@DS注解时,我们可以指定要使用的数据源的名称,也可以使用SpEL表达式来动态决定使用哪个数据源。通过使用@DS注解,我们可以在不修改代码的情况下轻松切换不同的数据源。这个注解的引入可以极大地简化了数据源切换的操作,提高了开发效率和灵活性。

@DS注解

使用介绍

1、引入jar包
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.4.1</version>
</dependency>
2、配置文件配置多数据源

注意:此处采用yaml格式进行配置,读者可根据实际情况采用对应格式配置数据源信息

spring:
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      datasource:
        master:
          url:
          username:
          password:
          driver-class-name:
        slave:
          url:
          username:
          password:
          driver-class-name:
3、mapper层或者dao层配置@DS指定数据源

注意1:如果你的dao层继承了mybatis-plus提供的ServiceImpl类,即dao层可以不经过显式mapper层直接调用sql,则在dao层上加@DS,否则,建议加在mapper层
注意2: 该注解可以用在类上,也可以用在方法上

// ServiceImpl是Mybatis-Plus框架中扩展的Service接口的实现类。
// 该包中包含了Mybatis-Plus的扩展Service接口的实现类,
// 这些实现类提供了一些常用的数据库操作方法,如新增、删除、修改、查询等。通过继承这些实现类,
// 可以快速地编写Service层的代码,减少重复的工作量。
@Component
@DS("slave")  // 不配置该注解,默认走master数据源
public class UserDao extends ServiceImpl<UserMapper, User> {
	@Autowired
    private UserMapper userMapper;
}
@Mapper
public interface UserMapper extends BaseMapper<User>{
}

原理分析

jar包的结构

在这里插入图片描述

底层实现核心类是com.baomidou.dynamic.datasource.DynamicRoutingDataSource

类属性分析

// 分组识别符
private static final String UNDERLINE = "_";
// 用于存储所有的数据源,以数据源名称作为键
private final Map<String, DataSource> dataSourceMap = new ConcurrentHashMap<>();

// 用于存储分组数据源,以分组名称作为键
private final Map<String, GroupDataSource> groupDataSources = new ConcurrentHashMap<>();

// 一个DynamicDataSourceProvider类型的列表,用于提供动态数据源的实现
@Autowired
private List<DynamicDataSourceProvider> providers;

// 动态数据源的策略类。默认为LoadBalanceDynamicDataSourceStrategy
@Setter
private Class<? extends DynamicDataSourceStrategy> strategy = LoadBalanceDynamicDataSourceStrategy.class;

// 主数据源的标识。默认为"master"
@Setter
private String primary = "master";

// 表示是否严格检查数据源的存在性。默认为false
@Setter
private Boolean strict = false;

// 表示是否启用P6Spy数据源。默认为false
@Setter
private Boolean p6spy = false;

// 表示是否启用Seata数据源代理。默认为false
@Setter
private Boolean seata = false;
1.项目初始化时,获取所有数据源信息——DynamicRoutingDataSource.addDataSource
  1. 检查开启数据源配置是否开启——p6spy、seata
  2. 添加并分组数据源
  3. 检测默认数据源是否配置
@Override
public void afterPropertiesSet() throws Exception {
	// 检查开启了配置但没有相关依赖
	checkEnv();
	// 添加并分组数据源
	Map<String, DataSource> dataSources = new HashMap<>();
	for (DynamicDataSourceProvider provider : providers) {
	    dataSources.putAll(provider.loadDataSources());
	}
	for (Map.Entry<String, DataSource> dsItem : dataSources.entrySet()) {
	    addDataSource(dsItem.getKey(), dsItem.getValue());
	}
	// 检测默认数据源是否设置
	if (groupDataSources.containsKey(primary)) {
	    log.info("dynamic-datasource initial loaded [{}] datasource,primary group datasource named [{}]", dataSources.size(), primary);
	} else 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());
	}
}
  1. 将数据源加入到全部数据源map中
  2. 将数据源添加到分组中
  3. 关闭数据源
public synchronized void addDataSource(String ds, DataSource dataSource) {
    DataSource oldDataSource = dataSourceMap.put(ds, dataSource);
    // 新数据源添加到分组
    this.addGroupDataSource(ds, dataSource);
    // 关闭老的数据源
    if (oldDataSource != null) {
        closeDataSource(ds, oldDataSource);
    }
    log.info("dynamic-datasource - add a datasource named [{}] success", ds);
}
  1. 校验数据源名称是否包含分组标识符
  2. 按分组标识符进行分割,以分组标识符前的字符串作为分组名
  3. 将数据源添加到分组数据源中
private void addGroupDataSource(String ds, DataSource dataSource) {
    if (ds.contains(UNDERLINE)) {
        String group = ds.split(UNDERLINE)[0];
        GroupDataSource groupDataSource = groupDataSources.get(group);
        if (groupDataSource == null) {
            try {
                groupDataSource = new GroupDataSource(group, strategy.getDeclaredConstructor().newInstance());
                groupDataSources.put(group, groupDataSource);
            } catch (Exception e) {
                throw new RuntimeException("dynamic-datasource - add the datasource named " + ds + " error", e);
            }
        }
        groupDataSource.addDatasource(ds, dataSource);
    }
}
2.进行数据库操作,执行对应方法时,会被 DynamicDataSourceAnnotationInterceptor拦截器拦截
  1. 该拦截器继承了MethodInterceptor,会对所有方法进行拦截
  2. 拦截后,invoke方法中执行determineDatasource方法,扫描加了@DS注解的类或者方法,
  3. 调用DynamicDataSourceContextHolder.poll方法。

在这里插入图片描述
在这里插入图片描述

3.DynamicDataSourceContextHolder.poll方法将当前线程的数据源名加到对应的ThreadLocal线程本地中
public final class DynamicDataSourceContextHolder {

    /**
     * 为什么要用链表存储(准确的是栈)
     * <pre>
     * 为了支持嵌套切换,如ABC三个service都是不同的数据源
     * 其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换,形成了链。
     * 传统的只设置当前线程的方式不能满足此业务需求,必须使用栈,后进先出。
     * </pre>
     */
    private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedThreadLocal<Deque<String>>("dynamic-datasource") {
@Override
        protected Deque<String> initialValue() {
            return new ArrayDeque<>();
        }
    };
/**
     * 设置当前线程数据源
     * <p>
     * 如非必要不要手动调用,调用后确保最终清除
     * </p>
     *
     * @param ds 数据源名称
     */
    public static String push(String ds) {
        String dataSourceStr = StringUtils.isEmpty(ds) ? "" : ds;
        LOOKUP_KEY_HOLDER.get().push(dataSourceStr);
        return dataSourceStr;
    }
}

到此线程中threadLocal就保存了需要切换的数据源名称了

4、数据操作完成后,方法返回第二步中的拦截器,执行DynamicDataSourceContextHolder.poll();清除掉此次Threadlocal中的数据源,避免影响后续数据操作。

@DSTranscation注解

Spring自带事务@Transactional的实现在一个事务里,只能有一个数据库链接,表现形式即只有第一个@DS注解会生效,之后的数据库操作都采用第一次使用到的数据库连接。
@DSTranscation可以实现动态数据源切换下的事务

使用介绍

同上,一般加在业务层方法上
不能和@Transcation混用

原理分析

在这里插入图片描述

1.核心方法DynamicLocalTransactionAdvisor
  1. 如果有xid,直接反射调用原方法,说明会话已经创建。
  2. 如果没有xid,说明新会话,首先生成xid,绑到上下文上
  3. 执行原方法,如果有异常,修改状态为false
  4. 调用会话的notify方法,处理状态(关键)
  5. 删除会话上下文(关键)
@Slf4j
public class DynamicLocalTransactionAdvisor implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        if (!StringUtils.isEmpty(TransactionContext.getXID())) {
            return methodInvocation.proceed();
        }
        boolean state = true;
        Object o;
        String xid = UUID.randomUUID().toString();
        TransactionContext.bind(xid);
        try {
            o = methodInvocation.proceed();
        } catch (Exception e) {
            state = false;
            throw e;
        } finally {
            ConnectionFactory.notify(state);
            TransactionContext.remove();
        }
        return o;
    }
}
2.事务上下文类:用于记录当前事务的xid
public class TransactionContext {

    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * Gets xid.
     *
     * @return the xid
     */
    public static String getXID() {
        String xid = CONTEXT_HOLDER.get();
        if (!StringUtils.isEmpty(xid)) {
            return xid;
        }
        return null;
    }

    /**
     * Unbind string.
     *
     * @return the string
     */
    public static String unbind(String xid) {
        CONTEXT_HOLDER.remove();
        return xid;
    }

    /**
     * bind string.
     *
     * @return the string
     */
    public static String bind(String xid) {
        CONTEXT_HOLDER.set(xid);
        return xid;
    }

    /**
     * remove
     */
    public static void remove() {
        CONTEXT_HOLDER.remove();
    }

}

3. 会话工厂:用于管理用到的数据源链接
  1. 关闭使用到的数据源的自动提交能力【putConnection】
  2. 将使用到的数据源放到当前线程的threadLocal中【putConnection】
  3. 循环调用了所有数据库连接的notify方法。有一个false就都rollback了【notify】
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<>();
                }
            };

    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) {
        try {
            Map<String, ConnectionProxy> concurrentHashMap = CONNECTION_HOLDER.get();
            for (ConnectionProxy connectionProxy : concurrentHashMap.values()) {
                connectionProxy.notify(state);
            }
        } finally {
            CONNECTION_HOLDER.remove();
        }
    }

}

4.抽象动态数据源的事务处理
  1. 如果获取不到xid,则非DS事务,直接返回数据库连接
  2. 如果获取到xid,则为DS事务
  3. 基于2,如果ConnectionFactory获取不到对应数据库连接,则调用getConnectionProxy方法
  4. 基于3,如果能获取到对应数据库连接,直接返回该连接
  5. 基于2,将新的数据库连接放到ConnectionProxy中
public abstract class AbstractRoutingDataSource extends AbstractDataSource {

    protected abstract DataSource determineDataSource();

    @Override
    public Connection getConnection() throws SQLException {
        String xid = TransactionContext.getXID();
        if (StringUtils.isEmpty(xid)) {
            return determineDataSource().getConnection();
        } else {
            String ds = DynamicDataSourceContextHolder.peek();
            ds = StringUtils.isEmpty(ds) ? "default" : 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 = DynamicDataSourceContextHolder.peek();
            ds = StringUtils.isEmpty(ds) ? "default" : ds;
            ConnectionProxy connection = ConnectionFactory.getConnection(ds);
            return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection(username, password))
                    : connection;
        }
    }

    private Connection getConnectionProxy(String ds, Connection connection) {
        ConnectionProxy connectionProxy = new ConnectionProxy(connection, ds);
        ConnectionFactory.putConnection(ds, connectionProxy);
        return connectionProxy;
    }
   //略
}

参考文档:
@DSTransactional注解原理
多数据源管理:掌握@DS注解的威力

  • 29
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值