- connection holder is null
- 异常背景
- 异常特点
- 初步分析
- 深度分析
- druid连接池源码追踪
- druid连接池参数
- 连接池的初始化方法
- 连接摧毁过程
- 回收超时方法
- 异常出现可能原因
- spring事务管理原理
- 事务实现方式
- TransactionTemplate事务模板
- 事务模板execute方法解析
- spring事务核心类图
- getTransaction获取事务源码分析
- doGetTransaction
- getResource
- spring事务缓存数据源连接
- 新建事务dobegin
- 源码追踪总结
- 经验总结
connection holder is null
- 第一次发生是在圣诞节加班冒烟自测需求时曾发生过该异常,当时排查过可能是由于某个地方事务过长造成的,恰好我又在冒烟新增的接口,就去看了一遍,发现确实方法链路较长,且整个接口都处于事务中,我便将需要事务的逻辑单独抽出,重新测了一遍,发现该异常没有发生了。便不了了之。
- 第二次是在上线当天,测试环境出现大量异常,并导致接口成功率降至30%,排查无果,最后觉得是自动化测试调用频繁的原因,解决后便没有出现异常,开始上线。
- 由于上线当天出现了其他的bug,注意力全部都集中在bug排查修复上,凌晨四点半又开始发生异常,便开始重新review此次版本新增代码,发现了XX编辑接口,手动开启事务后紧接着抛出异常,觉得可能是该原因,不过该接口只调用了一次,想不通为什么会造成几十几百次异常。
- 在测试环境重复点击该异常(由于点击频率过高,200个事务堵塞了200条tomcat线程,服务器直接503)
- 排查进度缓慢,也快6点,客户也快要上班了,便先回滚代码,后续再说,先初步怀疑是这个XX编辑手动开启事务的坑。
- 隐蔽性强
- 触发点不明确
- 复现难度高
看看本次异常先是由哪里抛出
DruidPooledConnection是一个静态代理,持有ConnectionHolder, connection Holder里持有具体的connection对象, 可以看到在数据源连接在执行druidPooledConnection的所有和数据库相关方法时,都会先调用checkState()判断connection holder是否为null,如果是null就抛connection holder is null的异常。
holder是怎么被置为null的?
走一遍durid连接池源码就清楚了
深度分析 druid连接池源码追踪先看看druid连接池各个参数代表的意思
druid连接池参数看完参数配置详解后,注意两个参数removeAbandoned和removeAbandonedTimeout,没错,这就是与这次异常息息相关的参数配置。
本次引发异常的configuration服务,removeAbandoned参数为true,removeAbandonedTimeout未配置(不清楚未配置原因),不配置默认五分钟超时
先进入连接池的初始化方法
只看主要逻辑即可
连接池的初始化方法public void init() throws SQLException { if (inited) { return; } final ReentrantLock lock = this.lock; try { lock.lockInterruptibly(); } catch (InterruptedException e) { throw new SQLException("interrupt", e); } try { this.id = DruidDriver.createDataSourceId(); for (Filter filter : filters) { filter.init(this); } connections = new DruidConnectionHolder[maxActive]; SQLException connectError = null; for (int i = 0, size = getInitialSize(); i < size; ++i) { //1.创建连接 Connection conn = createPhysicalConnection(); //2.将连接封装成DruidConnectionHolder对象 DruidConnectionHolder holder = new DruidConnectionHolder(this, conn); //3.将连接添加到连接数组中 connections[poolingCount] = holder; incrementPoolingCount();//连接池中连接数自增+1 } createAndLogThread(); createAndStartCreatorThread(); createAndStartDestroyThread(); initedLatch.await(); }finally { inited = true; lock.unlock(); } }
- 连接池初始化的逻辑主要如下:
- 判断是否已经初始化,如果已经初始化直接跳出;如果没有初始化则继续初始化
- 防止并发初始化需要加锁处理
- 初始化过滤器并进行初始化参数校验
- 初始化连接数组,并根据配置的初始化大小创建指定数量的连接存入数组中,初始化的连接数就是传入的参数值initialSIze的值
- 创建并开启创建连接和销毁连接的线程
- 标记初始化完成并释放锁
这里着重看createAndStartDestroyThread方法,毕竟我们的问题是conn holder读到了null,可能就是这个holder已经走了销毁程序,并且又放回了线程池中
销毁连接的任务交给了DestroyTask来实现,逻辑如下:
好了,从现在开始,关联到了上面所说的removeAbandoned参数了
销毁连接的任务主要有两个核心逻辑:
- 销毁空闲连接 shrink方法
当一个连接长时间没有被使用,如果不及时清理就会造成资源浪费,所以需要定时检查空闲时间过长的连接进行断开连接销毁
- 回收超时连接 removeAbandoned方法
当一个连接被一个线程长时间占有没有被归还,有可能是程序出故障了或是有漏洞导致迟迟没有归还连接,这样就可能会导致连接池中的连接不够用,所以需要定时检查霸占连接时间过长的线程,如果超过规定时间没有归还连接,则强制回收该连接。
我们只需要看回收超时方法即可
回收超时方法public int removeAbandoned() { int removeCount = 0; long currrentNanos = System.nanoTime(); ListabandonedList = new ArrayList(); synchronized(this.activeConnections) { Iterator iter = this.activeConnections.keySet().iterator(); while(iter.hasNext()) { DruidPooledConnection pooledConnection = (DruidPooledConnection)iter.next(); if (!pooledConnection.isRunning()) { long timeMillis = (currrentNanos - pooledConnection.getConnectedTimeNano()) / 1000000L; if (timeMillis >= this.removeAbandonedTimeoutMillis) { iter.remove(); pooledConnection.setTraceEnable(false); abandonedList.add(pooledConnection); } } } } if (abandonedList.size() > 0) { Iterator var5 = abandonedList.iterator(); while(true) { DruidPooledConnection pooledConnection; do { while(true) { if (!var5.hasNext()) { return removeCount; } pooledConnection = (DruidPooledConnection)var5.next(); synchronized(pooledConnection) { if (!pooledConnection.isDisable()) { break; } } } JdbcUtils.close(pooledConnection); pooledConnection.abandond(); ++this.removeAbandonedCount; ++removeCount; } while(!this.isLogAbandoned()); } } else { return removeCount; } }
回收超时连接方法的主要逻辑
- 定义需要回收的连接列表
- 遍历判断超时未回收的连接,并加入列表中
- 遍历回收连接列表,进行连接回收,强制断开连接
- JdbcUtils.close(pooledConnection)最终调向了recycle()把holder置为null
由上可知数据源连接在销毁后并没有放回连接池,只是将holder和conn置为null
异常出现可能原因由上面分析知道 只要连接超过默认5分钟时间 就会被置为null 那就会有两种导致异常的可能
- 执行链路过长,连接在执行sql的时候会检查holder是否为null,那是不是一整条链路执行时间过长超出了5分钟,导致下一次执行的时候报conn holder is null?
不太可能,排查过所有的执行链路时长都没有超过五分钟的
- 系统中事务长时间未提交,若事务在五分钟后提交也会导致holder is null
确实本次经过代码提交排查也看到了有一处手动事务后未提交就抛出异常的,导致该事务一直处于未提交状态
不过当时在想,这个请求频率不高,应用重启后只调用了一次,怎么会报出几百条异常,系统中设置的最大活跃线程数为1000,即使我占用了一条并置为null,后续还有999条可用链接,并且durid连接池也不会使用到它。
还是先看看spring事务源码吧
spring事务管理原理 事务实现方式在Spring中,事务有两种实现方式:
- 编程式事务管理: 编程式事务管理使用TransactionTemplate可实现更细粒度的事务控制。
- 申明式事务管理: 基于Spring AOP实现。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。申明式事务管理不需要入侵代码,通过@Transactional就可以进行事务 *** 作,方便快捷,且不会出错。
其实不管是编程式事务还是申明式事务,最终调用的底层核心代码是一致的。
为了贴合此次异常,此次异常的原因之一是由手动事务造成,所以只追踪编程式事务源码
编程式事务,spring提供了模板类TransactionTemplate
TransactionTemplate事务模板重点:
TransactionTemplate实现了TransactionOperations中的execute()方法
事务模板execute方法解析publicT execute(TransactionCallback action) throws TransactionException { // 内部封装好的事务管理器 if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) { return ((CallbackPreferringPlatformTransactionManager)this.transactionManager).execute(this, action); } // 需要手动获取事务,执行方法,提交事务的管理器 else { // 1.获取事务状态 TransactionStatus status = this.transactionManager.getTransaction(this); Object result; try { // 2.执行业务逻辑 result = action.doInTransaction(status); } catch (RuntimeException var5) { // 应用运行时异常 -> 回滚 this.rollbackOnException(status, var5); throw var5; } catch (Error var6) { // Error异常 -> 回滚 this.rollbackOnException(status, var6); throw var6; } catch (Exception var7) { // 未知异常 -> 回滚 this.rollbackOnException(status, var7); throw new UndeclaredThrowableException(var7, "TransactionCallback threw undeclared checked exception"); } // 3.事务提交 this.transactionManager.commit(status); return result; } }
一个完整的spring事务应该是遵循spring事务模板来完成
先开启事务,紧接着try住业务逻辑,在catch中回滚,在final中提交
spring事务核心类图PlatformTransactionManager顶级接口定义了最核心的事务管理方法,下面一层是AbstractPlatformTransactionManager抽象类,实现了PlatformTransactionManager接口的方法并定义了一些抽象方法,供子类拓展。
DataSourceTransactionmanager,即JDBC单数据库事务管理器,基于Connection实现
恰好这次可能出问题的代码也有getTransaction方法,先看看这个里面做了什么先
getTransaction获取事务源码分析AbstractPlatformTransactionManager实现了getTransaction()方法如下:
@Override public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException { Object transaction = doGetTransaction(); // Cache debug flag to avoid repeated checks. boolean debugEnabled = logger.isDebugEnabled(); if (definition == null) { // Use defaults if no transaction definition given. definition = new DefaultTransactionDefinition(); } // 如果当前已经存在事务 if (isExistingTransaction(transaction)) { // 根据不同传播机制不同处理 return handleExistingTransaction(definition, transaction, debugEnabled); } // 超时不能小于默认值 if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) { throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout()); } // 当前不存在事务,传播机制=MANDATORY(支持当前事务,没事务报错),报错 if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) { throw new IllegalTransactionStateException( "No existing transaction found for transaction marked with propagation 'mandatory'"); }// 当前不存在事务,传播机制=REQUIRED/REQUIRED_NEW/NESTED,这三种情况,需要新开启事务,且加上事务同步 else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED || definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW || definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { 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; } catch (RuntimeException ex) { resume(null, suspendedResources); throw ex; } catch (Error err) { resume(null, suspendedResources); throw err; } } else { // 当前不存在事务当前不存在事务,且传播机制=PROPAGATION_SUPPORTS/PROPAGATION_NOT_SUPPORTED/PROPAGATION_NEVER,这三种情况,创建“空”事务:没有实际事务,但可能是同步。警告:定义了隔离级别,但并没有真实的事务初始化,隔离级别被忽略有隔离级别但是并没有定义实际的事务初始化,有隔离级别但是并没有定义实际的事务初始化, if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) { logger.warn("Custom isolation level specified but no actual transaction initiated; " + "isolation level will effectively be ignored: " + definition); } boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null); } }
该方法走向两个分支
- 当前已存在事务:isExistingTransaction()判断是否存在事务,存在事务handleExistingTransaction()根据不同传播机制不同处理
- 当前不存在事务: 不同传播机制不同处理
进入第一行的doGetTransaction方法 跳转到DataSourceTransactionManager的实现
getResource这也有个ConnectionHolder,先进去看看getResource方法做了什么
spring事务缓存数据源连接原来是从map中获取的ConnectionHolder
spring事务中有多个map
第一个resources会缓存两种数据对
- 会话工厂和会话k=SqlsessionFactory v=SqlSessionHolder
- 数据源和连接k=DataSource v=ConnectionHolder
此刻就明白了,当该线程中还开启着事务,就会一直在threadlocal map中缓存一份数据源连接
当下次进来的时候则直接从缓存中拿连接。
那么我要是想重新开个事务呢?会重新往数据源拿连接不?
新建事务dobegin进入dobegin
@Override protected void doBegin(Object transaction, TransactionDefinition definition) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; Connection con = null; try {// 如果事务还没有connection或者connection在事务同步状态,重置新的connectionHolder if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) { Connection newCon = this.dataSource.getConnection(); if (logger.isDebugEnabled()) { logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction"); }// 重置新的connectionHolder txObject.setConnectionHolder(new ConnectionHolder(newCon), true); } //设置新的连接为事务同步中 txObject.getConnectionHolder().setSynchronizedWithTransaction(true); con = txObject.getConnectionHolder().getConnection(); //conn设置事务隔离级别,只读 Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition); txObject.setPreviousIsolationLevel(previousIsolationLevel);//DataSourceTransactionObject设置事务隔离级别 // 如果是自动提交切换到手动提交 if (con.getAutoCommit()) { txObject.setMustRestoreAutoCommit(true); if (logger.isDebugEnabled()) { logger.debug("Switching JDBC Connection [" + con + "] to manual commit"); } con.setAutoCommit(false); } // 如果只读,执行sql设置事务只读 prepareTransactionalConnection(con, definition); txObject.getConnectionHolder().setTransactionActive(true);// 设置connection持有者的事务开启状态 int timeout = determineTimeout(definition); if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { txObject.getConnectionHolder().setTimeoutInSeconds(timeout);// 设置超时秒数 } // 绑定connection持有者到当前线程 if (txObject.isNewConnectionHolder()) { TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder()); } } catch (Throwable ex) { if (txObject.isNewConnectionHolder()) { DataSourceUtils.releaseConnection(con, this.dataSource); txObject.setConnectionHolder(null, false); } throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex); } }
由该方法可见,若线程缓存中还有该数据源连接,就会直接使用这个连接holder(该holder是spring事务的连接holder,不是durid连接池的holder)
若该事务连接holder的connection所关联的druid 的连接holder为null,在执行sql时触发的checkstate方法会抛出 connection holder is null 异常
通过kibana查看日志确定手动事务开启和后续触发的异常都是同应用线程
至此排查完毕
源码追踪总结手动开启事务后,获取了durid连接池中的某条数据源连接,放入了spring事务的本次线程缓存中,由于中途抛出异常,提前终止了代码,此次事务并没有提交或者回滚,在这个应用线程内一直缓存着这份数据源连接,五分钟后,durid连接被置为null,其他请求通过此应用线程执行时,看到了数据源连接还在缓存中,便不会去从druid连接池中获取,在执行sql时检查conn状态则会抛出connection holder is null异常。
durid开源作者温绍锦说过:不要试图缓存你从连接池中获取的连接
可是spring事务缓存了此份连接[泣不成声]
不过只要做好事务处理,就不会发生该异常
- 将事务交由spring控制
- 手动控制时,遵循事务模板
- 重复出现异常情况时,一定要排查清楚根本原因并解决后再上线
- 针对外部原因影响,先消除外部因素,多复现问题
- 多从源码角度分析问题产生原因
- 多review自身代码是否合理
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)