能吊打大多数面试官的多数据源知识点

需求背景介绍

分享一个实际开发中关于@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在考虑切换数据源的时候,本⾝要考虑事务的,但是⼈家是这样说的

以上为个⼈经验,希望能给⼤家⼀个参考,也希望⼤家多多⽀持。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全粘架构师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值