多数据源@DS和@Transactional实战

目录

  • 考虑到业务层面有多数据源切换的需求
  • 里面的pull和poll实际就是操作一个容器
  • 数据源
  • 外层controller调用的service
  • 内层service
  • 根据method的注解判断是否开启事务
  • 这里就是按照不同的事务传播机制
  • 这里是创建新事务
  • 对于数据源的切换,必然要更替数据库连接

考虑到业务层面有多数据源切换的需求 同时又要考虑事务,我使用了Mybatis-Plus3中的@DS作为多数据源的切换,它的原理的就是一个拦截器
@Overridepublic Object invoke(MethodInvocation invocation) throws Throwable {try {DynamicDataSourceContextHolder.push(determineDatasource(invocation)); return invocation.proceed(); } finally {DynamicDataSourceContextHolder.poll(); }}


里面的pull和poll实际就是操作一个容器 在环绕里面进来做"压栈",出去做"弹栈",数据结构是这样的
public final class DynamicDataSourceContextHolder { /*** 为什么要用链表存储(准确的是栈)*
* 为了支持嵌套切换,如ABC三个service都是不同的数据源* 其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换,形成了链。* 传统的只设置当前线程的方式不能满足此业务需求,必须模拟栈,后进先出。*

*/@SuppressWarnings("unchecked")private static final ThreadLocal LOOKUP_KEY_HOLDER = new ThreadLocal() {@Overrideprotected Object initialValue() {return new ArrayDeque(); }}; private DynamicDataSourceContextHolder() {} /*** 获得当前线程数据源** @return 数据源名称*/public static String peek() {return LOOKUP_KEY_HOLDER.get().peek(); } /*** 设置当前线程数据源* * 如非必要不要手动调用,调用后确保最终清除*
** @param ds 数据源名称*/public static void push(String ds) {LOOKUP_KEY_HOLDER.get().push(StringUtils.isEmpty(ds) ? "" : ds); } /*** 清空当前线程数据源* * 如果当前线程是连续切换数据源 只会移除掉当前线程的数据源名称*
*/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(); }

上面就是@DS大概实现,然后我就碰到坑了,外层service加了@Transactional,通过service调用另一个数据源做insert,在切面里看数据源切换了,但是还是显示事务内的数据源还是旧的,代码结构简单罗列下:

数据源
dynamic:primary: masterstrict: falsedatasource:master:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://***/phorcys-centre?useSSL=falseusername: rootpassword: *****interface:url: jdbc:mysql://***/phorcys-interface?useSSL=falseusername: rootpassword: *****driver-class-name: com.mysql.cj.jdbc.Driver


外层controller调用的service
@AutowiredUserService userService; @AutowiredRedisClient redisClient; @GetMapping("/demo")@Transactionalpublic GeneralResponse demo(@RequestBody(required = false) GeneralRequest request){SysUser sysUser = new SysUser(); sysUser.setCode("wonder"); sysUser.setName("王吉坤"); sysUser.insert(); redisClient.set("token",sysUser); List sysUsers = new SysUser().selectAll(); String item01 = userService.getUserInfo("ITEM01"); return GeneralResponse.success(); }


内层service
@Servicepublic 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 item01 = new QueryWrapper().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,知道问题出在什么地方,贴一个调用栈截图
多数据源@DS和@Transactional实战
文章图片

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 exceptioncompleteTransactionAfterThrowing(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) {@Overridepublic 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 里我最关心两点,一个是数据库连接的选择和初始化,一个是把事务的自动提交关掉
多数据源@DS和@Transactional实战
文章图片

这里就能解释得通,为什么@Transactional里的数据源还是旧的。因为开启事务的同时,会去数据库连接池拿数据库连接,如果只开启一个事务,在切面时候会获取数据源,设置dataSource;如果在内层的service使用@DS切换了数据源,实际上是又做了一层拦截,改变了DataSourceHolder的栈顶dataSource,对于整个事务的连接是没有影响的,在这个事务切面内的所有数据库的操作都会使用代理之后的事务连接,所以会产生数据源没有切换的问题

对于数据源的切换,必然要更替数据库连接 我的理解是必须改变事务的传播机制,产生新的事务,所以第一内层service不仅要加@DS,还要加@Transactional注解,并且指定
Propagation.REQUIRES_NEW,因为这样在处理handleExistingTransaction 时,就会走这段逻辑
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {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在考虑切换数据源的时候,本身要考虑事务的,但是人家是这样说的
多数据源@DS和@Transactional实战
文章图片

【多数据源@DS和@Transactional实战】以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

    推荐阅读