笔者之前就事务和动态数据源之间的问题,做过一些探讨和总结(详见事务内动态数据源切换失效及传播属性)。后来从源码层面分析有了一些收获,篇幅较长不便写在原文中,因此另起一篇作为补充。
注意,上篇的实验和结论依旧有效,本文旨在将理论和实践结合,给出更精确的解决方案及底层原理。
先上结论,帮助同样被这个问题困扰的朋友们。
首先明确,事务控制得最小单位为“同一个数据库连接”,即想要正常控制事务回滚提交,那么整个过程只能基于一个Connection;而使用不同数据源就意味着一定对应不同的Connection,因此既要使用多数据源切换、又要使得所有数据库操作同步回滚提交,不引入分布式事务的前提下是做不到的(也许只是我做不到,如果有解决方案也可以分享)。
其次,在事务开启时如果没有指定数据源,事务会取默认数据源并使用他,即配置文件中的primary数据源。
基于上面的原则,结论如下:
事务和数据源注解的用法,直接上代码。
class Service1 { //由于未指定数据源便开启了事务,会使用primary数据源 @Transactional(rollbackFor = Exception.class) public void m1() { //SQL调用 } //开启事务时指定了数据源,会使用datasource2数据源 @DS("datasource2") @Transactional(rollbackFor = Exception.class) public void m2() { //SQL调用 } }
上面为一个方法内只使用单个数据源的方式,在开启事务时就需要指定数据源,否则会默认使用主数据源;一般常用的方式是在Mapper接口上加@DS,但这种方式在这种场景下是无效的,因为事务在Service层就开启,进入Mapper哪怕切换出火花来也不好使。
class Service1 { //由于未指定数据源便开启了事务,会使用primary数据源 //m3加入该事务,也会使用primary //m4新建事务,使用datasource2 @Transactional(rollbackFor = Exception.class) public void m1() { //SQL调用 Service2.m3(); Service2.m4(); } //开启事务时指定了数据源,会使用datasource2数据源 //m3加入该事务,也会使用datasource2 //m4新建事务,使用datasource3 @DS("datasource2") @Transactional(rollbackFor = Exception.class) public void m2() { //SQL调用 Service2.m3(); Service2.m4(); } } class Service2 { //默认传播属性,加入已有事务 @DS("datasource2") @Transactional(rollbackFor = Exception.class) public void m3() { //SQL调用 } //REQUIRES_NEW,挂起原事务,开启新事务 @DS("datasource3") @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) public void m4() { //SQL调用 } }
上述代码一个方法内多次切换数据源,首先一定要确保是注入Bean来调用方法,否则加啥注解也不好使,动态代理会直接失效。
先看m1(),没有指定数据源所以使用primary数据源,调用得m3()使用了默认的REQUIRED传播属性,会加入m1()的事务一起使用primary,因此m3()上的数据源切换失效;而m4()使用了REQUIRES_NEW,挂起了m1()开启的事务并新建了事务,开启事务的同时指定了数据源datasource3,因此m4()不会受任何人影响做自己想做的。
m2()类似,会使m3()使用m2()开启时指定的datasource2,m4()用自己的datasource3。
结论有了,自然也要有辩证的过程,矛盾集中在@Transactional和@DS同时使用,因此我们要整明白这两个注解都干了什么、哪一步产生了冲突、又该如何解决这种冲突,我们来一步一步盘。
断点加在从Controller跳转到Service实现类中间,因为Spring注解采用了AOP动态代理来实现功能增强,那连接点就在加了注解的方法上,切点在方法执行前;因此不论是事务的开启还是数据源的切换,我们都需要关注AOP是如何对其进行改造的。
在跳转进方法后进入了CglibAopProxy,入参为代理类、方法签名、方法实参和代理方法,重点是在连接点中获取方法对应的注解,再去ProxyFactory获取注解对应的拦截器,返回一个拦截器链列表;当拦截器链不为空时,就会将代理对象、被代理对象、被代理方法、方法入参、被代理类、拦截器链、代理方法传入代理方法执行器并执行。
//获取被代理原对象,也就是单例Bean target = targetSource.getTarget(); Class targetClass = target != null ? target.getClass() : null; //根据方法签名和类获取拦截器链 List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); Object retVal; //拦截器为空则直接反射调用,不为空则进入拦截器逻辑 if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) { Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); retVal = methodProxy.invoke(target, argsToUse); } else { retVal = (new CglibAopProxy.CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy)).proceed(); } Debug中方法注解和chain,正对应着我们加入的两个拦截器: 到这一步,说明AOP成功感知到了调用方法需要事务和数据源切换的增强,并将根据拦截器链挨个处理;执行的顺序是先切数据源、再开启事务,如此合理也自然不会是巧合,肯定是框架内部指定了Order之类的属性,至于如何控制优先级就不展开讨论了。 2.2 DS拦截器 按照顺序,首先要处理@DS对应的DynamicDataSourceAnnotationInterceptor。 invoke()方法的入参为this当前对象,即我们上面说到的所有东西都传过来了,拦截器首先要看看准备往哪个数据源切;这一步逻辑也很简单,就是去方法签名里找DS注解对应的value,比较特别的是他内部维护了一个dsCache,是个放了所有方法使用数据源名称字符串的ConcurrentHashmap,每次先去dsCache查,有就返回没有就查了放进去供下次使用。 //DataSourceClassResolver处理切换数据源名称逻辑 public String findKey(Method method, Object targetObject) { if (method.getDeclaringClass() == Object.class) { return ""; } else { //根据方法签名获取数据源名称 //dsCache为本地Map缓存,查询过的会缓存下来避免重复查询 Object cacheKey = new MethodClassKey(method, targetObject.getClass()); String ds = (String)this.dsCache.get(cacheKey); if (ds == null) { ds = this.computeDatasource(method, targetObject); if (ds == null) { ds = ""; } this.dsCache.put(cacheKey, ds); } return ds; } } 获取到数据源名称后,关键的一步出现了!调用了工具类DynamicDataSourceContextHolder 并将数据源名称push了进去,push进了一个怎样的容器,这点是至关重要的,我们来具体看看。 private static final ThreadLocal> LOOKUP_KEY_HOLDER = new NamedThreadLocal>("dynamic-datasource") { protected Deque initialValue() { return new ArrayDeque(); } }; public static String push(String ds) { String dataSourceStr = StringUtils.isEmpty(ds) ? "" : ds; ((Deque)LOOKUP_KEY_HOLDER.get()).push(dataSourceStr); return dataSourceStr; } 可以看到线程绑定变量ThreadLocal里放了一个Deque,Deque是一个双端队列,可以当作栈用也可以当作队列用。实例化时使用了ArrayDeque,因此在此处将其用作了先进先出的队列,其push方法底层调用了addFirst(),将数据放在了队列头。 放完后DS拦截器的活儿基本也干完了,于是继续向下调用拦截器,因为拦截器是链式调用嘛,在所有链路走完前肯定不能断,因此就又回到ReflectiveMethodInvocation的proceed方法,处理下一个拦截器。 2.3 Transactional拦截器 TransactionInterceptor的invoke()方法只做了一件事,将方法签名、被代理类、反射执行器的proceed方法引用,传递给真正开启并处理事务的TransactionAspectSupport。可能会有疑惑的是这个“proceed方法引用”,其实也很简单,上一步DS拦截器最后一步调用了proceed方法,来继续向下处理拦截器链;这里也是同理,将方法引用传进去,当前拦截器处理完毕后再回到ReflectiveMethodInvocation向下处理。 之后便进入TransactionAspectSupport的invokeWithinTransaction()方法处理事务,我们来依次看看他都做了些什么。 先看看最前面,在学习编程式事务时大家都清楚,我们要获取事务属性TransactionDefinition、将其传入TransactionManager构建事务管理器并创建事务;这里的逻辑也是类似的, // If the transaction attribute is null, the method is non-transactional. TransactionAttributeSource tas = getTransactionAttributeSource(); final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null); final PlatformTransactionManager tm = determineTransactionManager(txAttr); final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); 原作者的注释也写得很清楚:如果事务属性为null,说明为非事务方法。这一点也是贯穿整个流程的,很多地方都是基于是否开启事务进行逻辑区分的,就比如第2行TransactionAttribute的获取,事务属性不为null才去获取他。TransactionAttribute其实就是TransactionDefinition的子类,猫叫咪咪而已。 这几行执行完以后,就有了事务属性、事务管理器以及连接点描述(就是方法名称,根据Method方法签名获取,但内部逻辑有点没看懂),接下来要根据这些信息进行事务逻辑处理。 先看第一个if分支,如果没有事务属性,或者事务管理器不是CallbackPreferringPlatformTransactionManager 事务管理器的实例,就会进入这部分逻辑。至于这个特定的事务管理器,网上没有搜到很明确的资料,但既然他叫回调、且else逻辑里也是以回调形式调用,姑且就认为他是回调事务管理器。 //没有事务属性(也就是没有事务)时,会进入这段逻辑 if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) { // Standard transaction demarcation with getTransaction and commit/rollback calls. TransactionInfo txInfo = createTransactionIfNecessary(tm, 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); } commitTransactionAfterReturning(txInfo); return retVal; } 我们传入了事务属性,也使用的是JDBC的DataSourceTransactionManager,因此会进入if逻辑。有意义的是createTransactionIfNecessary()方法,返回值是TransactionInfo也就是TransactionStatus的子类,进去看看里面的处理逻辑: // 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); 方法里将事务属性包装成了一个代理类,将事务名称定义为了方法名,之后便是喜闻乐见的用事务管理器开启事务getTransaction()。 Object transaction = doGetTransaction(); //JDBC实现类 DataSourceTransactionManager protected Object doGetTransaction() { DataSourceTransactionManager.DataSourceTransactionObject txObject = new DataSourceTransactionManager.DataSourceTransactionObject(); txObject.setSavepointAllowed(this.isNestedTransactionAllowed()); ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(this.obtainDataSource()); txObject.setConnectionHolder(conHolder, false); return txObject; } 先调用doGetTransaction()创建一个对象txObject,再将当前事务的一些信息封装进去。比较有意思的是获取ConnectionHolder,会先调用obtainDataSource()获取当前数据源集合,此时获取到的是: 可不要以为是原生DataSource哦,在项目启动时DynamicRoutingDataSource就作为DataSource实现类被注入,且里面放置了所有已经初始化的数据源。最后ConnectionHolder为null,因为事务是刚刚开启的;而是否持有连接与是否开启新事务有着很大联系,单个事物只能对应单个连接,要切换就必须开启新的事务并重新获取连接,这点在后面的handleExistingTransaction()方法逻辑里就有体现。 此时我们的事务管理器还空空的,没办法,刚开启事务,啥也拿不到。所以下面的逻辑都不会走。 if (definition == null) { // Use defaults if no transaction definition given. definition = new DefaultTransactionDefinition(); } //根据是否持有连接/事务是否活跃 判断当前是否有事务在处理中 //有的话调用handleExistingTransaction(),根据事务传播属性处理新旧事务切换等 if (isExistingTransaction(transaction)) { // Existing transaction found -> check propagation behavior to find out how to behave. return handleExistingTransaction(definition, transaction, debugEnabled); } // Check definition settings for new transaction. if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) { throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout()); } 下面是一长串判断,根据事务传播属性分别处理事务,我们使用了默认的REQUIRED,因此看看这段逻辑。 SuspendedResourcesHolder suspendedResources = suspend(null); if (debugEnabled) { logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition); } try { boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); DefaultTransactionStatus status = newTransactionStatus( definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); doBegin(transaction, definition); prepareSynchronization(status, definition); return status; } suspend()方法就不细看了,里面是TransactionSynchronizationManager相关的处理逻辑。他是线程同步事务管理器,可以实现类似CompletableFuture.whenComplete()的功能,意为这个事务执行完以后、异步事务再开始执行,可以避免主事务插入的数据没commit,异步事务想查又查不到的尴尬情景。 之后初始化了一个TransactionStatus,里面放了一些事务相关的基本属性,然后调用doBegin()方法开启事务。这一步是最重要的,前面铺垫了那么久好像一直没有数据源什么事,其实就是为了这一刻,咱们来一行一行看。 DataSourceTransactionManager.DataSourceTransactionObject txObject = (DataSourceTransactionManager.DataSourceTransactionObject)transaction; Connection con = null; if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) { Connection newCon = this.obtainDataSource().getConnection(); txObject.setConnectionHolder(new ConnectionHolder(newCon), true); } 先初始化几个属性,然后判断事务管理器是否持有连接,没有就进入if逻辑。前文提到obtainDataSource()获取当前数据源集合,获取到了DynamicRoutingDataSource,那再看看getConnection()干了点啥。 public Connection getConnection() throws SQLException { //获取全局事务号 String xid = TransactionContext.getXID(); //根据是否有事务号判断分支 if (StringUtils.isEmpty(xid)) { return this.determineDataSource().getConnection(); } else { String ds = DynamicDataSourceContextHolder.peek(); ds = StringUtils.isEmpty(ds) ? "default" : ds; ConnectionProxy connection = ConnectionFactory.getConnection(ds); return (Connection)(connection == null ? this.getConnectionProxy(ds, this.determineDataSource().getConnection()) : connection); } } 因为我们的事务号为null,因此要先调用determineDataSource()。 public DataSource determineDataSource() { String dsKey = DynamicDataSourceContextHolder.peek(); return this.getDataSource(dsKey); } 还记得这个DynamicDataSourceContextHolder吗?正是DS拦截器之前操作的动态数据源中,ThreadLocal里的双端队列Deque!调用Deque的peek()方法,从队列里拿到数据源名称后,又到服务启动时注册的数据源ConcurrentHashMap,拿到了我们所需的DataSource对象! 至此,我逐渐理解一切。 之后就是数据库连接池Druid的活儿了,他去池子里拿到了池化Connection;再回到事务管理器,将连接做成连接持有设置给事务管理器。到这里,我们的事务管理器终于不是光秃秃的了,他有了沉甸甸的数据库连接。 再回到一开始的抽象事务管理器类中,现在的TransactionStatus可谓是应有尽有。 将这些东西全部传入prepareTransactionInfo()方法,包括准备好的事务管理器、事务属性、方法名称、事务状态。 //所有信息封装成一个TransactionInfo对象 TransactionInfo txInfo = new TransactionInfo(tm, txAttr, joinpointIdentification); if (txAttr != null) { //绑定进TransactionInfo对象 txInfo.newTransactionStatus(status); } else { //打日志,啥也没干 } //将对象绑定至当前线程 txInfo.bindToThread(); return txInfo; 方法的逻辑非常简单,精简了一下就是上面的内容。创建了一个大对象,把所有事务相关的信息全部封装进去,然后绑定到当前线程。提到当前线程,猜也猜得到又是一个ThreadLocal,用一个变量先将旧事务存了起来,又将新事务塞进ThreadLocal,这也就实现了多事务的挂起和切换。 事务和数据源拦截器全整完了,之后就去找下一个进行处理,而我们的拦截器链已经全部走完了,因此就可以愉快地回到Service方法里处理SQL了。 3 问题分析 相信看完上面源码解析的朋友应该懂为什么会导致切换失败了,我们再从源码层面分析之前举例的几种情况。 3.1 加@DS与不加@DS 前文的分析表明,加上@DS会给ThreadLocal中的Deque加入指定数据源名称,如果我们不加,这个Deque就会是空的,这点肯定没有异议。 public DataSource determineDataSource() { String dsKey = DynamicDataSourceContextHolder.peek(); return this.getDataSource(dsKey); } 此时的dsKey为null,给getDataSource()方法传入null会发生什么呢? public DataSource getDataSource(String ds) { if (StringUtils.isEmpty(ds)) { return this.determinePrimaryDataSource(); } else if (!this.groupDataSources.isEmpty() && this.groupDataSources.containsKey(ds)) { log.debug("dynamic-datasource switch to the datasource named [{}]", ds); return ((GroupDataSource)this.groupDataSources.get(ds)).determineDataSource(); } else if (this.dataSourceMap.containsKey(ds)) { log.debug("dynamic-datasource switch to the datasource named [{}]", ds); return (DataSource)this.dataSourceMap.get(ds); } else if (this.strict) { throw new CannotFindDataSourceException("dynamic-datasource could not find a datasource named" + ds); } else { return this.determinePrimaryDataSource(); } } 没错,正如大家所想,为空就会去取primary主数据源。因此建议大家无论如何都要配置primary属性,默认为“master”,如果你没有master数据源就会直接报错。这也就解释了为什么在不指定数据源时,事务开启会直接使用主数据源。 3.2 不加@Transactional 不加事务时,执行到对应的SQL时会先获取对应数据源,如果加@DS就获取指定数据源、不加就使用主数据源。这也是最常用的用法,不加事务时想怎么切怎么切。 3.3 加@Transactional 加上事务可就不一样了,在方法开启事务的一瞬间,就会走我们上面所说的那一套逻辑,而这种又分两种情况。 一种是开启事务时就指定数据源。由于事务开启以前会先处理@DS拦截器逻辑,处理完数据源队列中就摆好了需要使用的数据源;此时事务拦截器去队列里取,此时队列不为空,直接就能取到所需的数据源,所以可以切换成功。 另一种是开启事务后,在内部调用的Mapper再指定数据源。看这个执行顺序就很明显,在开启事务时,数据源队列是空的,一peek发现为空就只能取主数据源;你里面的Mapper哪怕切一晚上,也只是加入了队列里,但无人在意。 4 总结 想要成功切换数据源一定要保证这几点: 第一要保证事务开启和数据源指定同步进行,不然事务开启只能拿到主数据源; 第二要保证创建一个新的事务,否则拦截器发现当前存在嵌套事务、又没有指定新建事务或不支持事务的传播属性,就会加入上一层的事务,一个事务内的数据库连接一定是同一个,更别说不同的数据源了,这是不可能的。 你可能感兴趣的:(数据库,mysql,java,spring,boot) 【股票数据API接口46】如何获取股票指历史分时BOLL数据之Python、Java等多种主流语言实例代码演示通过股票数据接口获取数据 Athena二哈 pythonjava开发语言股票数据接口api 如今,量化分析在股市领域风靡一时,其核心要素在于数据,获取股票数据,是踏上量化分析之路的第一步。你可以选择亲手编写爬虫来抓取,但更便捷的方式,莫过于利用专业的股票数据API接口。自编爬虫虽零成本,却伴随着时间与精力的巨大消耗,且常因目标页面变动而失效。大家可以依据自己的实际情况来决定数据获取方式。接下来,我将分享200多个实测可用且免费的专业股票数据接口,并通过Python、JavaScript( 【股票数据API接口45】如何获取股票指历史分时MACD数据之Python、Java等多种主流语言实例代码演示通过股票数据接口获取数据 Athena二哈 pythonjava开发语言api股票数据接口 如今,量化分析在股市领域风靡一时,其核心要素在于数据,获取股票数据,是踏上量化分析之路的第一步。你可以选择亲手编写爬虫来抓取,但更便捷的方式,莫过于利用专业的股票数据API接口。自编爬虫虽零成本,却伴随着时间与精力的巨大消耗,且常因目标页面变动而失效。大家可以依据自己的实际情况来决定数据获取方式。接下来,我将分享200多个实测可用且免费的专业股票数据接口,并通过Python、JavaScript( 使用 Three.js 转换 GLSL 粒子效果着色器 贵州数擎科技有限公司 javascript着色器开发语言 大家好!我是[数擎AI],一位热爱探索新技术的前端开发者,在这里分享前端和Web3D、AI技术的干货与实战经验。如果你对技术有热情,欢迎关注我的文章,我们一起成长、进步!开发领域:前端开发|AI应用|Web3D|元宇宙技术栈:JavaScript、React、ThreeJs、WebGL、Go经验经验:6年+前端开发经验,专注于图形渲染和AI技术开源项目:AI简历、元宇宙、数字孪生在这篇博客中,我们 MySQL × 向量数据库:大模型时代的黄金组合实战指南 mysql人工智能 一、大模型时代的数据存储革命1.1传统架构的局限性--传统商品表结构CREATETABLEproducts(idINTPRIMARYKEY,titleVARCHAR(255),descriptionTEXT,category_idINT);--典型关键词搜索SELECT*FROMproductsWHEREtitleLIKE'%智能手机%'ORdescriptionLIKE'%旗舰机型%';痛点分析 【mysql】WITH AS 语法详解 m0_74824091 面试学习路线阿里巴巴mysql数据库 【mysql】WITHAS语法详解【一】WITHAS语法的基本结构【二】案例1【三】案例2WITHAS语法是MySQL中的一种临时结果集,它可以在SELECT、INSERT、UPDATE或DELETE语句中使用。通过使用WITHAS语句,可以将一个查询的结果存储在一个临时表中,然后在后续的查询中引用这个临时表。这样可以简化复杂的查询,提高代码的可读性和可维护性。【一】WITHAS语法的基本结构WI SQL server 三种常用的触发器 漫天转悠 #SQLserverSQLserver三种常用触发器 SQLserver三种常用的触发器1.触发器的创建2.insert触发器3.update触发器4.delete触发器5.关于取值说明1.触发器的创建创建触发器时可以先判断一下当前数据库里是否已存在相同名字的触发器sqlserver的触发器名保存在sysobjects这张表里所以要知道是否存在只需创建前查询下该表即可ifnotexists(select1fromsysobjectswherename SpringBoot - Cookie & Session 用户登录及登录状态保持功能实现 Loop Lee javaspringboot 会话技术功能:提供用户登陆成功后的登陆标记(一次登录,一段时间都登录)会话定义:包含一次或多次请求和响应的访问操作建立会话:用户打开浏览器访问Web服务器资源时建立会话结束会话:一方断开连接时结束会话会话跟踪定义:一种维护浏览器状态的方法功能:服务器通过会话跟踪来识别多次请求是否来自于同一浏览器同一次会话的多次请求间共享数据会话跟踪方案客户端会话跟踪技术:Cookie服务端会话跟踪技术:Sessi 为什么面试狂问Redis,阿里面试官把我问到哑口无言… 2501_90433130 面试redis职场和发展 Redis我们在工作中经常会用到,但是为什么要用、redis的一些场景和实战问题,90%以上的人都不是很懂。曾经自己去面试阿里,就被Redis问题问到哑口无言…事后我专门去恶补了Redis,现在算是比较精通了。作为目前主流的NoSQL技术,redis在Java互联网中得到了非常广泛的使用,个时代码代码的秃头人员,对Redis肯定是不陌生的,如果连Redis都没用过,还真不好意思出去面试,指不定被面 项目经验之LZO压缩?思维导图 代码示例(java 架构) 用心去追梦 java架构开发语言 LZO(LightweightZip/Unzip)是一种高效的压缩算法,它以快速解压缩著称,适用于需要频繁读取和处理的数据。在Hadoop生态系统中,使用LZO压缩可以显著减少存储空间,并且由于其快速的解压速度,对于大规模数据处理任务来说是非常有利的。以下是关于LZO压缩的项目经验总结、思维导图描述以及Java代码示例。项目经验之LZO压缩LZO的优势快速解压:LZO算法设计时优先考虑了解压速度, 面试之《前端开发者如何优化页面的加载时间?》 只会写Bug的程序员 面试面试前端 前端开发者可以从多个方面入手优化页面的加载时间,以下是一些常见且有效的方法:优化资源加载压缩资源文件:对HTML、CSS、JavaScript文件进行压缩,去除不必要的空格、注释等,减小文件体积,加快下载速度。例如使用uglify-js压缩JavaScript文件,cssnano压缩CSS文件。优化图片:对图片进行压缩处理,降低图片的分辨率、色彩深度或采用更高效的图片格式(如WebP)。同时,根据 【SpringCloud】Gateway m0_74825526 面试学习路线阿里巴巴springcloudgatewayjava 目录一、网关路由1.1.认识网关1.2.快速入门?1.2.1.引入依赖1.2.2.配置路由二、网关登录校验2.1.Gateway工作原理?2.2.自定义过滤器2.3.登录校验2.4.微服务获取用户2.4.1.保存用户信息到请求头2.4.2.拦截器获取用户??2.5.OpenFeign传递用户三、配置管理3.1.配置共享?3.2.拉取配置共享3.2.1.引入依赖3.2.2.创建bootstrap.y 【MySQL】在 Centos7 环境安装 MySQL -- 详细完整教程 m0_74825526 面试学习路线阿里巴巴mysqlwebviewandroid 说明:安装与卸载中,用户全部切换成为root,一旦安装,普通用户就能使用。一、卸载内置环境1、卸载不要的环境[root@VM-8-5-centos~]$psajx|grepmariadb#先检查是否有mariadb存在13134148441484313134pts/014843S+10050:00grep--color=automariadb19010191871901019010?-1Sl271 计算机复试面试题总结 m0_67400972 面试学习路线阿里巴巴android前端后端 时隔两年,重新完善一下以前写的东西:更新!!!!1.c++,408,设计模式,编程技巧,开源框架(适合cpp后端开发)2.数据结构与算法面试题3.c++与STL面试题4.计算机网络面试题面试问题之编程语言1。C++的特点是什么?封装,继承,多态。支持面向对象和面向过程的开发。2.C++的异常处理机制?抛出异常和捕捉异常进行处理。(实际开发)3.c和c++,java的区别c是纯过程,c++是对象加过 计算机毕业设计 ——jspssm507Springboot 的论坛管理系统 程序媛9688 课程设计 作者:程序媛9688开发技术:SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等。文末获取源码+数据库感兴趣的可以先收藏起来,还有大家在毕设选题(免费咨询指导选题),项目以及论文编写等相关问题都可以给我留言咨询,希望帮助更多的人计算机毕业设计——jspssm507Springboot的论坛管理系统JSPSSM507SpringBoot论坛管理系统功 数据库分类与数据库基本原则(ACID、CAP、BASE) 气运2020 Redis数据库数据库nosqldatabase 分布式系统中ACID和CAP有什么区别-知乎(zhihu.com)关系型数据库遵循ACID规则&&NoSQL数据库BASECAP-玲汐-博客园(cnblogs.com)分布式系统设计时,遵循CAP原则_alpha_2017的博客-CSDN博客1、数据库与数据库规则1.1数据库1)关系型数据库SQL:传统的SQL数据库的事务通常都是支持ACID的强事务机制关系型数据库:-高度组织化结构化数据-结构化 php事务基本要素,ACID数据库事务正确执行的四个基本要素 不懂就承认 php事务基本要素 ACID数据库事务正确执行的四个基本要素ACID,指数据库事务正确执行的四个基本要素的缩写。包含:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。一个支持事务(Transaction)的数据库系统,必需要具有这四种特性ACID——数据库事务正确执行的四个基本要素ACID,指数据库事务正确执行的四个基本要素的缩写。包含:原 alter日志报WARNING: too many parse errors weixin_30480075 数据库版本:12.2.0操作系统版本:RHEL7.2最近观察到一个数据库alert日志老是报硬解析太多错误,且对应的sql语句都是查看数据字典表:2017-06-16T08:46:46.417468+08:00TTEST(4):WARNING:toomanyparseerrors,count=100SQLhash=0x03b29074TTEST(4):PARSEERROR:ospid=3504,e 数据库必知必会系列:数据库分片与分布式事务 AI天才研究院 AI大模型企业级应用开发实战大数据人工智能语言模型JavaPython架构设计 文章目录1.背景介绍分库分表分片集群分布式事务数据迁移2.核心概念与联系主从复制活动复制CAP原则BASE理论3.核心算法原理和具体操作步骤以及数学模型公式详细讲解分库分表水平分表垂直分库分片集群垂直拆分水平切分垂直切分水平拆分根据主键范围根据业务字段划分分布式事务两阶段提交协议三阶段提交协议可靠消息最终一致性ACID四要素4.具体代码实例和详细解释说明MyCat配置文件server.xml文件s Hutool - Script:脚本执行封装,以 JavaScript 为例 五行星辰 业务系统应用技术javascript开发语言java后端 一、简介在Java开发中,有时需要动态执行脚本代码,比如JavaScript脚本,来实现一些灵活的业务逻辑,如动态规则计算、数据处理等。Java本身提供了javax.script包来支持脚本执行,但使用起来较为繁琐。Hutool-Script模块对Java的脚本执行功能进行了封装,提供了更简洁易用的API,让开发者可以方便地执行各种脚本,这里主要介绍JavaScript脚本的执行。二、引入依赖如果 springboot习题 苍曦 java开发语言 第1章一、填空题1.Pivotal团队在原有Spring框架的基础上开发了全新的SpringBoot框架。2.SpringBoot框架在开发过程中大量使用约定优先配置的思想来摆脱框架中各种复杂的手动配置。3.SpringBoot2.1.3版本要求Java8及以上版本的支持。4.SpringBoot2.1.3版本框架官方声明支持的第三方项目构建工具包括有Maven(3.3+)和Gradle(4.4+ 数据整合平台Airbyte中的Shopify连接器使用指南 bavDHAUO python 技术背景介绍Airbyte是一种专门用于ELT数据集成的平台,支持从API、数据库和文件到数据仓库和数据湖的管道搭建。其拥有最大规模的ELT连接器目录,支持众多的数据仓库和数据库。本文将介绍如何使用Airbyte的Shopify连接器加载Shopify对象作为文档。核心原理解析Airbyte的Shopify连接器作为一个文档加载器,通过API将Shopify的订单、产品等对象加载为文档。用户可以通 数据库事务四大基本要素 Hyhhuang 数据库mysql基础 数据库事务四大基本要素简介这四大基本要素也可以理解成一个事务的四个特点,分别是原子性、一致性、隔离性和耐久性,各自对应的英文是Atomicity、Consistency、Isolation、Durability,所以经常在一些文档或帖子中见到的ACID其实就是指它们。一致性一个事务可以封装状态改变(除非它是一个只读的)。事务必须始终保持系统处于一致的状态,不管在任何给定的时间并发事务有多少。比如, Java 9模块与Maven的深度结合 t0_54program javamavenpython个人开发 在Java9引入模块化之后,如何将模块化与Maven项目结合成为了许多开发者关注的焦点。本文将通过一个简单的示例,展示如何在Maven项目中开发Java9模块,并使用非模块化的外部库(如Jsoup)。1.Maven项目配置首先,我们需要创建一个Maven项目,并在pom.xml中配置相关的依赖和插件。以下是完整的pom.xml文件内容:4.0.0com.logicbig.examplejava9- 计算机毕业设计 ——jspssm508Springboot 的旅游管理 奔强的程序 课程设计旅游 博主小档案:花花,一名来自世界500强的资深程序猿,毕业于国内知名985高校。技术专长:花花在深度学习任务中展现出卓越的能力,包括但不限于java、python等技术。近年来,花花更是将触角延伸至AI领域,对于机器学习、自然语言处理、智能推荐等前沿技术都有独到的见解和实践经验。服务内容:1、提供科研入门辅导(主要是代码方面)2、代码部署3、定制化需求解决等4、期末考试复习计算机毕业设计——jsps 计算机毕业设计 ——jspssm510springboot 的人职匹配推荐系统 程序媛9688 课程设计 作者:程序媛9688开发技术:SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等。文末获取源码+数据库感兴趣的可以先收藏起来,还有大家在毕设选题(免费咨询指导选题),项目以及论文编写等相关问题都可以给我留言咨询,希望帮助更多的人计算机毕业设计——jspssm510springboot的人职匹配推荐系统人职匹配推荐系统技术说明本毕业设计项目“jsps 计算机毕业设计 ——jspssm513Springboot 的小区物业管理系统 程序媛9688 课程设计 作者:程序媛9688开发技术:SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等。文末获取源码+数据库感兴趣的可以先收藏起来,还有大家在毕设选题(免费咨询指导选题),项目以及论文编写等相关问题都可以给我留言咨询,希望帮助更多的人计算机毕业设计——jspssm513Springboot的小区物业管理系统技术说明:小区物业管理系统(基于JSP+SSM+ 计算机毕业设计 ——jspssm514Springboot 的校园新闻网站 程序媛9688 课程设计 作者:程序媛9688开发技术:SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等。文末获取源码+数据库感兴趣的可以先收藏起来,还有大家在毕设选题(免费咨询指导选题),项目以及论文编写等相关问题都可以给我留言咨询,希望帮助更多的人计算机毕业设计——jspssm514Springboot的校园新闻网站技术说明:JSPSSM514Springboot校园 计算机毕业设计 ——jspssm504springboot 职称评审管理系统 程序媛9688 课程设计 作者:程序媛9688开发技术:SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等。文末获取源码+数据库感兴趣的可以先收藏起来,还有大家在毕设选题(免费咨询指导选题),项目以及论文编写等相关问题都可以给我留言咨询,希望帮助更多的人计算机毕业设计——jspssm504springboot职称评审管理系统JSPSSM504SpringBoot职称评审管理 基于Spring Boot的驾校预约管理系统 超级无敌暴龙战士塔塔开 Java课设与毕设资源springbootjavamybatis 文章目录项目介绍主要功能截图:登录首页学员管理教练管理车辆管理关系管理车辆维修模块个人中心部分代码展示设计总结项目获取方式作者主页:Java韩立简介:Java领域优质创作者、简历模板、学习资料、面试题库【关注我,都给你】文末获取源码联系项目介绍基于SpringBoot的驾校预约管理系统(可帮忙远程调试),java项目。eclipse和idea都能打开运行。推荐环境配置:eclipse/ideajd w231乡政府管理系统设计与实现 栗子计算机毕业设计 javaspringboot后端javaspringtomcat 作者简介:多年一线开发工作经验,原创团队,分享技术代码帮助学生学习,独立完成自己的网站项目。代码可以查看文章末尾⬇️联系方式获取,记得注明来意哦~赠送计算机毕业设计600个选题excel文件,帮助大学选题。赠送开题报告模板,帮助书写开题报告。作者完整代码目录供你选择:《Springboot网站项目》400套《ssm网站项目》800套《小程序项目》300套《App项目》500套《Python网站项目 web报表工具FineReport常见的数据集报错错误代码和解释 老A不折腾 web报表finereport代码可视化工具 在使用finereport制作报表,若预览发生错误,很多朋友便手忙脚乱不知所措了,其实没什么,只要看懂报错代码和含义,可以很快的排除错误,这里我就分享一下finereport的数据集报错错误代码和解释,如果有说的不准确的地方,也请各位小伙伴纠正一下。 NS-war-remote=错误代码\:1117 压缩部署不支持远程设计 NS_LayerReport_MultiDs=错误代码 Java的WeakReference与WeakHashMap bylijinnan java弱引用 首先看看 WeakReference wiki 上 Weak reference 的一个例子: public class ReferenceTest { public static void main(String[] args) throws InterruptedException { WeakReference r = new Wea Linux——(hostname)主机名与ip的映射 eksliang linuxhostname 一、 什么是主机名 无论在局域网还是INTERNET上,每台主机都有一个IP地址,是为了区分此台主机和彼台主机,也就是说IP地址就是主机的门牌号。但IP地址不方便记忆,所以又有了域名。域名只是在公网(INtERNET)中存在,每个域名都对应一个IP地址,但一个IP地址可有对应多个域名。域名类型 linuxsir.org 这样的; 主机名是用于什么的呢? 答:在一个局域网中,每台机器都有一个主 oracle 常用技巧 18289753290 oracle常用技巧 ①复制表结构和数据 create table temp_clientloginUser as select distinct userid from tbusrtloginlog ②仅复制数据 如果表结构一样 insert into mytable select * &nb 使用c3p0数据库连接池时出现com.mchange.v2.resourcepool.TimeoutException 酷的飞上天空 exception 有一个线上环境使用的是c3p0数据库,为外部提供接口服务。最近访问压力增大后台tomcat的日志里面频繁出现 com.mchange.v2.resourcepool.TimeoutException: A client timed out while waiting to acquire a resource from com.mchange.v2.resourcepool.BasicResou IT系统分析师如何学习大数据 蓝儿唯美 大数据 我是一名从事大数据项目的IT系统分析师。在深入这个项目前需要了解些什么呢?学习大数据的最佳方法就是先从了解信息系统是如何工作着手,尤其是数据库和基础设施。同样在开始前还需要了解大数据工具,如Cloudera、Hadoop、Spark、Hive、Pig、Flume、Sqoop与Mesos。系 统分析师需要明白如何组织、管理和保护数据。在市面上有几十款数据管理产品可以用于管理数据。你的大数据数据库可能 spring学习——简介 a-john spring Spring是一个开源框架,是为了解决企业应用开发的复杂性而创建的。Spring使用基本的JavaBean来完成以前只能由EJB完成的事情。然而Spring的用途不仅限于服务器端的开发,从简单性,可测试性和松耦合的角度而言,任何Java应用都可以从Spring中受益。其主要特征是依赖注入、AOP、持久化、事务、SpringMVC以及Acegi Security 为了降低Java开发的复杂性, 自定义颜色的xml文件 aijuans xml <?xml version="1.0" encoding="utf-8"?> <resources> <color name="white">#FFFFFF</color> <color name="black">#000000</color> & 运营到底是做什么的? aoyouzi 运营到底是做什么的? 文章来源:夏叔叔(微信号:woshixiashushu),欢迎大家关注!很久没有动笔写点东西,近些日子,由于爱狗团产品上线,不断面试,经常会被问道一个问题。问:爱狗团的运营主要做什么?答:带着用户一起嗨。为什么是带着用户玩起来呢?究竟什么是运营?运营到底是做什么的?那么,我们先来回答一个更简单的问题——互联网公司对运营考核什么?以爱狗团为例,绝大部分的移动互联网公司,对运营部门的考核分为三块——用 js面向对象类和对象 百合不是茶 js面向对象函数创建类和对象 接触js已经有几个月了,但是对js的面向对象的一些概念根本就是模糊的,js是一种面向对象的语言 但又不像java一样有class,js不是严格的面向对象语言 ,js在java web开发的地位和java不相上下 ,其中web的数据的反馈现在主流的使用json,json的语法和js的类和属性的创建相似 下面介绍一些js的类和对象的创建的技术 一:类和对 web.xml之资源管理对象配置 resource-env-ref bijian1013 javaweb.xmlservlet resource-env-ref元素来指定对管理对象的servlet引用的声明,该对象与servlet环境中的资源相关联 <resource-env-ref> <resource-env-ref-name>资源名</resource-env-ref-name> <resource-env-ref-type>查找资源时返回的资源类 Create a composite component with a custom namespace sunjing https://weblogs.java.net/blog/mriem/archive/2013/11/22/jsf-tip-45-create-composite-component-custom-namespace When you developed a composite component the namespace you would be seeing would 【MongoDB学习笔记十二】Mongo副本集服务器角色之Arbiter bit1129 mongodb 一、复本集为什么要加入Arbiter这个角色 回答这个问题,要从复本集的存活条件和Aribter服务器的特性两方面来说。 什么是Artiber? An arbiter does not have a copy of data set and cannot become a primary. Replica sets may have arbiters to add a Javascript开发笔记 白糖_ JavaScript 获取iframe内的元素 通常我们使用window.frames["frameId"].document.getElementById("divId").innerHTML这样的形式来获取iframe内的元素,这种写法在IE、safari、chrome下都是通过的,唯独在fireforx下不通过。其实jquery的contents方法提供了对if Web浏览器Chrome打开一段时间后,运行alert无效 bozch Webchormealert无效 今天在开发的时候,突然间发现alert在chrome浏览器就没法弹出了,很是怪异。 试了试其他浏览器,发现都是没有问题的。 开始想以为是chorme浏览器有啥机制导致的,就开始尝试各种代码让alert出来。尝试结果是仍然没有显示出来。 这样开发的结果,如果客户在使用的时候没有提示,那会带来致命的体验。哎,没啥办法了 就关闭浏览器重启。 结果就好了,这也太怪异了。难道是cho 编程之美-高效地安排会议 图着色问题 贪心算法 bylijinnan 编程之美 import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Random; public class GraphColoringProblem { /**编程之美 高效地安排会议 图着色问题 贪心算法 * 假设要用很多个教室对一组 机器学习相关概念和开发工具 chenbowen00 算法matlab机器学习 基本概念: 机器学习(Machine Learning, ML)是一门多领域交叉学科,涉及概率论、统计学、逼近论、凸分析、算法复杂度理论等多门学科。专门研究计算机怎样模拟或实现人类的学习行为,以获取新的知识或技能,重新组织已有的知识结构使之不断改善自身的性能。 它是人工智能的核心,是使计算机具有智能的根本途径,其应用遍及人工智能的各个领域,它主要使用归纳、综合而不是演绎。 开发工具 M [宇宙经济学]关于在太空建立永久定居点的可能性 comsci 经济 大家都知道,地球上的房地产都比较昂贵,而且土地证经常会因为新的政府的意志而变幻文本格式........ 所以,在地球议会尚不具有在太空行使法律和权力的力量之前,我们外太阳系统的友好联盟可以考虑在地月系的某些引力平衡点上面,修建规模较大的定居点 oracle 11g database control 证书错误 daizj oracle证书错误oracle 11G 安装 oracle 11g database control 证书错误 win7 安装完oracle11后打开 Database control 后,会打开em管理页面,提示证书错误,点“继续浏览此网站”,还是会继续停留在证书错误页面 解决办法: 是 KB2661254 这个更新补丁引起的,它限制了 RSA 密钥位长度少于 1024 位的证书的使用。具体可以看微软官方公告: Java I/O之用FilenameFilter实现根据文件扩展名删除文件 游其是你 FilenameFilter 在Java中,你可以通过实现FilenameFilter类并重写accept(File dir, String name) 方法实现文件过滤功能。 在这个例子中,我们向你展示在“c:\\folder”路径下列出所有“.txt”格式的文件并删除。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 C语言数组的简单以及一维数组的简单排序算法示例,二维数组简单示例 dcj3sjt126com carray # include <stdio.h> int main(void) { int a[5] = {1, 2, 3, 4, 5}; //a 是数组的名字 5是表示数组元素的个数,并且这五个元素分别用a[0], a[1]...a[4] int i; for (i=0; i<5; ++i) printf("%d\n", PRIMARY, INDEX, UNIQUE 这3种是一类 PRIMARY 主键。 就是 唯一 且 不能为空。 INDEX 索引,普通的 UNIQUE 唯一索引 dcj3sjt126com primary PRIMARY, INDEX, UNIQUE 这3种是一类PRIMARY 主键。 就是 唯一 且 不能为空。INDEX 索引,普通的UNIQUE 唯一索引。 不允许有重复。FULLTEXT 是全文索引,用于在一篇文章中,检索文本信息的。举个例子来说,比如你在为某商场做一个会员卡的系统。这个系统有一个会员表有下列字段:会员编号 INT会员姓名 java集合辅助类 Collections、Arrays shuizhaosi888 CollectionsArraysHashCode Arrays、Collections 1 )数组集合之间转换 public static <T> List<T> asList(T... a) { return new ArrayList<>(a); } a)Arrays.asL Spring Security(10)——退出登录logout 234390216 logoutSpring Security退出登录logout-urlLogoutFilter 要实现退出登录的功能我们需要在http元素下定义logout元素,这样Spring Security将自动为我们添加用于处理退出登录的过滤器LogoutFilter到FilterChain。当我们指定了http元素的auto-config属性为true时logout定义是会自动配置的,此时我们默认退出登录的URL为“/j_spring_secu 透过源码学前端 之 Backbone 三 Model 逐行分析JS源代码 backbone源码分析js学习 Backbone 分析第三部分 Model 概述: Model 提供了数据存储,将数据以JSON的形式保存在 Model的 attributes里, 但重点功能在于其提供了一套功能强大,使用简单的存、取、删、改数据方法,并在不同的操作里加了相应的监听事件, 如每次修改添加里都会触发 change,这在据模型变动来修改视图时很常用,并且与collection建立了关联。 SpringMVC源码总结(七)mvc:annotation-driven中的HttpMessageConverter 乒乓狂魔 springMVC 这一篇文章主要介绍下HttpMessageConverter整个注册过程包含自定义的HttpMessageConverter,然后对一些HttpMessageConverter进行具体介绍。 HttpMessageConverter接口介绍: public interface HttpMessageConverter<T> { /** * Indicate 分布式基础知识和算法理论 bluky999 算法zookeeper分布式一致性哈希paxos 分布式基础知识和算法理论 BY NODEXY@2014.8.12 本文永久链接:http://nodex.iteye.com/blog/2103218 在大数据的背景下,不管是做存储,做搜索,做数据分析,或者做产品或服务本身,面向互联网和移动互联网用户,已经不可避免地要面对分布式环境。笔者在此收录一些分布式相关的基础知识和算法理论介绍,在完善自我知识体系的同 Android Studio的.gitignore以及gitignore无效的解决 bell0901 androidgitignore github上.gitignore模板合集,里面有各种.gitignore : https://github.com/github/gitignore 自己用的Android Studio下项目的.gitignore文件,对github上的android.gitignore添加了 # OSX files //mac os下 .DS_Store 成为高级程序员的10个步骤 tomcat_oracle 编程 What 软件工程师的职业生涯要历经以下几个阶段:初级、中级,最后才是高级。这篇文章主要是讲如何通过 10 个步骤助你成为一名高级软件工程师。 Why 得到更多的报酬!因为你的薪水会随着你水平的提高而增加 提升你的职业生涯。成为了高级软件工程师之后,就可以朝着架构师、团队负责人、CTO 等职位前进 历经更大的挑战。随着你的成长,各种影响力也会提高。 mongdb在linux下的安装 xtuhcy mongodblinux 一、查询linux版本号: lsb_release -a LSB Version: :base-4.0-amd64:base-4.0-noarch:core-4.0-amd64:core-4.0-noarch:graphics-4.0-amd64:graphics-4.0-noarch:printing-4.0-amd64:printing-4.0-noa 按字母分类: ABCDEFGHIJKLMNOPQRSTUVWXYZ其他
Debug中方法注解和chain,正对应着我们加入的两个拦截器:
到这一步,说明AOP成功感知到了调用方法需要事务和数据源切换的增强,并将根据拦截器链挨个处理;执行的顺序是先切数据源、再开启事务,如此合理也自然不会是巧合,肯定是框架内部指定了Order之类的属性,至于如何控制优先级就不展开讨论了。
按照顺序,首先要处理@DS对应的DynamicDataSourceAnnotationInterceptor。
invoke()方法的入参为this当前对象,即我们上面说到的所有东西都传过来了,拦截器首先要看看准备往哪个数据源切;这一步逻辑也很简单,就是去方法签名里找DS注解对应的value,比较特别的是他内部维护了一个dsCache,是个放了所有方法使用数据源名称字符串的ConcurrentHashmap,每次先去dsCache查,有就返回没有就查了放进去供下次使用。
//DataSourceClassResolver处理切换数据源名称逻辑 public String findKey(Method method, Object targetObject) { if (method.getDeclaringClass() == Object.class) { return ""; } else { //根据方法签名获取数据源名称 //dsCache为本地Map缓存,查询过的会缓存下来避免重复查询 Object cacheKey = new MethodClassKey(method, targetObject.getClass()); String ds = (String)this.dsCache.get(cacheKey); if (ds == null) { ds = this.computeDatasource(method, targetObject); if (ds == null) { ds = ""; } this.dsCache.put(cacheKey, ds); } return ds; } }
获取到数据源名称后,关键的一步出现了!调用了工具类DynamicDataSourceContextHolder
并将数据源名称push了进去,push进了一个怎样的容器,这点是至关重要的,我们来具体看看。
private static final ThreadLocal> LOOKUP_KEY_HOLDER = new NamedThreadLocal>("dynamic-datasource") { protected Deque initialValue() { return new ArrayDeque(); } }; public static String push(String ds) { String dataSourceStr = StringUtils.isEmpty(ds) ? "" : ds; ((Deque)LOOKUP_KEY_HOLDER.get()).push(dataSourceStr); return dataSourceStr; }
可以看到线程绑定变量ThreadLocal里放了一个Deque,Deque是一个双端队列,可以当作栈用也可以当作队列用。实例化时使用了ArrayDeque,因此在此处将其用作了先进先出的队列,其push方法底层调用了addFirst(),将数据放在了队列头。
放完后DS拦截器的活儿基本也干完了,于是继续向下调用拦截器,因为拦截器是链式调用嘛,在所有链路走完前肯定不能断,因此就又回到ReflectiveMethodInvocation的proceed方法,处理下一个拦截器。
TransactionInterceptor的invoke()方法只做了一件事,将方法签名、被代理类、反射执行器的proceed方法引用,传递给真正开启并处理事务的TransactionAspectSupport。可能会有疑惑的是这个“proceed方法引用”,其实也很简单,上一步DS拦截器最后一步调用了proceed方法,来继续向下处理拦截器链;这里也是同理,将方法引用传进去,当前拦截器处理完毕后再回到ReflectiveMethodInvocation向下处理。
之后便进入TransactionAspectSupport的invokeWithinTransaction()方法处理事务,我们来依次看看他都做了些什么。
先看看最前面,在学习编程式事务时大家都清楚,我们要获取事务属性TransactionDefinition、将其传入TransactionManager构建事务管理器并创建事务;这里的逻辑也是类似的,
// If the transaction attribute is null, the method is non-transactional. TransactionAttributeSource tas = getTransactionAttributeSource(); final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null); final PlatformTransactionManager tm = determineTransactionManager(txAttr); final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
原作者的注释也写得很清楚:如果事务属性为null,说明为非事务方法。这一点也是贯穿整个流程的,很多地方都是基于是否开启事务进行逻辑区分的,就比如第2行TransactionAttribute的获取,事务属性不为null才去获取他。TransactionAttribute其实就是TransactionDefinition的子类,猫叫咪咪而已。
这几行执行完以后,就有了事务属性、事务管理器以及连接点描述(就是方法名称,根据Method方法签名获取,但内部逻辑有点没看懂),接下来要根据这些信息进行事务逻辑处理。
先看第一个if分支,如果没有事务属性,或者事务管理器不是CallbackPreferringPlatformTransactionManager
事务管理器的实例,就会进入这部分逻辑。至于这个特定的事务管理器,网上没有搜到很明确的资料,但既然他叫回调、且else逻辑里也是以回调形式调用,姑且就认为他是回调事务管理器。
//没有事务属性(也就是没有事务)时,会进入这段逻辑 if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) { // Standard transaction demarcation with getTransaction and commit/rollback calls. TransactionInfo txInfo = createTransactionIfNecessary(tm, 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); } commitTransactionAfterReturning(txInfo); return retVal; }
我们传入了事务属性,也使用的是JDBC的DataSourceTransactionManager,因此会进入if逻辑。有意义的是createTransactionIfNecessary()方法,返回值是TransactionInfo也就是TransactionStatus的子类,进去看看里面的处理逻辑:
// 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);
方法里将事务属性包装成了一个代理类,将事务名称定义为了方法名,之后便是喜闻乐见的用事务管理器开启事务getTransaction()。
Object transaction = doGetTransaction(); //JDBC实现类 DataSourceTransactionManager protected Object doGetTransaction() { DataSourceTransactionManager.DataSourceTransactionObject txObject = new DataSourceTransactionManager.DataSourceTransactionObject(); txObject.setSavepointAllowed(this.isNestedTransactionAllowed()); ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(this.obtainDataSource()); txObject.setConnectionHolder(conHolder, false); return txObject; }
先调用doGetTransaction()创建一个对象txObject,再将当前事务的一些信息封装进去。比较有意思的是获取ConnectionHolder,会先调用obtainDataSource()获取当前数据源集合,此时获取到的是:
可不要以为是原生DataSource哦,在项目启动时DynamicRoutingDataSource就作为DataSource实现类被注入,且里面放置了所有已经初始化的数据源。最后ConnectionHolder为null,因为事务是刚刚开启的;而是否持有连接与是否开启新事务有着很大联系,单个事物只能对应单个连接,要切换就必须开启新的事务并重新获取连接,这点在后面的handleExistingTransaction()方法逻辑里就有体现。
此时我们的事务管理器还空空的,没办法,刚开启事务,啥也拿不到。所以下面的逻辑都不会走。
if (definition == null) { // Use defaults if no transaction definition given. definition = new DefaultTransactionDefinition(); } //根据是否持有连接/事务是否活跃 判断当前是否有事务在处理中 //有的话调用handleExistingTransaction(),根据事务传播属性处理新旧事务切换等 if (isExistingTransaction(transaction)) { // Existing transaction found -> check propagation behavior to find out how to behave. return handleExistingTransaction(definition, transaction, debugEnabled); } // Check definition settings for new transaction. if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) { throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout()); }
下面是一长串判断,根据事务传播属性分别处理事务,我们使用了默认的REQUIRED,因此看看这段逻辑。
SuspendedResourcesHolder suspendedResources = suspend(null); if (debugEnabled) { logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition); } try { boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); DefaultTransactionStatus status = newTransactionStatus( definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); doBegin(transaction, definition); prepareSynchronization(status, definition); return status; }
suspend()方法就不细看了,里面是TransactionSynchronizationManager相关的处理逻辑。他是线程同步事务管理器,可以实现类似CompletableFuture.whenComplete()的功能,意为这个事务执行完以后、异步事务再开始执行,可以避免主事务插入的数据没commit,异步事务想查又查不到的尴尬情景。
之后初始化了一个TransactionStatus,里面放了一些事务相关的基本属性,然后调用doBegin()方法开启事务。这一步是最重要的,前面铺垫了那么久好像一直没有数据源什么事,其实就是为了这一刻,咱们来一行一行看。
DataSourceTransactionManager.DataSourceTransactionObject txObject = (DataSourceTransactionManager.DataSourceTransactionObject)transaction; Connection con = null; if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) { Connection newCon = this.obtainDataSource().getConnection(); txObject.setConnectionHolder(new ConnectionHolder(newCon), true); }
先初始化几个属性,然后判断事务管理器是否持有连接,没有就进入if逻辑。前文提到obtainDataSource()获取当前数据源集合,获取到了DynamicRoutingDataSource,那再看看getConnection()干了点啥。
public Connection getConnection() throws SQLException { //获取全局事务号 String xid = TransactionContext.getXID(); //根据是否有事务号判断分支 if (StringUtils.isEmpty(xid)) { return this.determineDataSource().getConnection(); } else { String ds = DynamicDataSourceContextHolder.peek(); ds = StringUtils.isEmpty(ds) ? "default" : ds; ConnectionProxy connection = ConnectionFactory.getConnection(ds); return (Connection)(connection == null ? this.getConnectionProxy(ds, this.determineDataSource().getConnection()) : connection); } }
因为我们的事务号为null,因此要先调用determineDataSource()。
public DataSource determineDataSource() { String dsKey = DynamicDataSourceContextHolder.peek(); return this.getDataSource(dsKey); }
还记得这个DynamicDataSourceContextHolder吗?正是DS拦截器之前操作的动态数据源中,ThreadLocal里的双端队列Deque!调用Deque的peek()方法,从队列里拿到数据源名称后,又到服务启动时注册的数据源ConcurrentHashMap,拿到了我们所需的DataSource对象!
至此,我逐渐理解一切。
之后就是数据库连接池Druid的活儿了,他去池子里拿到了池化Connection;再回到事务管理器,将连接做成连接持有设置给事务管理器。到这里,我们的事务管理器终于不是光秃秃的了,他有了沉甸甸的数据库连接。
再回到一开始的抽象事务管理器类中,现在的TransactionStatus可谓是应有尽有。
将这些东西全部传入prepareTransactionInfo()方法,包括准备好的事务管理器、事务属性、方法名称、事务状态。
//所有信息封装成一个TransactionInfo对象 TransactionInfo txInfo = new TransactionInfo(tm, txAttr, joinpointIdentification); if (txAttr != null) { //绑定进TransactionInfo对象 txInfo.newTransactionStatus(status); } else { //打日志,啥也没干 } //将对象绑定至当前线程 txInfo.bindToThread(); return txInfo;
方法的逻辑非常简单,精简了一下就是上面的内容。创建了一个大对象,把所有事务相关的信息全部封装进去,然后绑定到当前线程。提到当前线程,猜也猜得到又是一个ThreadLocal,用一个变量先将旧事务存了起来,又将新事务塞进ThreadLocal,这也就实现了多事务的挂起和切换。
事务和数据源拦截器全整完了,之后就去找下一个进行处理,而我们的拦截器链已经全部走完了,因此就可以愉快地回到Service方法里处理SQL了。
相信看完上面源码解析的朋友应该懂为什么会导致切换失败了,我们再从源码层面分析之前举例的几种情况。
前文的分析表明,加上@DS会给ThreadLocal中的Deque加入指定数据源名称,如果我们不加,这个Deque就会是空的,这点肯定没有异议。
此时的dsKey为null,给getDataSource()方法传入null会发生什么呢?
public DataSource getDataSource(String ds) { if (StringUtils.isEmpty(ds)) { return this.determinePrimaryDataSource(); } else if (!this.groupDataSources.isEmpty() && this.groupDataSources.containsKey(ds)) { log.debug("dynamic-datasource switch to the datasource named [{}]", ds); return ((GroupDataSource)this.groupDataSources.get(ds)).determineDataSource(); } else if (this.dataSourceMap.containsKey(ds)) { log.debug("dynamic-datasource switch to the datasource named [{}]", ds); return (DataSource)this.dataSourceMap.get(ds); } else if (this.strict) { throw new CannotFindDataSourceException("dynamic-datasource could not find a datasource named" + ds); } else { return this.determinePrimaryDataSource(); } }
没错,正如大家所想,为空就会去取primary主数据源。因此建议大家无论如何都要配置primary属性,默认为“master”,如果你没有master数据源就会直接报错。这也就解释了为什么在不指定数据源时,事务开启会直接使用主数据源。
不加事务时,执行到对应的SQL时会先获取对应数据源,如果加@DS就获取指定数据源、不加就使用主数据源。这也是最常用的用法,不加事务时想怎么切怎么切。
加上事务可就不一样了,在方法开启事务的一瞬间,就会走我们上面所说的那一套逻辑,而这种又分两种情况。
一种是开启事务时就指定数据源。由于事务开启以前会先处理@DS拦截器逻辑,处理完数据源队列中就摆好了需要使用的数据源;此时事务拦截器去队列里取,此时队列不为空,直接就能取到所需的数据源,所以可以切换成功。
另一种是开启事务后,在内部调用的Mapper再指定数据源。看这个执行顺序就很明显,在开启事务时,数据源队列是空的,一peek发现为空就只能取主数据源;你里面的Mapper哪怕切一晚上,也只是加入了队列里,但无人在意。
想要成功切换数据源一定要保证这几点: