关于java:高并发下少扣了十几万保费

9次阅读

共计 5090 个字符,预计需要花费 13 分钟才能阅读完成。

场景引入

明天的注释开始,咱们先引入一个简略的业务场景

保费代扣(金融公司固定日从用户账户划扣保费)

看图谈话

定时工作开始跑批的时候,咱们会去查问这一笔单子相干的代扣信息,而后在咱们的保费申请表中新增一笔数据,紧接着就异步发动一个流程编排,进入真正的代扣逻辑解决。

打算十分完满,然而,在压测过程中却频频发现异常,次要问题体现在两点

  • 间断性的提醒保费申请不存在(保费代扣的流程编排第一个节点就是校验保费申请信息是否存在),咱们通过保费申请编号去数据库中确实存在数据,然而翻看日志,sql 的 query 后果确实为 0
  • 经常性的日志提醒 Caused by: java.sql.SQLException: connection holder is null,导致后续跑批失败

小伙伴们能够开始思考,这两个问题有可能是因为什么导致的

数据库连贯为何失落

在测试反馈定时工作执行失败的时候,刚看到这个异样提醒的时候,connection holder is null,第一反馈就是我的数据库连贯还没执行完工作去哪儿了

我的项目中应用的是 druid 连接池,遇事不决,github 上看下

https://github.com/alibaba/dr…

在 github 上,看到了也有不少小伙伴提了 issue

大抵看下来,明确这个问题大概率是两个起因

  • 连接数超下限
  • 单个连贯超时

带着这两个问题,我去查看了阿波罗的相干配置

目前零碎中应用的是阿里的 druid 连接池,配置参数为

// 最大连接数
spring.datasource.druid.max-active = 128
// 配置获取连贯期待超时的工夫
spring.datasource.druid.max-wait = 60000
// 超时主动动回收连贯配置
spring.datasource.druid.remove-abandoned = true
// 连贯超时回收工夫
spring.datasource.druid.remove-abandoned-timeout-millis = 30000

带着这些参数,我去翻看了一下这个代扣数据查问的逻辑

在看到长达 100 多行的一个办法被 @Trancation 注解笼罩的时候,我就晓得这个事不妙

接下来到了 @Trancation 的科普工夫,尽管 @Trancation 用起来很爽,然而使用不当极易被拉进火葬场, 咱们晓得申明式事务是基于 AOP 来治理的,在业务办法前后进行拦挡,针对咱们的业务代码没有入侵性,然而申明式事务的局限性就在于它的最低粒度要作用与办法上。

认真翻看这个逻辑,因为代扣所需数据较多,进行了屡次跨模块查问,并且代码还通过多人之手,大家每次都缝缝补补一些逻辑,并且都心领神会的没有发现这个事务过大的问题

很显然,咱们没方法找到第一个写上这个 @Trancation 的人,对他说,小伙子,你这写的有问题呀,事务过大了🌶️🐔

宽以律己,严以待人,呸,说反了,严以律己,宽以待人,不是所有人都在第五层,所以,在平时代码中,针对这种事务范畴的管制, 咱们能够有选择性的去应用编程性事务去解决 ,阿里巴巴标准中针对这一点其实是对咱们有阐明的

只管编程式事务看起来不是那么“优雅”,然而起码能够尽可能去揭示或者躲避其余的小伙伴这个大事务的呈现,并且绝对于申明式事务没有那么容易生效

那么回来这个正题,其实剖析到这里大家应该曾经有思路,为何会呈现连贯失落,就是因为事务过大,长时间占着这一个数据库链接,迟迟不舍得开释,导致连接池资源有余,或者单个连贯超时,解决方案也很简略,为了不阻塞测试进度,咱们能够暂时性敞开连贯超时回收的机制,将连接数调大,而后治标的办法还是去入手优化这个屎山

你认为这里就完结了吗?当然不是。

在排查的时候我思考了一个问题

@Trancational 在什么时侯开启开启事务的?

给你三秒的思考工夫。

ok,如果你很分明的晓得能够跳过这部分,如果你不是十分分明,或者你认为是一进入办法就开启了事务,那么咱们来看下这个 @Trancational 开启事务的源码

咱们先写个 demo 将断点打到办法刚开始的中央

察看堆栈,这个中央十分可疑

org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction

接下来咱们断点到 #createTransactionIfNecessary 这个办法

这个办法是用来创立事务,将以后事务状态和信息保留到 TransactionInfo 对象中,包含设置流传行为、隔离级别、手动提交、开启事务等,绑定事务 txInfo 到以后线程,这外面用了应用许多缓存和 threadLocal 对象,不便获取连贯信息等,并且着里有一点,咱们先持续往下走

往下逐渐追踪,察看堆栈即可找到真正开始处理事务的逻辑

org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin

贴心的我曾经帮你正文好了,不用谢

咱们能够看到在这段代码里这一段将事务切换到手动提交,将 autoCommit 设置为 false

            // Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
            // so we don't want to do it unnecessarily (for example if we've explicitly
            // configured the connection pool to set it already).
            // 必须时切换手动提交事务。在一些 JDBC 驱动中是很低廉的,非必须的话不要用。(比方咱们曾经把连接池设置抵触)
            // 如果是主动提交, 事务对象的必须复原主动提交为 true, 连贯的主动提交敞开。if (con.getAutoCommit()) {
                // doCleanupAfterCompletion 办法中 事务完结时会判断这个值为 true 时,连贯才会主动提交。txObject.setMustRestoreAutoCommit(true);
                if (logger.isDebugEnabled()) {logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
                }
                con.setAutoCommit(false);
            }

