前言
在日常的后端开发过程中,由于业务的变更或者系统的切换,我们经常会遇到需要配置多个数据源的情况。为了解决数据源的动态切换问题,可以引入@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
- 检查开启数据源配置是否开启——p6spy、seata
- 添加并分组数据源
- 检测默认数据源是否配置
@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());
}
}
- 将数据源加入到全部数据源map中
- 将数据源添加到分组中
- 关闭数据源
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);
}
- 校验数据源名称是否包含分组标识符
- 按分组标识符进行分割,以分组标识符前的字符串作为分组名
- 将数据源添加到分组数据源中
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拦截器拦截
- 该拦截器继承了MethodInterceptor,会对所有方法进行拦截
- 拦截后,invoke方法中执行determineDatasource方法,扫描加了@DS注解的类或者方法,
- 调用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
- 如果有xid,直接反射调用原方法,说明会话已经创建。
- 如果没有xid,说明新会话,首先生成xid,绑到上下文上
- 执行原方法,如果有异常,修改状态为false
- 调用会话的notify方法,处理状态(关键)
- 删除会话上下文(关键)
@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. 会话工厂:用于管理用到的数据源链接
- 关闭使用到的数据源的自动提交能力【putConnection】
- 将使用到的数据源放到当前线程的threadLocal中【putConnection】
- 循环调用了所有数据库连接的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.抽象动态数据源的事务处理
- 如果获取不到xid,则非DS事务,直接返回数据库连接
- 如果获取到xid,则为DS事务
- 基于2,如果ConnectionFactory获取不到对应数据库连接,则调用getConnectionProxy方法
- 基于3,如果能获取到对应数据库连接,直接返回该连接
- 基于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;
}
//略
}