高并发下,少扣了十几万保费?

本文揭示了在保费代扣业务中,过度使用@Trancation导致的数据库连接问题。作者分析了事务过大、连接池限制及超时回收机制的影响,提供了调整事务范围和解决连接丢失的策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

场景引入

今天的正文开始,我们先引入一个简单的业务场景

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

看图说话

定时任务开始跑批的时候,我们会去查询这一笔单子相关的代扣信息,然后在我们的保费申请表中新增一笔数据,紧接着就异步发起一个流程编排,进入真正的代扣逻辑处理。

计划非常完美,但是,在压测过程中却频频发现异常,主要问题体现在两点 - 间断性的提示保费申请不存在(保费代扣的流程编排第一个节点就是校验保费申请信息是否存在),我们通过保费申请编号去数据库中的确存在数据,但是翻看日志,sql 的 query 结果的确为 0 - 经常性的日志提示 Caused by: java.sql.SQLException: connection holder is null,导致后续跑批失败

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

数据库连接为何丢失

在测试反馈定时任务执行失败的时候,刚看到这个异常提示的时候,connection holder is null,第一反应就是我的数据库连接还没执行完任务去哪儿了

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

https://github.com/alibaba/druid/issues?q=connection+holder+is+null

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

大致看下来,明白这个问题大概率是两个原因 - 连接数超上限 - 单个连接超时

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

目前系统中使用的是阿里的 druid 连接池,配置参数为

JAVA // 最大连接数 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

Java // 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 informationschema.INNODBTRX

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

真相大白了,原来 @Trancational 是在执行第一个sql 的时候开启的事务,具体的源码跟踪交给小伙伴们自己动手实践一下

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

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

接下来上一波伪代码

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

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

那么,如何控制在事务提交之后,在去发起我们的流程编排呢

这边就需要用到我们今天的主角

Java // 注册事务后置处理 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 的实现,也是对本文的巩固学习

一不小心,又进步了,卷死你的同事从这一步开始

<think>嗯,用户问的是“年化风险保费”,我需要先理解这个概念到底是什么。首先,我应该回忆一下保险相关的术语,可能涉及到保费的计算方式。年化,通常是指将某个周期性的费用或收益按年度来计算,比如年化利率。风险保费,应该是保险公司根据承担的风险而收取的费用部分。 接下来,我需要确定年化风险保费的具体定义。可能需要分解成两部分:风险保费和年化处理。风险保费是保险公司为覆盖预期损失和风险而收取的费用,这部分通常基于精算模型,考虑被保人的风险因素,比如年龄、健康状况、职业等。而年化则是将非年度的保费转换成年度形式,方便比较和理解。 然后,用户可能想知道如何计算年化风险保费。这里可能需要考虑保险类型的不同,比如寿险、车险等,计算方式可能有差异。例如,定期寿险的风险保费可能基于死亡率表,而车险则基于事故率等因素。同时,年化处理可能涉及将短期保费转换为年费率,比如按月缴纳的保费乘以12,但这可能不完全准确,因为需要考虑复利或折现因素。 另外,要注意区分风险保费与其他保费组成部分,比如附加保费(运营成本、利润等)。年化风险保费可能只涉及纯风险部分,而不包含其他费用。需要明确说明这一点,避免用户混淆。 可能还需要举例说明,比如假设某保险产品的月风险保费是100元,年化后就是1200元,但实际计算可能更复杂,需考虑时间价值和风险发生概率的时间分布。如果有公式的话,应该用LaTeX格式呈现,比如$$R_{annual} = R_{monthly} \times 12$$,但需要确认是否正确。 同时,用户可能想知道年化风险保费的应用场景,比如在比较不同保险产品时,年化处理可以帮助消费者更清晰地看到每年的风险成本。或者保险公司在定价时如何使用年化风险保费作为基础。 需要验证这些信息是否正确,可能需要查阅保险精算的参考资料,确保定义和计算方法的准确性。如果有不确定的地方,应该明确说明,或者建议用户咨询专业人士。 总结下来,回答的结构应该是先定义年化风险保费,分解解释各个组成部分,然后说明计算方法和影响因素,最后举例说明应用场景或计算实例,确保用户能够逐步理解这个概念。</think>年化风险保费是保险精算中的重要概念,其核心是将保险标的的**风险成本**按年度折算后的费用。下面分步解释: --- ### 一、定义与组成 1. **风险保费** 保险公司为承担被保人潜在损失而收取的纯风险成本,计算公式为: $$风险保费 = 预期损失 \times (1 + 风险附加系数)$$ 其中: - **预期损失**:基于历史数据计算的损失概率(如死亡率、事故率) - **风险附加系数**:覆盖不确定性的缓冲比例 2. **年化处理** 将非年度周期(如月缴、季缴)的保费转换为等效年度金额。例如: - 若月缴风险保费为$50$元,则年化后为: $$年化风险保费 = 50 \times 12 = 600 \text{元}$$ --- ### 二、关键影响因素 1. **风险概率** - 寿险:基于生命表计算的死亡率 - 车险:车辆型号、驾驶员事故率 *公式示例*: $$预期损失 = \sum (损失金额_i \times 发生概率_i)$$ 2. **时间价值** 长期保单需考虑资金的时间价值,公式为: $$年化风险保费 = \frac{总风险保费}{(1 + 折现率)^n}$$ 其中$n$为保单年限。 --- ### 三、实际应用案例 **案例:定期寿险定价** - 假设30岁男性投保100万元,1年期死亡率$0.05\%$ - 风险附加系数$10\%$ - 计算过程: $$风险保费 = 1,000,000 \times 0.05\% \times (1+10\%) = 550\text{元}$$ 若按月缴纳,则: $$月缴风险保费 = \frac{550}{12} \approx 45.83\text{元}$$ --- ### 四、与其他保费的区别 | 项目 | 风险保费 | 附加保费 | |-------------|---------------|----------------| | **用途** | 覆盖预期损失 | 运营成本+利润 | | **是否年化**| 通常需年化 | 直接按比例分摊 | --- ### 五、注意事项 - 年化计算时需明确是否含税 - 长期险种可能涉及**均衡保费**(将多年风险分摊到各年) - 不同产品的年化方法可能不同(如消费型保险vs储蓄型保险) 如果需要具体产品的年化风险保费计算演示,可提供保险类型和参数,我会进一步分析。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值