Spring长事务导致connectionclosed,又熬了一个大夜
是的,今早一到公司就收到了机器人的告警,从异常日志来看是数据库连接已关闭,然后我在解决这个问题的过程中发现了几个问题,不急,听我一一道来
异常被try后没有继续抛出,导致继续执行后续操作
我们看到前文示例代码会发现我们在 try 之后只是 rollback 了,对于异常也只是打印一下并没有继续抛出。
那么就会导致一种情况:假设你在 Service 层中调用多个调用数据库的修改方法,那么第一个操作失败后异常没有抛出,Service 层不知道,就会继续向后面执行,修复很简单,只需要将异常抛出即可: // 案例1:参考MybatisPlus的com.baomidou.mybatisplus.extension.toolkit.SqlHelper##executeBatch()实现 batchSqlSession.rollback(); Throwable unwrapped = ExceptionUtil.unwrapThrowable(e); if (unwrapped instanceof RuntimeException) { MyBatisExceptionTranslator myBatisExceptionTranslator = new MyBatisExceptionTranslator(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), true); throw Objects.requireNonNull(myBatisExceptionTranslator.translateExceptionIfPossible((RuntimeException) unwrapped)); } throw new CommonException(unwrapped); // 案例2:简单来说,只要能把异常抛出去即可,并不定需要像上面这么复杂 batchSqlSession.rollback(); throw new CustomException(e); 大事务/长事务导致 connection closed代码场景
我们来看一段业务功能的伪代码,大致如下: @Transactional(rollbackFor = Exception.class) @Override public Integer billCheck() { // 获取对应的策略 策略 = getStrategy(); // 前置参数校验 if (必要参数是否存在){ return false; } try { // 解析文件 文件里的数据集合 = 策略.parseFile(file); // 将文件里的数据插入数据库表 影响的行数 = 策略.handleFileData(文件里的数据); if (影响的行数 > 0) { // 将文件里的数据和本地的数据进行对比操作 对比后的数据 = 策略.doBillCheck(参数); // 将对比的结果分开插入到数据库中 batchUtils.batchUpdateOrInsert(成功的数据, 某Mapper.class, (billErr, mapper) -> mapper.insert(data)); batchUtils.batchUpdateOrInsert(失败的数据, 某Mapper.class, (billErr, mapper) -> mapper.insert(data)); batchUtils.batchUpdateOrInsert(需要更新的数据, 某Mapper.class, (billErr, mapper) -> mapper.update(data)); } // 发送企业微信机器人通知 策略.sendRobotMessage(); log.info("耗时:{}毫秒", 耗时); } catch (Exception e) { log.error("对账出错", e); throw new CommonException("对账出错"); } return 影响的行数; }
我们梳理一下,这是一个普通的模板方法 + 策略模式的应用,因为业务场景中不管是哪个通道的文件都会必经如下几个步骤,所以就将其抽象了。我们可以发现这个方法里面做了很多数据库操作,并且使用了声明式事务注解,然后里面大致有如下几个步骤: 解析文件 将文件里的数据插入数据库表 将文件里的数据和本地的数据进行对比操作 将对比的结果分开插入到数据库中
然后我们再来看一段配置,它来自 druid 连接池框架,如下: spring: datasource: druid: remove-abandoned: true ## 单位:秒 remove-abandoned-timeout: 60 log-abandoned: true
以上三条属性一般是用来 防止连接泄露 的,说明如下: removeAbandoned :要求获取到连接后,如果空闲时间超过 removeAbandonedTimeoutMillis 秒后没有 close,druid 会强制回收,默认false; logAbandoned :如果回收了连接,是否要打印一条 log,默认 false; removeAbandonedTimeoutMillis :连接回收的超时时间,默认5分钟;
看到这里我想大部分同学可能已经知道是什么问题了,没错, 肯定是因为拿到了连接,但拿的时间超过了这个限制,导致 druid 直接强制回收了该连接 ,但是知根知底方能百战百胜,这么好的机会怎么能不深入了解一下?关注公号:码猿技术专栏,回复关键词:1111 获取阿里内部性能调优手册~ 什么时候获取的连接?
是的,既然是连接超时被关闭,那我们肯定要先找到是什么时候拿到的连接,是方法中第一次操作数据库【将文件里的数据插入数据库表】的时候?那当然不是,我们知道 Mybatis 有一个 Executor_ _接口,感兴趣的可以自行了解,它定义了数据库操作的基本方法,它才是SQL语句幕后的执行者,我们直接来看获取连接的地方 org.apache.ibatis.executor.BaseExecutor##getConnection : protected Connection getConnection(Log statementLog) throws SQLException { Connection connection = transaction.getConnection(); if (statementLog.isDebugEnabled()) { return ConnectionLogger.newInstance(connection, statementLog, queryStack); } else { return connection; } }
我们可以看出来,我们是通过 Transaction 去获取连接的,但如果我们是第一次操作的时候才去获取的连接,那怎么会连接超时呢?所以我初步推断是开启事务的时候可能就已经获取连接了,那我们来求证一下,来到 Spring 的事务管理器PlatformTransactionManager ,Mybatis 用的是它的实现类DataSourceTransactionManager , 然后我们一路跟 getTransaction 方法来到AbstractPlatformTransactionManager##getTransaction ,再到DataSourceTransactionManager##doBegin public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException { // 省略无关代码 ... doBegin(transaction, definition); // 省略无关代码 ... } @Override protected void doBegin(Object transaction, TransactionDefinition definition) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; Connection con = null; try { // 如果数据源事务对象的ConnectionHolder为null或者是事务同步的 if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) { // 获取当前数据源的数据库连接 Connection newCon = obtainDataSource().getConnection(); if (logger.isDebugEnabled()) { logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction"); } txObject.setConnectionHolder(new ConnectionHolder(newCon), true); } }
就是这!它其实在进入方法的最开始,开启事务的时候就已经获取了连接,然后由于【解析文件】耗时过长,导致整个方法的执行时间超过了 60s 被强制回收连接,但你以为这就结束了?没错,当时出现这个问题的时候,我还手动触发了一次,结果第二次通过了,你说诡异不诡异?两次执行的时间都是 90s。 druid removeAbandoned 背后的秘密
所以我们继续看一下 druid 是怎么强制回收连接的,Druid每隔 timeBetweenEvictionRunsMillis (默认1分钟)会调用DestroyTask,在这里会判断是否可以回收泄露的连接,就是因为它是1分钟执行一次,所以可能第二次正好它执行的时候还没超过 60s,所以这次简直就是玄学了啊。 public class DestroyTask implements Runnable { public DestroyTask() { } @Override public void run() { shrink(true, keepAlive); // 判断removeAbandoned是否为true,默认是false if (isRemoveAbandoned()) { removeAbandoned(); } } }
然后我们看到 removeAbandoned 方法,这里面有一段代码如下: for (; iter.hasNext();) { DruidPooledConnection pooledConnection = iter.next(); // 判断该连接是否还在运行,只回收不运行的连接 // Druid会在连接执行query,update的时候设置为正在运行, // 并在回收后设置为不运行 if (pooledConnection.isRunning()) { continue; } long timeMillis = (currrentNanos - pooledConnection.getConnectedTimeNano()) / (1000 * 1000); //判断连接借出去的时间大小 if (timeMillis >= removeAbandonedTimeoutMillis) { iter.remove(); pooledConnection.setTraceEnable(false); abandonedList.add(pooledConnection); } } //判断是否要记录连接回收日志,这个很重要,可以及时发现项目中是否有连接泄露 if (isLogAbandoned()) { StringBuilder buf = new StringBuilder(); buf.append("abandon connection, owner thread: "); buf.append(pooledConnection.getOwnerThread().getName()); buf.append(", connected at : "); buf.append(pooledConnection.getConnectedTimeMillis()); buf.append(", open stackTrace "); }
是的,如果你的连接被强制回收了的话,你只需要将 LogAbandoned 设置为 true,就可以通过日志看到相关信息了 解决方案
到这,问题就基本都发现了,那么我最后是怎么解决的呢?原本我是想的把不需要事务的动作抽离出来新建一个方法,后面我发现这样子好像模板方法并不好使了,我就采用了 编程式事务 ,感兴趣的可以自己在了解一下,最后伪代码如下: @Autowired private TransactionTemplate transactionTemplate; @Transactional(rollbackFor = Exception.class) @Override public Integer billCheck() { // 获取对应的策略 策略 = getStrategy(); // 前置参数校验 if (必要参数是否存在){ return false; } try { // 解析文件 文件里的数据集合 = 策略.parseFile(file); // 编程式事务 影响的行数 = transactionTemplate.execute(transactionStatus -> { // 将文件里的数据插入数据库表 return 策略.handleFileData(文件里的数据); }); if (影响的行数 > 0) { // 将文件里的数据和本地的数据进行对比操作 对比后的数据 = 策略.doBillCheck(参数); // 编程式事务 transactionTemplate.execute(transactionStatus -> { // 将对比的结果分开插入到数据库中 batchUtils.batchUpdateOrInsert(成功的数据, 某Mapper.class, (billErr, mapper) -> mapper.insert(data)); batchUtils.batchUpdateOrInsert(失败的数据, 某Mapper.class, (billErr, mapper) -> mapper.insert(data)); batchUtils.batchUpdateOrInsert(需要更新的数据, 某Mapper.class, (billErr, mapper) -> mapper.update(data)); return Boolean.TRUE; }); } // 发送企业微信机器人通知 策略.sendRobotMessage(); log.info("耗时:{}毫秒", 耗时); } catch (Exception e) { log.error("对账出错", e); throw new CommonException("对账出错"); } return 影响的行数; }
这样子,我们将解析文件和对比数据(只是查询)这种耗时操作放在了事务外,并且将原本一个事务里的操作拆成了两个小事务,这样子基本就避免了大事务的问题了,完结撒花~ 大事务/长事务可能造成的影响并发情况下,数据库连接池容易被撑爆 锁定太多的数据,造成大量的阻塞和锁超时 执行时间长,容易造成主从延迟 回滚所需要的时间比较长 undo log膨胀
所以在业务涉及中,你一定要对大事务特别对待,比如业务设计时,把大事务拆成小事务。 总结
声明式事务有一个局限,那就是他的最小粒度要作用在方法上 !所以大家在用的时候要格外格外注意大事务的问题,尽量避免在事务中做一些无关数据库的操作,比如RPC远程调用、文件解析等,都是血泪的教训啊!!
原文链接:https://mp.weixin.qq.com/s/NEo8zo4YDOMnu87kBbjjnw
python学习whileTrue的用法学习python过程中,我们经常会遇到whileTrue的用法。今天我们来讲解下它的用法。一理论while(true)是一个无限循环,表示一直为真。()里的是while的条件,tr
JAVA开发搞了一年多大数据的总结2021年7月份加入了当前项目组,以一个原汁原味的Java开发工程师的身份进来的,来了没多久,项目组唯一一名大数据开发工程师要离职了,一时间一大堆的数据需求急需人来接手,此刻又招不
刚上市就火了,和奔驰GLS同级,5。3秒破百能上绿牌,实拍理想L9对于新能源车型,大部分消费者主要考虑的还是续航问题,担心在路途中电量耗尽,没有办法及时补充电量,所以不敢下手。对于这个方面的问题,可能增程式的新能源汽车能够得到解决,在电动的基础上
真菌毒力效应因子触发过敏性炎症的机制撰文十一月侵袭性真菌病原体每年会造成大约150万人的死亡,其导致的疾病称为被忽略的流行病。新的耐药性病原体层出不穷,现有的药物疗效非常有限且毒副作用很高。但是目前真菌病原体是如何逃
一个时代的落幕媒体人周刊不知不觉一个时代就要落幕了。马云走了,远赴欧洲。刘强东退出京东,转让了股份。腾讯也在和联通炒CP。互联网的巨头们,逐渐迎来了自己的结局!03年的非典,不出门,也能购物的销
新能源代步微型车再添一员悍将,雷诺江铃小麒麟即将上市随着五菱宏光miniev的火爆,也使各家车企甚是眼红,于是各类微型车层出不穷,这不,近日雷诺江铃集团官方就正式曝光了,旗下全新的新能源微型车雷诺江铃小麒麟,并宣布将于11月11日正
小鹏的宿命翻看一下股市行情,小鹏汽车的股价一路下跌,标志着新能源汽车这场大戏的开场造车新势力篇正在落幕。何小鹏的造车梦要么就此终结,要么换个地方圆梦,股市已经告诉人们一切。记得当年在中国汽车
图扑软件3D组态编辑器,低代码零代码构建数字孪生工厂行业背景随着中国制造2025计划的提出,新一轮的工业改革拉开序幕。大数据积累的指数级增长为智能商业爆发奠定了良好的基础,传统制造业高污染高能耗低效率的生产模式已不符合现代工业要求。
县委书记亲自推销,浙江小镇保姆为何被大城市秒抢?11月财经新势力常山模式,给国内一些没有资源产业的劳务输出地区带来启示。正解局出品不久前,一篇题为我的阿姨被人以1万月薪挖走了的文章,讲述了在深圳的一位二胎妈妈请阿姨的苦恼阿姨价格
纪晓岚断案,自称短视,其实是大智慧纪晓岚断案,自称短视,其实是大智慧清乾隆五十年,乾隆皇帝有感于纪晓岚编篡四库全书的功劳,颁旨提拔纪晓岚为都察院御史,掌管全国最高监察机构,考察官吏,整饬纲常。显示出乾隆皇帝对纪晓岚
浙江九旬老太陈金英,10年还清2077万欠款,她是怎么做到的?你可曾想过有这样一位老人,年过九旬还在大街上经营着自己的生意,只为能还清两千多万的欠款。十年的风风雨雨,她没了自己的工厂,没了自己的住房也没了自己的丈夫,但是她始终没有遗忘自己心中