那么,此时开启了事务了么,看起来不像

此处就是将咱们的主动提交敞开了,那么咱们先来写个小 demo 试验一下

接下来利用这个 sql 查问一下以后事务,又是一个知识点,拿本子记起来

SELECT * FROM information_schema.INNODB_TRX

接下来咱们将断点走上来,在看下,是否事务开启了

水落石出了,原来 @Trancational 是在执行第一个 sql 的时候开启的事务,具体的源码跟踪交给小伙伴们本人入手实际一下

数据库中有数据,为何就查问不到呢?

接下来回顾一下咱们的第一个问题,为何数据库中明明有数据,然而在执行过程中却查不到呢?

接下来上一波伪代码

眼尖的小伙伴,可能一下子就发现了,是不是这个异步发动流程编排的锅

对于 Spring 中事务的流传这边就不给小伙伴赘述了

那么,如何管制在事务提交之后,在去发动咱们的流程编排呢

这边就须要用到咱们明天的配角

      // 注册事务后置解决
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCommit() {log.info("事务提交实现,发动流程!");
                // 异步发动流程编排
                this.asyncExecute(insurancePremiumApplyDo, flowId);
            });
        }

这样就很好解决了咱们呈现的问题,你还认为这样就完结了么,当然不是,知其然还须要知其所以然,咱们来看下 TransactionSynchronizationManager 这个事务管理器是如何帮我做到这个事务提交后才进行执行咱们的代码呢。

一样 demo 走起,看下这一个 afterCommit 办法是否真的起作用了

果然十分的完满。接下来咱们去看下这个 afterCommit 是如何起作用的。

正式揭开源码的面纱之前,咱们想一想,如果是咱们去实现这个 afterCommit 性能,咱们会怎么做?

第一想法是不是判断以后线程绑定的事务是否解决完,如果解决完了就去调用这个 afterCommit 的办法。

那么在哪里去判断这个事务处理完了呢?对,就是咱们前文提到的 #invokeWithinTransaction 中的 commitTransactionAfterReturning 这个办法。这个办法名曾经很分明通知了咱们,我这个办法执行的就是提交了事务后的办法,看来咱们的理论知识十分的充分,接下来,咱们只须要扒开源码看看,去验证一下咱们的猜测。验证咱们猜测之前

咱们先按程序看一下 demo 代码中 #registerSynchronization 这个办法里做了什么事

这个办法看起来还是蛮简略易懂的,将咱们 new 进去的 TransactionSynchronization 对象,放入一个 synchronizations 咱们暂且称为事务同步器这样的 ThreadLocal 里,为何是装在 ThreadLocal 里的,并且是一个 Set 汇合,咱们其实想一下,一个线程是否能够被多个事务绑定,如何存储被多个事务绑定的线程 咱们就能够了解为何应用了 ThreadLocal 和 Set 汇合了,咱们临时留个心眼,接着往下看

咱们间接将断点打在 afterCommit 上,debug 剖析一波,是谁在调用 afterCommit,和咱们的猜测是否统一。登程进入 afterCommit 外部

xiu,代码 debug 进到了这里

咱们关注到图里的第二个办法,这边正文也写的十分分明,actually 示意理论,就是理论调用咱们 afterCommit 的中央

并且这边是一个循环,也合乎咱们上文看到的为何把咱们 new 进去的对象 装进了 synchronizations 中,至于为何是 list,而不是 set,感兴趣的小伙伴能够去钻研一下,不是咱们剖析的重点

咱们接着看 谁调用了 #invokeAfterCommit 办法呢

哦,就是它下面的 #triggerAfterCommit 办法,想必英文过了四级的同学,都能了解正文的意思

Trigger {@code afterCommit} callbacks on all currently registered synchronizations.

在所有的 synchronizations 上触发 afterCommit 这个办法

ok,接下来就是揭晓谜底的时刻,是谁去触发了这个 afterCommit 办法,咱们的猜想是否正确呢,是否是这个 commitTransactionAfterReturning 办法呢?

关上咱们的堆栈看一看

在间隔第 6 行的中央找到了 #invokeWithinTransaction 中的 commitTransactionAfterReturning 这个办法,猜测正确。

接下来咱们就能够正向看一下,这个 #afterCommit 是如何实现的

长图预警,缓缓看,能够珍藏点赞在看

总结

看到这里咱们曾经能够分明了

  • @Trancation 的应用小细节,不倡议过大的事务,倡议如果可能的话手动开启事务
  • @Trancation 事务开启的实现,是办法进来的时候只是将主动提交敞开,事务就绪,在代码真正调用 sql 的时候去开启事务
  • 事务后置提交的底层实现 #afterCommit 的源码

对于 @Trancation 的事务源码还是十分易懂,倡议小伙伴们能够去读一读,也给小伙伴留一个作业,看一本人入手去翻阅一下 org.springframework.transaction.support.TransactionSynchronization#beforeCommit
的实现,也是对本文的坚固学习

一不小心,又提高了,卷死你的共事从这一步开始

闲言碎语

三月份很快就过来了

想去听的 A 公馆乐队的门票也没去成,

咸鱼上买了 打搅一下乐团的票,也因为深圳疫情被强行延期了

所幸的是深圳疫情曾经逐步被管制住了,

整个三月做了无数次核酸,好几次被大白说我 阿~ 的姿态很规范

心愿能够不必记着 24 小时核酸、能够不必进入公共场所就要扫码。祝大家四月高兴


往期举荐

面试官居然和我死磕 Maven

浅谈 ThreadLocalMaven

正文完
 0