目前百度能搜索到的大部分是各自单独实现的aop以及多数据源的管理,使用起来比较复杂,而且做得也相对来说不是很完善,比如是否要考虑嵌套事务这种以及多数据源事务的支持方式,很多博主都没有涉及到,大多是简单的基于spring框架的拦截处理,自己实现不仅有不完善的缺点、可扩展性也是比较差的,后期版本维护升级或者支持更加复杂的功能就需要进行大改动以及全面的测试,风险系数也是蛮高的。mybatis-plus高版本在2021年下半年已经自己实现了在不借助外部协调者seata的情况下通过实现spring分布式事务的相关接口和相关拦截器而最终做到了多数据源事务的统一提交和回滚管理。接下来我将对其使用方式以及实现细则进行详细的说明。
mybatis的yml配置如下
spring:
datasource:
master:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://ip:3306/xx?useAffectedRows=true&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
username: xx
password: xx
type: com.zaxxer.hikari.HikariDataSource
minimum-idle: 5
maximum-pool-size: 15
auto-commit: true
idle-timeout: 30000
pool-name: springHikariCP-master
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1 #连接获取时验证
mybatis-plus的配置如下
spring:
datasource:
dynamic:
strict: false #设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候会抛出异常,不启动则使用默认数据源.
datasource:
sysmaster:
url: jdbc:mysql://ip:3306/xx?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
username: xx
password: xx
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource
hikari:
min-idle: 5
max-pool-size: 15
is-auto-commit: false
idle-timeout: 30000
pool-name: springHikariCP
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1
其中涉及字段的变化,主要由于使用的数据源的properties的配置类的变化
以下是mybatis-plus用到的HikariCpConfig的配置类
com.baomidou
mybatis-plus
3.4.1
com.baomidou
mybatis-plus-boot-starter
3.4.1
其次是service层需要加入多数据源事务的注解 @DSTransactional
对于dao层则加上数据源的注解 @DS("master")
这样子就实现了多数据源的事务支持了,是不是非常简单,主要是这些事情在新版本中都已经被集成到spring框架中去了,旧版本没有集成的则需要自己实现多数据源切换以及事务的回滚提交等操作,相当麻烦,不过本着知其然知其所以然的学习态度,接下来将对mybatis-plus如何实现的多数据源事务进行一个较为详细的说明
3、mybatis-plus如何实现多数据源的事务管理
总共分层两步,首先需要多数据源按照key,value的方式进行装配,其次创建aop代理对象,对多数据源下的事务进行统一提交和回滚的处理,通过阅读源码总结出如下两个流程图。
以下是多数据源装配的过程
以上流程装配的dataSource是ItemDataSource,其结构如下
数据源属性类如下 DynamicDataSourceProperties
以下是两阶段提交模式的代理操作流程
其中多数据源事务处理:首先创建全局事务,然后分别统一对每个连接里的预提交操作进行提交,如果遇到提交失败的,则在当前全局事务中对所有的连接进行回滚操作,核心代码如下:
commit或者rollback的操作会触发每个connectionProxy的threadLocal的处理
以上数据源切换的关键点是需要threadLocal来支撑的,DynamicDataSourceContextHolder是核心基于ThreadLocal的切换数据源工具类(为什么要用链表存储(准确的是栈) 为了支持嵌套切换,如ABC三个service都是不同的数据源 其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换,形成了链。 传统的只设置当前线程的方式不能满足此业务需求,必须使用栈,后进先出。)
其实现如下提供了进出栈的方法以及当前线程数据源的获取方法
private static final ThreadLocal> LOOKUP_KEY_HOLDER = new NamedThreadLocal>("dynamic-datasource") {
@Override
protected Deque initialValue() {
return new ArrayDeque<>();
}
};
private DynamicDataSourceContextHolder() {
}
/**
* 获得当前线程数据源
*
* @return 数据源名称
*/
public static String peek() {
return LOOKUP_KEY_HOLDER.get().peek();
}
/**
* 设置当前线程数据源
*
* 如非必要不要手动调用,调用后确保最终清除
*
*
* @param ds 数据源名称
*/
public static String push(String ds) {
String dataSourceStr = StringUtils.isEmpty(ds) ? "" : ds;
LOOKUP_KEY_HOLDER.get().push(dataSourceStr);
return dataSourceStr;
}
/**
* 清空当前线程数据源
*
* 如果当前线程是连续切换数据源 只会移除掉当前线程的数据源名称
*
*/
public static void poll() {
Deque deque = LOOKUP_KEY_HOLDER.get();
deque.poll();
if (deque.isEmpty()) {
LOOKUP_KEY_HOLDER.remove();
}
}
/**
* 强制清空本地线程
*
* 防止内存泄漏,如手动调用了push可调用此方法确保清除
*
*/
public static void clear() {
LOOKUP_KEY_HOLDER.remove();
}