笔者之前就事务和动态数据源之间的问题,做过一些探讨和总结(详见事务内动态数据源切换失效及传播属性)。后来从源码层面分析有了一些收获,篇幅较长不便写在原文中,因此另起一篇作为补充。
注意,上篇的实验和结论依旧有效,本文旨在将理论和实践结合,给出更精确的解决方案及底层原理。
先上结论,帮助同样被这个问题困扰的朋友们。
首先明确,事务控制得最小单位为“同一个数据库连接”,即想要正常控制事务回滚提交,那么整个过程只能基于一个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) PostgreSQL Like查询与正则表达式 西召 PostgreSQLPostgreSQLlike模糊查询 安装PostgreSQL之后,PostgreSQL会创建一个名为“postgres”的用户,创建一个名为“postgres”的数据库。我们就可以使用这个默认的库做实验。首先建表并插入数据:CREATETABLEpublic.user(IDSERIALPRIMARYKEYNOTNULL,UserIDvarchar(100)NOTNULL,UserNamevarchar(100)NOTNULL,Pho Python 爬虫流程及robots协议介绍 流沙丶 Python项目爬虫实战 Python爬虫流程及robots协议介绍**网络爬虫(Spider)是一种高效的数据挖掘的方式,常见的百度,谷歌,火狐等浏览器,其实就是一个非常大的爬虫项目**爬虫大致分为了四个阶段:确定目标:我们想要爬取的网页数据采集:已经爬取到的HTML数据数据提取:从HTML中提取我们想要的数据数据存储:将提取出来的数据保存在数据库,保存成JSON文件等robots协议:用简单直接的txt格式文本方式告诉 Webpack、Vite区别知多少? m0_74824044 面试学习路线阿里巴巴webpack前端node.js 前端的项目打包,我们常用的构建工具有Webpack和Vite,那么Webpack和Vite是两种不同的前端构建工具,那么你们又是否了解它们的区别呢?我们在做项目时要如何选择呢?一、工具定义1、Webpack:是一个强大的静态模块打包工具,它可以将各种类型的文件,如JavaScript、CSS、图片等,作为模块进行打包,并生成最终的静态资源文件。Webpack使用各种loader和plugin来处理 力扣热题 100:哈希专题三道题详细解析(JAVA) 剑走偏锋o.O leetcode哈希算法java 文章目录一、两数之和1.题目描述2.示例3.解题思路4.代码实现(Java)5.复杂度分析二、字母异位词分组1.题目描述2.示例3.解题思路4.代码实现(Java)5.复杂度分析三、最长连续序列1.题目描述2.示例3.解题思路4.代码实现(Java)5.复杂度分析在力扣(LeetCode)平台上,热题100是许多开发者提升算法能力的必刷清单。今天,我们就来详细解析热题100中与哈希相关的三道题,帮 JAVA命名规则,开发命名规范 北风toto 次要的个人笔记java开发语言 1、JAVA常用的开发规范应用项目名称:字母全部小写,下划线隔开,例子"my_project_name"变量名:小驼峰规则(小写字母开头),例子"userCount"类命名:大驼峰规则(大写字母开头),例子"UserAccount",但是有例外的"UserDTO",“UserVO”,后缀要全为大写才是规范写法缩进:4个空格微服务设计满足单向依赖原则2、Vue常用的开发规范应用项目名称:字母全部小写 3万字长文:SQL Server面试题和参考答案(持续更新) 大模型大数据攻城狮 oracle数据库大数据面试求职 目录解释关系数据库中的主键、外键、超键和候选键的区别。描述SQLServer中聚集索引和非聚集索引的不同。什么是事务?SQLServer中如何保证事务的ACID属性?什么是视图?在SQLServer中视图有什么用途?解释SQLServer中的锁机制以及死锁是如何产生的。如何优化SQL查询以提高性能?写出SQL语句:取出表A中第31到第40记录。解释SQLServer中的临时表和表变量的区别。什么是 验证码介绍及生成与验证(HTML + JavaScript实现) 学习&实践爱好者 JavaScrip技术HTML5与CSS3编程实践系列javascript 验证码介绍及生成与验证(HTML+JavaScript实现)验证码验证码(全自动区分计算机和人类的图灵测试,CAPTCHA,CompletelyAutomatedPublicTuringtesttotellComputersandHumansApart)是一种用于区分用户是人类还是自动化程序的安全机制,通过生成人类易识别、机器难破解的测试题目实现身份验证。作用防止恶 Milvus高性能向量数据库与大模型结合 山塘小鱼儿 数据库阿里云向量数据库 Milvus|高性能向量数据库,为规模而构建Milvus是一个为GenAI应用构建的开源向量数据库。使用pip安装,执行高速搜索,并扩展到数十亿个向量。https://milvus.io/zhMilvus是什么?Milvus是一种高性能、高扩展性的向量数据库,可在从笔记本电脑到大规模分布式系统等各种环境中高效运行。它既可以开源软件的形式提供,也可以云服务的形式提供。Milvus是LFAI&Data 【数据结构】哈希表 alien爱吃蛋挞 数据结构Java深入Java接口与类数据结构java 目录哈希表基本思想基本原理哈希表工作机制简化描述关于查找、插入和删除HashMap主要成员变量主要方法内部实现细节注意事项哈希表哈希表是一种基于哈希函数的数据结构,它通过键值对的形式存储数据,并允许通过键快速查找对应的值。Java中的哈希表主要通过HashMap类来实现,它是java.util包的一部分。基本思想使用一个数组(table数组)来存放数据,但每个数组位置(也称为槽位或桶)不仅仅存放一 当进行npm install指令,安装依赖的情况下,存在如下报错 舒克日记 javanpm前端node.js 当进行npminstall指令,安装依赖的情况下,存在如下报错D:\ssmprogramcode\springboot8i5qd7np\src\main\resources\front\front>npminstallnpmERR!codeENOTFOUNDnpmERR!errnoENOTFOUNDnpmERR!networkrequesttohttps://registry.nlark.com/ Flutter系列教程之(2)——Dart语言快速入门 l软件定制开发工作室 Flutter教程flutter 目录1.变量与类型1.1num类型1.2String类型1.3Object与Dynamic1.4类型判断/转换1.5变量和常量2.方法/函数3.类、接口、抽象类3.1类3.2接口4.集合4.1List4.2Set4.3Map5.总结Dart语言的语法和Kotlin、Java有类似之处,这里就通过对比Java和Kotlin来快速入门Dart语言1.变量与类型1.1num类型基础常见的类型有num(分 Python开发 yzx991013 开发语言深度学习人工智能python Python开发工程师-职责:设计、开发与维护高效、稳定、安全的Python应用程序及服务;与跨职能团队合作,理解业务需求并转化为技术方案;编写高质量、可重用、易维护的代码,包含单元测试与文档;解决系统和应用程序的技术问题,进行故障排除与优化。-要求:熟练掌握Python编程语言,有丰富开发经验;熟悉常用Python开发框架与库;熟悉Web开发技术和数据库设计优化;掌握版本控制系统,有团队协作经验 Puppeteer.js 一个可以不动手操作浏览的插件 海上彼尚 node.jsjavascript开发语言ecmascript 目录Puppeteer.js简介安装Puppeteer基础使用1.启动浏览器并打开页面2.核心API详解浏览器对象(Browser)页面对象(Page)选择器常见用例用例1:生成网页截图用例2:生成PDF用例3:提交表单用例4:等待元素加载用例5:模拟移动设备高级功能1.拦截网络请求2.执行自定义JavaScript3.处理弹窗4.下载文件最佳实践总结Puppeteer.js简介Puppeteer 将Javascript打包成exe可执行文件 海上彼尚 node.jsjavascript开发语言node.js 目录什么是pkg?安装pkg基本使用步骤1:准备你的Node.js项目步骤2:通过命令行打包步骤3:运行生成的可执行文件配置package.json处理资源文件高级用法指定Node.js版本和平台打包整个项目处理环境变量常见问题问题1:动态导入模块失败问题2:文件路径错误问题3:缺少依赖示例:打包Express应用总结什么是pkg?pkg是一个命令行工具,能将Node.js项目及其依赖打包成一个单 Spring Cloud Alibaba与Spring Boot、Spring Cloud版本对应关系 web13093320398 面试学习路线阿里巴巴springbootspringcloudjava 一、前言在搭建SpringCloud项目环境架构的时候,需要选择SpringBoot和SpringCloud进行兼容的版本号,因此对于选择SpringBoot版本与SpringCloud版本的对应关系很重要,如果版本关系不对应,常见的会遇见项目启动不起来,怪异的则会是你的项目出现一些诡异的问题,查资料也不好查。下面就收集一下SpringBoot与SpringCloud版本之间的对应关系,在搭建项目 Spring注解驱动开发之@Autowired 从不吃红薯 基于注解管理Beanspringjava后端 一、@Autowired注解概述1.1核心作用@Autowired是Spring框架中实现依赖注入的核心注解,采用类型优先的自动装配策略。其设计初衷是为了简化XML配置,实现更优雅的组件管理。1.2源码解析@Target({ElementType.CONSTRUCTOR,ElementType.METHOD,ElementType.PARAMETER,ElementType.FIELD,Eleme 一个游戏程序员的学习资料【转载】 Snail -Bernoulli 游戏程序员游戏程序员成长路线 想起写这篇文章是在看侯杰先生的《深入浅出MFC》时,突然觉得自己在大学这几年关于游戏编程方面还算是有些心得,因此写出这篇小文,介绍我眼中的游戏程序员的书单与源代码参考。一则是作为自己今后两年学习目标的备忘录,二来没准对别人也有点参考价值。我的原则是只写自己研究过或准备研究的资料,所以内容无疑会带上强烈的个人喜好色彩,比如对网络,数据库等重要方面完全没有涉及。因为自己主要对三维图形引擎,人工智能算法 java毕业设计家教管理系统mybatis+源码+调试部署+系统+数据库+lw 练练科技 数据库javamybatis java毕业设计家教管理系统mybatis+源码+调试部署+系统+数据库+lwjava毕业设计家教管理系统mybatis+源码+调试部署+系统+数据库+lw本源码技术栈:项目架构:B/S架构开发语言:Java语言开发软件:ideaeclipse前端技术:Layui、HTML、CSS、JS、JQuery等技术后端技术:JAVA运行环境:Win10、JDK1.8数据库:MySQL5.7/8.0源码地址 数据库的表操作以及单表查询、慢查询的优化思路 后面有命令和格式的总结 passion_flower_ mysql 表操作表是数据库存储数据的基本单位,由若干个字段组成,主要用来存储数据记录。使用编辑器编辑指令mysql>editmysql>\e在mysql客户端内执行系统命令mysql>systemlsmysql>\!ls创建表表名:school.student1字段名称idnamesexage记录1tommale23记录2jackmale21记录3alicefemale19语法mysql>createtab JVM详解:内存管理与类加载机制 猿享天开 Java开发从入门到精通jvm内存管理java JVM详解:内存管理与类加载机制JVM(JavaVirtualMachine)是Java生态的基石,理解其内存管理和类加载机制是掌握Java核心技术的关键。以下从底层实现原理到优化策略进行全面剖析。一、JVM内存管理体系1.内存区域划分JVM内存区域线程共享区线程私有区HeapMethodArea运行时常量池VMStackNativeStackPCRegister2.核心区域详解内存区域存储内容配 java毕业生设计宠物领养管理系统计算机源码+系统+mysql+调试部署+lw 你眼里的星星 javamysql宠物 java毕业生设计宠物领养管理系统计算机源码+系统+mysql+调试部署+lwjava毕业生设计宠物领养管理系统计算机源码+系统+mysql+调试部署+lw本源码技术栈:项目架构:B/S架构开发语言:Java语言开发软件:ideaeclipse前端技术:Layui、HTML、CSS、JS、JQuery等技术后端技术:JAVA运行环境:Win10、JDK1.8数据库:MySQL5.7/8.0源码地址 Spring Boot项目@Cacheable注解的使用 m0_54851477 面试学习路线阿里巴巴springbootspringjava @Cacheable是Spring框架中用于缓存的注解之一,它可以帮助你轻松地将方法的结果缓存起来,从而提高应用的性能。下面详细介绍如何使用@Cacheable注解以及相关的配置和注意事项。1.基本用法1.1添加依赖首先,确保你的项目中包含了SpringCache的依赖。如果你使用的是SpringBoot,可以在pom.xml或build.gradle中添加以下依赖:Maven:org.sprin 太强了!测试 Claude 3.7 Sonnet 模型前端代码能力提示词及Claude Code 系统提示词 技术程序猿华锋 AIGC资讯前端人工智能 Claude3.7Sonnet测试模型的前端代码能力提示词;CreateasingleHTMLfilecontainingCSSandJavaScripttogenerateananimatedweathercard.Thecardshouldvisuallyrepresentthefollowingweatherconditionswithdistinctanimations:Wind:(e.g 【Java基础篇】——第2篇:Java语法基础 猿享天开 Java开发从入门到精通java开发语言 第2篇:Java语法基础2.1引言在上一篇中,我们介绍了Java的基本概念、应用场景以及如何搭建开发环境。本篇将深入探讨Java的语法基础,涵盖变量与数据类型、运算符、控制结构、数组、方法、面向对象编程的进一步内容、异常处理以及常用的编程规范。通过本篇内容,读者将能够编写基本的Java程序,理解其核心语法结构。2.2Java的基本组成Java程序由以下几个基本组成部分构成:包(Package):用 学习Java:全面解析Java方法(Methods) 小志开发 JAVAjava 在学习Java编程语言的过程中,**方法(Method)**是一个非常重要的概念。方法是面向对象编程(OOP)的核心之一,也是编写复杂程序的基础工具。掌握方法的概念和使用方法能够极大提高代码的复用性和可维护性。1.方法的基本概念定义在Java中,方法是类中的一个逻辑单元,用于定义特定的操作或功能。通过调用方法,可以在不影响变量状态的情况下,实现代码的模块化和复用。以下是一些关于方法的基本知识点:方 SpringCloud微服务实战——搭建企业级开发框架(二十四):集成行为验证码和图片验证码实现登录功能 全栈程序猿 MavenSpringCloudspringcloud微服务java 随着近几年技术的发展,人们对于系统安全性和用户体验的要求越来越高,大多数网站系统都逐渐采用行为验证码来代替图片验证码。行为验证码指的是通过用户行为来验证用户身份的验证码,如滑动拼图、识别图片中的特定物品等。 行为验证码的重要性在于可以有效地防止机器人和恶意程序对网站或应用程序进行恶意攻击、刷流量、撞库等行为,从而保障了用户和网站的安全。相较于传统的图形验证码等方式,行为验证码更难被破解,同时 <a>标签中href=“javascript:;“ Alkaid: JSjavascript javascript:是表示在触发默认动作时,执行一段JavaScript代码,而javascript:;表示什么都不执行,跟href=”javascript:void(0)”是一样的,void是JavaScript的一个运算符,void(0)就是什么都不做的意思,这样点击时就没有任何反应,页面就不会跳转。 【Java】@JsonFormat和@DateTimeFormat注解区别 Alkaid: SpringBootJavajava 注解@JsonFormat主要是后端到前端的时间格式的转换注解@DateTimeFormat主要是前端到后端的时间格式的转换 Spring Boot 整合 RabbitMQ 详解 码农爱java 【RabbitMQ】java-rabbitmqspringbootrabbitmq消息中间件MQ实战 前言:在消息中间件领域中RabbitMQ也是一种非常常见的消息中间件了,本篇简单分享一下SpringBoot项目集成RabbitMQ的过程。RabbitMQ系列文章传送门RabbitMQ的介绍及核心概念讲解@RabbitListener注解详解SpringBoot集成RabbitMQ可以分为三大步,如下:在proerties或者yml文件中添加RabbitMQ配置。项目pom.xml文件中引入sp python mongodb连接池_mongoDB中的连接池(转载) weixin_39989949 pythonmongodb连接池 一.mongoDB中的连接池刚上手MongoDB,在做应用时,受以前使用关系型数据库的影响,会考虑数据库连接池的问题!关系型数据库中,我们做连接池无非就是事先建立好N个连接(connection),并构建成一个连接池(connectionpool),提供去连接和归还连接等操作。而在MongoDB中,我们先来看看怎么进行操作,以insert为例:Mongom=newMongo("localhost" 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哪怕切一晚上,也只是加入了队列里,但无人在意。
想要成功切换数据源一定要保证这几点: