事务嵌套问题_注意Spring事务这一点,避免出现大事务

d2a4529d22929df088109dfd35b1eda5.png

背景

本篇文章主要分享压测的(高并发)时候发现的一些问题。之前的两篇文章已经讲述了在高并发的情况下,消息队列和数据库连接池的一些总结和优化,有兴趣的可以在我的公众号中去翻阅。废话不多说,进入正题。

事务,想必各位CRUD之王对其并不陌生,基本上有多个写请求的都需要使用事务,而Spring对于事务的使用又特别的简单,只需要一个@Transactional注解即可,如下面的例子:

    @Transactionalpublic int createOrder(Order order){orderDbStorage.save(order);orderItemDbStorage.save(order.getItems());return order.getId();}

在我们创建订单的时候, 通常需要将订单和订单项放在同一个事务里面保证其满足ACID,这里我们只需要在我们创建订单的方法上面写上事务注解即可。

事务的合理使用

对于上面的创建订单的代码,如果现在需要新增一个需求,在创建订单之后发送一个消息到消息队列或者调用一个RPC,你会怎么做呢?很多同学首先会想到,直接在事务方法里面进行调用:

    @Transactionalpublic int createOrder(Order order){orderDbStorage.save(order);orderItemDbStorage.save(order.getItems());sendRpc();sendMessage();return order.getId();}

这种代码在很多人写的业务中都会出现,事务中嵌套rpc,嵌套一些非DB的操作,一般情况下这么写的确也没什么问题,一旦非DB写操作出现比较慢,或者流量比较大,就会出现大事务的问题。由于事务的一直不提交,就会导致数据库连接被占用。这个时候你可能会问,我扩大点数据库连接不就行了吗,100个不行就上1000个,在上篇文章已经讲过数据库连接池大小依然会影响我们数据库的性能,所以,数据库连接并不是想扩多少扩多少。

那我们应该怎么对其进行优化呢?在这里可以仔细想想,我们的非db操作,其实是不满足我们事务的ACID的,那么干嘛要写在事务里面,所以这里我们可以将其提取出来。

    public int createOrder(Order order){createOrderService.createOrder(order);sendRpc();sendMessage();}

在这个方法里面先去调用事务的创建订单,然后在去调用其他非DB操作。如果我们现在想要更复杂一点的逻辑,比如创建订单成功就发送成功的RPC请求,失败就发送失败的RPC请求,由上面的代码我们可以做如下转化:

    public int createOrder(Order order){try {createOrderService.createOrder(order);sendSuccessedRpc();}catch (Exception e){sendFailedRpc();throw e;}}

通常我们会捕获异常,或者根据返回值来进行一些特殊处理,这里的实现需要显示的捕获异常,并且在次抛出,这种方式不是很优雅,那么怎么才能更好的写这种话逻辑呢?

TransactionSynchronizationManager

在Spring的事务中刚好提供了一些工具方法,来帮助我们完成这种需求。在TransactionSynchronizationManager中提供了让我们对事务注册callBack的方法:

public static void registerSynchronization(TransactionSynchronization synchronization)throws IllegalStateException {Assert.notNull(synchronization, "TransactionSynchronization must not be null");if (!isSynchronizationActive()) {throw new IllegalStateException("Transaction synchronization is not active");}synchronizations.get().add(synchronization);}

TransactionSynchronization也就是我们事务的callBack,提供了一些扩展点给我们:

public interface TransactionSynchronization extends Flushable {int STATUS_COMMITTED = 0;int STATUS_ROLLED_BACK = 1;int STATUS_UNKNOWN = 2;/*** 挂起时触发*/void suspend();/*** 挂起事务抛出异常的时候 会触发*/void resume();@Overridevoid flush();/*** 在事务提交之前触发*/void beforeCommit(boolean readOnly);/*** 在事务完成之前触发*/void beforeCompletion();/*** 在事务提交之后触发*/void afterCommit();/*** 在事务完成之后触发*/void afterCompletion(int status);
}

我们可以利用afterComplettion方法实现我们上面的业务逻辑:

    @Transactionalpublic int createOrder(Order order){orderDbStorage.save(order);orderItemDbStorage.save(order.getItems());TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {@Overridepublic void afterCompletion(int status) {if (status == STATUS_COMMITTED){sendSuccessedRpc();}else {sendFailedRpc();}}});return order.getId();}

这里我们直接实现了afterCompletion,通过事务的status进行判断,我们应该具体发送哪个RPC。当然我们可以进一步封装TransactionSynchronizationManager.registerSynchronization将其封装成一个事务的Util,可以使我们的代码更加简洁。

