需求背景介绍
分享一个实际开发中关于@DS和@Transactional的多数据源知识点,绝对能吊打大多数面试官。
考虑到业务层⾯有多数据源切换的需求
在Springboot开发中,要同时考虑到事务和多数据源的情况下,我使⽤了Mybatis-Plus3中的***@DS***作为多数据源的切换,它的原理的就是⼀个拦截器
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
try {
DynamicDataSourceContextHolder.push(determineDatasource(invocation));
return invocation.proceed();
} finally {
DynamicDataSourceContextHolder.poll();
}
}
⾥⾯的pull和poll实际就是操作⼀个容器
在环绕⾥⾯进来做***“压栈”,出去做"弹栈"***,数据结构是这样的
public final class DynamicDataSourceContextHolder {
/**
* 为什么要⽤链表存储(准确的是栈)
* <pre>
* 为了⽀持嵌套切换,如ABC三个service都是不同的数据源
* 其中A的某个业务要调B的⽅法,B的⽅法需要调⽤C的⽅法。⼀级⼀级调⽤切换,形成了链。
* 传统的只设置当前线程的⽅式不能满⾜此业务需求,必须模拟栈,后进先出。
* </pre>
*/
@SuppressWarnings("unchecked")
private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new ThreadLocal() {
@Override
protected Object initialValue() {
return new ArrayDeque();
}
};
private DynamicDataSourceContextHolder() {
}
/**
* 获得当前线程数据源
*
* @return 数据源名称
*/
public static String peek() {
return LOOKUP_KEY_HOLDER.get().peek();
}
/**
* 设置当前线程数据源
* <p>
* 如⾮必要不要⼿动调⽤,调⽤后确保最终清除
* </p>
*
* @param ds 数据源名称
*/
public static void push(String ds) {
LOOKUP_KEY_HOLDER.get().push(StringUtils.isEmpty(ds) ? "" : ds);
}
/**
* 清空当前线程数据源
* <p>
* 如果当前线程是连续切换数据源 只会移除掉当前线程的数据源名称
* </p>
*/
public static void poll() {
Deque<String> deque = LOOKUP_KEY_HOLDER.get();
deque.poll();
if (deque.isEmpty()) {
LOOKUP_KEY_HOLDER.remove();
}
}
/**
* 强制清空本地线程
* <p>
* 防⽌内存泄漏,如⼿动调⽤了push可调⽤此⽅法确保清除
* </p>
*/
public static void clear() {
LOOKUP_KEY_HOLDER.remove();
}
上⾯就是**@DS⼤概实现,然后我就碰到坑了,外层service加了@Transactional**,通过service调⽤另⼀个数据源做insert,在切⾯⾥看数据源切换了,但是还是显⽰事务内的数据源还是旧的,代码结构简单罗列下:
数据源配置
spring:
#多数据源配置
datasource:
hikari:
max-lifetime: 50
dynamic:
primary: common #设置默认的数据源或者数据源组,默认值即为master
strict: false #设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候会抛出异常,不启动则使用默认数据源.
p6spy: false
datasource:
#公共数据库配置信息
common:
url: jdbc:mysql://***?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: ******
password: ******
driver-class-name: com.mysql.cj.jdbc.Driver
#第二个库配置信息
dataTemp:
url: jdbc:mysql://***?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: ******
password: ******
driver-class-name: com.mysql.cj.jdbc.Driver
外层controller调⽤的service
@Autowired UserService userService;
@Autowired RedisClient redisClient;
@GetMapping("/demo") @Transactional
public GeneralResponse demo(@RequestBody(required = false) GeneralRequest request){
SysUser sysUser = new SysUser();
sysUser.setCode("zs");
sysUser.setName("张三");
sysUser.insert();
redisClient.set("token",sysUser);
List<SysUser> sysUsers = new SysUser().selectAll();
String item01 = userService.getUserInfo("ITEM01");
return GeneralResponse.success();
}
内层service
@Service
public class UserServiceImpl implements UserService {
@Override
@DS("interface")
@Transactional
// @Transactional(propagation = Propagation.REQUIRES_NEW)
public String getUserInfo(String name) {
SapItemRecord sr = new SapItemRecord();
sr.setBatchId(1L);
sr.setItemCode("ITEM01");
sr.setDescription("物料1号");
if (sr.insert()) {
LambdaQueryWrapper<SapItemRecord> item01 = new QueryWrapper<SapItemRecord>().lambda().eq(SapItemRecord::getItemCode, name);
SapItemRecord sapItemRecord = new SapItemRecord().selectOne(item01);
ExceptionUtils.seed("内层事务异常");
// return sapItemRecord.getDescription();
}
return "response : wonder";
}
}
**1.**最开始内层不加事务,全局只有⼀个事务,⽆效;
**2.内层加事务@Transactional,**⽆效;
3.改变事务的传播⽅式@Transactional(propagation = Propagation.REQUIRES_NEW),事务⽣效
看了java⽅法栈和源码,springframework5 ⾥⾯spring-tx,知道问题出在什么地⽅,贴⼀个调⽤栈截图
spring的事务是基于aop的,这个不解释了,直接进⼊事务拦截器TransactionInterceptor,找到它调⽤的invokeWithinTransaction⽅法,只看本⽂章关注部分
根据method的注解判断是否开启事务
处理异常,在finally⾥处理cleanupTransactionInfo
if(txAttr ==null||!(ptm instanceof CallbackPreferringPlatformTransactionManager))
{ // Standard transaction demarcation with getTransaction and commit/rollback calls.
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
Object retVal;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
retVal = invocation.proceedWithInvocation();
} catch (Throwable ex) {
// target invocation exception
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
} finally {
cleanupTransactionInfo(txInfo);
}
....
}
protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm, @Nullable TransactionAttribute txAttr, final String joinpointIdentification) {
// If no name specified, apply method identification as transaction name.
if (txAttr != null && txAttr.getName() == null) {
txAttr = new DelegatingTransactionAttribute(txAttr) {
@Override
public String getName() {
return joinpointIdentification;
}
};
}
TransactionStatus status = null;
if (txAttr != null) {
if (tm != null) {
// 重点是这⾥,获取事务
status = tm.getTransaction(txAttr);
} else {
if (logger.isDebugEnabled()) {
logger.debug("Skipping transactional joinpoint [" + joinpointIdentification + "] because no transaction manager has been configured");
}
}
}
return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}
这里就是按照不同的事务传播机制
去做不同的处理,判断是否存在事务,存在事务就执⾏handleExistingTransaction,不存在的话满⾜创建的条件就startTransaction,这⾥我的情形就是第⼀次直接创建,第⼆次执⾏exist逻辑
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException {
// Use defaults if no transaction definition given.
TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults());
Object transaction = doGetTransaction();
boolean debugEnabled = logger.isDebugEnabled();
if (isExistingTransaction(transaction)) {
// Existing transaction found -> check propagation behavior to find out how to behave.
return handleExistingTransaction(def, transaction, debugEnabled);
}
// Check definition settings for new transaction.
if (def.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
throw new InvalidTimeoutException("Invalid transaction timeout", def.getTimeout());
}
// No existing transaction found -> check propagation behavior to find out how to proceed.
if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
throw new IllegalTransactionStateException("No existing transaction found for transaction marked with propagation 'mandatory'");
} else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED || def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW || def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
SuspendedResourcesHolder suspendedResources = suspend(null);
if (debugEnabled) {
logger.debug("Creating new transaction with name [" + def.getName() + "]: " + def);
}
try {
return startTransaction(def, transaction, debugEnabled, suspendedResources);
} catch (RuntimeException | Error ex) {
resume(null, suspendedResources);
throw ex;
}
} else {
// Create "empty" transaction: no actual transaction, but potentially synchronization.
if (def.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) {
logger.warn("Custom isolation level specified but no actual transaction initiated; " + "isolation level will effectively be ignored: " + def);
}
boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
return prepareTransactionStatus(def, null, true, newSynchronization, debugEnabled, null);
}
}
这里是创建新事务
private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction, boolean debugEnabled, @Nullable SuspendedResourcesHolder suspendedResources) {
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
doBegin(transaction, definition);
//dobegin⾥⾯关乎数据源和数据库连接
prepareSynchronization(status, definition);
return status;
}
doBegin ⾥我最关⼼两点,⼀个是数据库连接的选择和初始化,⼀个是把事务的⾃动提交关掉
这⾥就能解释得通,为什么**@Transactional⾥的数据源还是旧的。因为开启事务的同时,会去数据库连接池拿数据库连接,如果只开启⼀个事务,在切⾯时候会获取数据源,设置dataSource**;如果在内 层的service使⽤**@DS切换了数据源,实际上是⼜做了⼀层拦截,改变了DataSourceHolder的栈顶dataSource**,对于整个事务的连接是没有影响的,在这个事务切⾯内的所有数据库的操作都会使⽤代理 之后的事务连接,所以会产⽣数据源没有切换的问题
对于数据源的切换,必然要更替数据库连接
我的理解是必须改变事务的传播机制,产⽣新的事务,所以第⼀内层service不仅要加**@DS**,还要加**@Transactional**注解,并且指定
Propagation.REQUIRES_NEW,因为这样在处理handleExistingTransaction 时,就会⾛这段逻辑
{
if (debugEnabled) {
logger.debug("Suspending current transaction, creating new transaction with name [" + definition.getName() + "]");
}
SuspendedResourcesHolder suspendedResources = suspend(transaction);
try {
return startTransaction(definition, transaction, debugEnabled, suspendedResources);
} catch (RuntimeException | Error beginEx) {
resumeAfterBeginException(transaction, suspendedResources, beginEx);
throw beginEx;
}
}
结论
⾛startTransaction,再doBegin,创建新事务,重新拿切换之后的dataSource作为新事务的conn,这样内层事务的数据源就是***@DS注解内的,从⽽完成了数据源切换并且事务⽣效,PROPAGATION_REQUIRES_NEW ⽅式下,事务的回滚都是⽣效的,亲测,所以使⽤MybatisPlus3.x的可以使⽤@DS了,当然你也可以⾃⼰写切⾯去切换DataSource***,原理跟DS差不多,我⽤baomidou,因为它⾹啊!但是我觉得baomidou在考虑切换数据源的时候,本⾝要考虑事务的,但是⼈家是这样说的
以上为个⼈经验,希望能给⼤家⼀个参考,也希望⼤家多多⽀持。