通过这种方式我们不必把所有非DB操作都写在方法之外,这样代码更具有逻辑连贯性,更加易读,并且优雅。

afterCompletion的坑

这个注册事务的回调代码在我们在我们的业务逻辑中经常会出现,比如某个事务做完之后的刷新缓存,发送消息队列,发送通知消息等等,在日常的使用中,大家用这个基本也没出什么问题,但是在打压的过程中,发现了这一块出现了瓶颈,耗时特别久,通过一系列的监测,发现是从数据库连接池获取连接等待的时间较长,最终我们定位到了afterCompeltion这个动作,居然没有归还数据库连接。

在Spring的AbstractPlatformTransactionManager中,对commit处理的代码如下:

private void processCommit(DefaultTransactionStatus status) throws TransactionException {try {boolean beforeCompletionInvoked = false;try {prepareForCommit(status);triggerBeforeCommit(status);triggerBeforeCompletion(status);beforeCompletionInvoked = true;boolean globalRollbackOnly = false;if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) {globalRollbackOnly = status.isGlobalRollbackOnly();}if (status.hasSavepoint()) {if (status.isDebug()) {logger.debug("Releasing transaction savepoint");}status.releaseHeldSavepoint();}else if (status.isNewTransaction()) {if (status.isDebug()) {logger.debug("Initiating transaction commit");}doCommit(status);}// Throw UnexpectedRollbackException if we have a global rollback-only// marker but still didn't get a corresponding exception from commit.if (globalRollbackOnly) {throw new UnexpectedRollbackException("Transaction silently rolled back because it has been marked as rollback-only");}}// Trigger afterCommit callbacks, with an exception thrown there// propagated to callers but the transaction still considered as committed.try {triggerAfterCommit(status);}finally {triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);}}finally {cleanupAfterCompletion(status);}}

这里我们只需要关注 倒数几行代码即可,可以发现我们的triggerAfterCompletion,是倒数第二个执行逻辑,当执行完所有的代码之后就会执行我们的cleanupAfterCompletion,而我们的归还数据库连接也在这段代码之中,这样就导致了我们获取数据库连接变慢。

如何优化

对于上面的问题如何优化呢?这里有三种方案可以进行优化:

  • 将非DB操作提到事务之外,这种方法也就是我们上面最原始的方法,对于一些简单的逻辑可以提取,但是对于一些复杂的逻辑,比如事务的嵌套,嵌套里面调用了afterCompletion,这样做会增大很多工作量,并且很容易出现问题。
  • 通过多线程异步去做,提升数据库连接池归还速度,这种适合于注册afterCompletion时写在事务最后的时候,直接将需要做的放在其它线程去做。但是如果注册afterCompletion的时候出现在我们事务之间,比如嵌套事务,就会导致我们要做的后续业务逻辑和事务并行。
  • 模仿Spring事务回调注册,实现新的注解。上面两种方法都有各自的弊端,所以最后我们采用了这种方法,实现了一个自定义注解@MethodCallBack,在使用事务的上面都打上这个注解,然后通过类似的注册代码进行。
    @Transactional@MethodCallBackpublic int createOrder(Order order){orderDbStorage.save(order);orderItemDbStorage.save(order.getItems());MethodCallbackHelper.registerOnSuccess(() -> sendSuccessedRpc());MethodCallbackHelper.registerOnThrowable(throwable -> sendFailedRpc());return order.getId();}

通过第三种方法基本只需要把我们注册事务回调的地方都进行替换就可以正常使用了。

再谈大事务

说了这么久大事务,到底什么才是大事务呢?简单点就是事务时间运行得长,那么就是大事务。一般来说导致事务时间运行时间长的因素不外乎下面几种:

  • 数据操作得很多,比如在一个事务里面插入了很多数据,那么这个事务执行时间自然就会变得很长。
  • 锁的竞争大,当所有的连接都同时对同一个数据进行操作,那么就会出现排队等待,事务时间自然就会变长。
  • 事务中有其他非DB操作,比如一些RPC请求,有些人说我的RPC很快的,不会增加事务的运行时间,但是RPC请求本身就是一个不稳定的因素,受很多因素影响,网络波动,下游服务响应缓慢,如果这些因素一旦出现,就会有大量的事务时间很长,有可能导致Mysql挂掉,从而引起雪崩。

上面的三种情况,前面两种可能来说不是特别常见,但是第三种事务中有很多非DB操作,这个是我们非常常见,通常出现这个情况的原因很多时候是我们自己习惯规范,初学者或者一些经验不丰富的人写代码,往往会先写一个大方法,直接在这个方法加上事务注解,然后再往里面补充,哪管他是什么逻辑,一把梭,就像下面这张图一样:

e7104bd04432b25e8678c6d60dff92b4.png

当然还有些人是想搞什么分布式事务,可惜用错了方法,对于分布式事务可以关注Seata,同样可以用一个注解就能帮助你做到分布式事务。

最后

其实最后想想,为什么会出现这种问题呢?一般大家的理解都是会认为都是在完成之后做的了,数据库连接肯定早都释放了,但是事实并非如此。所以,我们使用很多API的时候不能望文生义,如果其没有详细的doc,那么你应该更加深入了解其实现细节。

当然最后希望大家写代码之前尽量还是不要一把梭,认真对待每一句代码。

作者:咖啡拿铁
链接:https://juejin.im/post/5dce0de8e51d45400425aeb7

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/465475.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

多布局怎么搭建_怎么制作网页?网页制作基本步骤

怎么制作网页?网页制作基本步骤,互联网时代,各行各业的企业为了适应时代的发展,纷纷从线下转移至线上来拓展自己的业务。若想让更多人了解自己的企业,拥有一个专属网站至关重要。网站不仅可以详细介绍企业信息,更重要…

能运行shell吗_terminal, shell, bash, zsh

##秋天的第一杯奶茶梗?Terminal: shell运行在里面。A terminal refers to a wrapper program that runs a shell.Shell:The shell is the program that actually processes commands and returns outputs. Most shells also manage foreground and background processes, comm…

OSPF的虚链路配置

OSPF的虚链路配置<?xml:namespace prefix o ns "urn:schemas-microsoft-com:office:office" />OSPF多区要求普通区域必须和骨干区域&#xff08;area 0&#xff09;相连&#xff0c;如果一个区域不能和骨干区域直接相连&#xff0c;可以使用虚链路把该区域逻…

webmvcconfigurer配置跨域_为什么加了 Spring Security 会导致 Spring Boot 跨域失效呢?...

点击上方 IT牧场 &#xff0c;选择 置顶或者星标技术干货每日送达作者&#xff1a;欧阳我去链接&#xff1a;https://segmentfault.com/a/1190000019485883作为一个后端开发&#xff0c;我们经常遇到的一个问题就是需要配置 CORS&#xff0c;好让我们的前端能够访问到我们的 AP…

[jQuery]3D效果的标签云

刚才看了篇园友关于如何自定义标签云的文章&#xff0c;心痒痒自己也想弄一个&#xff0c;其实原理非常简单&#xff0c;就是动态load标签页里的标签&#xff0c;按需要的格式重新动态生成DOM结构&#xff0c;再通过第三方的js插件(他们用的是Google Visualization API Gallery…

1071svm函数 r语言_R语言机器学习之核心包e1071 - 数据分析

R语言有很多包可以做机器学习(Machine Learning)的任务。机器学习的任务主要有有监督的学习方式和无监督的学习方式。有监督学习&#xff1a;在正确结果指导下的学习方式&#xff0c;若是正确结果是定性的&#xff0c;属于分类问题&#xff1b;若正确结果是定量的&#xff0c;属…

重装系统失败后怎么用好系统U盘启动解决?

很多朋友在第一次重装电脑系统时都会出现这样或那样的错误&#xff0c;导致重装系统后进不去系统&#xff0c;非常的被动。那么当我们遇到这种情况该怎么办呢&#xff1f;其实我们可以使用好系统U盘启动来解决重装系统后进不去的问题。 好系统U盘启动解决重装系统后进不去的方法…

MEncoder 使用实例

MEncoder 是一个简单的影片编码程序&#xff0c;它可用于将视频或者音频文件在 MPlayer 可播放的格式当中互相转换。说到 MEncoder 就不得不知道 MPlayer。MPlayer 是一款为 Linux 编写的电影播放器。它能播放大部分 XAnim、RealPlayer 以及 Win32 DLL 支持的 MPEG、VOB、AVI、…

div为空的时候 浮动没有效果_3种CSS清除浮动的方法

点击上方 "前端技术精选" 关注&#xff0c;星标或者置顶12点00分准时推送&#xff0c;第一时间送达作者&#xff1a;html中文网 | 编辑&#xff1a;前端妹来源&#xff1a;html.cn/web/css/19613.html前端技术精选(ID&#xff1a;FrontEndTech)第 55 次推文 图源&…

3. 中间件安全基础(三)

0x00 前言 前两篇文章我们对六款中间件的基本信息和相关的安全配置做了介绍&#xff0c;这篇文章我们主要就中间件常见的漏洞利用方式及修复方法做出讲解。如果某些地方存在疑问可以对比着前两篇文章阅读&#xff0c;更好地加深理解。 0x01 Apache 解析漏洞是指非程序文件被异常…

php mysql 删除语句怎么写_php mysql 删除语句是什么

php mysql删除语句是Delete&#xff0c;DELETE语句用于从数据库表中删除行&#xff0c;其语法是“DELETE FROM table_name WHERE some_column some_value”。PHP MySQL DeleteDELETE 语句用于从数据库表中删除行。删除数据库中的数据DELETE FROM 语句用于从数据库表中删除记录…

mysql数据排序指令_MySQL 排序 | 菜鸟教程

MySQL 排序我们知道从 MySQL 表中使用 SQL SELECT 语句来读取数据。如果我们需要对读取的数据进行排序&#xff0c;我们就可以使用 MySQL 的 ORDER BY 子句来设定你想按哪个字段哪种方式来进行排序&#xff0c;再返回搜索结果。语法以下是 SQL SELECT 语句使用 ORDER BY 子句将…

mysql配置环境变量(win 10)_mysql配置环境变量(win 10)

1、安装完mysql后就需要配置环境变量 (win 10)选择“我的电脑”&#xff0c;单击右键&#xff0c;选择“属性->高级->环境变量中的系统变量&#xff0c;对 MYSQL_HOME、Path 这 2 个系统变量分别设置如下相应的值(设置原则&#xff1a;如果存在相应的变量&#xff0c;直接…

017-通过govendor管理依赖包

1&#xff1a;安装 go get -u github.com/kardianos/govendor 2&#xff1a;配置环境变量 需要把 $GOPATH/bin/ 加到 PATH 中 D:\my_workspace\go_ws\bin 3&#xff1a;在$GOPATH/src目录下新建测试工程go_test,然后再此目录下新建src目录 4&#xff1a;在go_test目录执行&…

《鸟哥的Linux基础》

硬件设备 LINUX历史 主机规划与磁盘分配 man , info 文件权限与目录配置 SUID: 二进制程序的执行者可以获取root的权限SGID: 目录或文件的执行者可以获取改程序群组的权限SBIT: 目录下创建的文件或目录只有创建者有权限删除r&#xff1a; 文件: 可读目录: 可获取文件列表w:…

pythontuple([1、2、3)_Python 语句\nprint(tuple([1,2,3]))\n的运行结果是

在5%之的阶段化率城市发展间的阶段加速是城市化&#xff0c;语句运行表明展的各国共同规律化发城市世界。结果礼仪又叫距离距离私人。语句运行作高尔奠基会主义现义的基的是社实主。表述下列对此的是错误&#xff0c;号文年中出央一件提&#xff0c;品价重要制和制度革完格形农…

Silverlight实例教程 - Out of Browser的自定义应用

在上两篇教程中&#xff0c;讲述了Silverlight的Out of Browser理论知识和基础实践。本节将讲述如何创建自定义的Out of Browser应用以及如何调试Silverlight的Out of Browser应用。Silverlight Out of Browser的自定义化从Silverlight 4开始,OOB应用支持信任权限设置和窗口自定…

python基于opencv的手势识别_怎么在Python3.5 中利用OpenCV实现一个手势识别功能

怎么在Python3.5 中利用OpenCV实现一个手势识别功能发布时间&#xff1a;2020-12-22 11:56:32来源&#xff1a;亿速云阅读&#xff1a;67作者&#xff1a;Leah怎么在Python3.5 中利用OpenCV实现一个手势识别功能&#xff1f;相信很多没有经验的人对此束手无策&#xff0c;为此本…

上班玩手机被辞退

今天下班回来&#xff0c;看到脉脉上的一则信息&#xff0c;有一位同学从事IOS开发&#xff0c;但是因为公司没有项目做&#xff0c;然后就只能闲着没事干。公司想辞退这位同学&#xff0c;但是又想不出好的办法&#xff0c;因为他确实没有做什么违反纪律的事。然后猛生一记&am…

CSP 1.0 语言规范

为什么80%的码农都做不了架构师&#xff1f;>>> 点击在线查看wiki版本&#xff1a;CSP1.0语言规范 点击下载PDF版本&#xff1a;CSP1.0语言规范 转载于:https://my.oschina.net/akee/blog/6020