Spring官方推荐的@Transactional还能导致生产事故?

在Spring中进行事务管理非常简单,只需要在方法上加上注解@Transactional,Spring就可以自动帮我们进行事务的开启、提交、回滚操作。甚至很多人心里已经将Spring事务与@Transactional划上了等号,只要有数据库相关操作就直接给方法加上@Transactional注解。

不瞒你说,我之前也一直是这样,直到使用@Transactional导致了一次生产事故,而那次生产事故还导致我当月绩效被打了D...

@Transactional导致的生产事故

19年在公司做了一个内部报销的项目,有这样一个业务逻辑:

1、员工加班打车可以通过滴滴出行企业版直接打车,第二天打车费用可以直接同步到我们的报销平台

2、员工可以在报销平台勾选自己打车费用并创建一张报销单进行报销,创建报销单的同时会创建一条审批流(统一流程平台)让领导审批

当时创建报销单的代码是这么写的:

/*** 保存报销单并创建工作流*/
@Transactional(rollbackFor = Exception.class)
public void save(RequestBillDTO requestBillDTO){//调用流程HTTP接口创建工作流workflowUtil.createFlow("BILL",requestBillDTO);//转换DTO对象RequestBill requestBill = JkMappingUtils.convert(requestBillDTO, RequestBill.class);requestBillDao.save(requestBill);//保存明细表requestDetailDao.save(requestBill.getDetail())
}

代码非常简单也很 “优雅”,先通过http接口调用工作流引擎创建审批流,然后保存报销单,而为了保证操作的事务,在整个方法上加上了@Transactional注解(仔细想想,这样真的能保证事务吗?)。

报销项目属于公司内部项目,本身是没什么高并发的,系统也一直稳定运行着。

在年末的一天下午(前几天刚好下了大雪,打车的人特别多),公司发通知邮件说年度报销窗口即将关闭,需要尽快将未报销的费用报销掉,而刚好那天工作流引擎在进行安全加固。

收到邮件后报销的人开始逐渐增多,在接近下班的时候到达顶峰,此时报销系统开始出现了故障:数据库监控平台一直收到告警短信,数据库连接不足,出现大量死锁;日志显示调用流程引擎接口出现大量超时;同时一直提示CannotGetJdbcConnectionException,数据库连接池连接占满。

在发生故障后,我们尝试过杀掉死锁进程,也进行过暴力重启,只是不到10分钟故障再次出现,收到大量电话投诉。
最后没办法只能向全员发送停机维护邮件并发送故障报告,而后,绩效被打了个D,惨...。

事故原因分析

通过对日志的分析我们很容易就可以定位到故障原因就是保存报销单的save()方法,而罪魁祸首就是那个@Transactional注解。

我们知道@Transactional 注解,是使用 AOP 实现的,本质就是在目标方法执行前后进行拦截。在目标方法执行前加入或创建一个事务,在执行方法执行后,根据实际情况选择提交或是回滚事务。

当 Spring 遇到该注解时,会自动从数据库连接池中获取 connection,并开启事务然后绑定到 ThreadLocal 上,对于@Transactional注解包裹的整个方法都是使用同一个connection连接。如果我们出现了耗时的操作,比如第三方接口调用,业务逻辑复杂,大批量数据处理等就会导致我们我们占用这个connection的时间会很长,数据库连接一直被占用不释放。一旦类似操作过多,就会导致数据库连接池耗尽。

在一个事务中执行RPC操作导致数据库连接池撑爆属于是典型的长事务问题,类似的操作还有在事务中进行大量数据查询,业务规则处理等...

何为长事务?

顾名思义就是运行时间比较长,长时间未提交的事务,也可以称之为大事务。

长事务会引发哪些问题?

长事务引发的常见危害有:

  1. 数据库连接池被占满,应用无法获取连接资源;

  2. 容易引发数据库死锁;

  3. 数据库回滚时间长;

  4. 在主从架构中会导致主从延时变大。

如何避免长事务?

既然知道了长事务的危害,那如何在开发中避免出现长事务问题呢?

很明显,解决长事务的宗旨就是 对事务方法进行拆分,尽量让事务变小,变快,减小事务的颗粒度。

既然提到了事务的颗粒度,我们就先回顾一下Spring进行事务管理的方式。

声明式事务

首先我们要知道,通过在方法上使用@Transactional注解进行事务管理的操作叫声明式事务 。

使用声明式事务的优点 很明显,就是使用很简单,可以自动帮我们进行事务的开启、提交以及回滚等操作。使用这种方式,程序员只需要关注业务逻辑就可以了。

声明式事务有一个最大的缺点,就是事务的颗粒度是整个方法,无法进行精细化控制。

与声明式事务对应的就是编程式事务。

基于底层的API,开发者在代码中手动的管理事务的开启、提交、回滚等操作。在spring项目中可以使用TransactionTemplate类的对象,手动控制事务。

@Autowired 
private TransactionTemplate transactionTemplate; ... public void save(RequestBill requestBill) { transactionTemplate.execute(transactionStatus -> {requestBillDao.save(requestBill);//保存明细表requestDetailDao.save(requestBill.getDetail());return Boolean.TRUE; });
}

使用编程式事务最大的好处就是可以精细化控制事务范围。

所以避免长事务最简单的方法就是不要使用声明式事务@Transactional,而是使用编程式事务手动控制事务范围。

有的同学会说,@Transactional使用这么简单,有没有办法既可以使用@Transactional,又能避免产生长事务?

e4a51c63476f2ab41ab8c416b34d9cb9.png

那就需要对方法进行拆分,将不需要事务管理的逻辑与事务操作分开:

@Service
public class OrderService{public void createOrder(OrderCreateDTO createDTO){query();validate();saveData(createDTO);}//事务操作@Transactional(rollbackFor = Throwable.class)public void saveData(OrderCreateDTO createDTO){orderDao.insert(createDTO);}
}

query()validate()不需要事务,我们将其与事务方法saveData()拆开。

当然,这种拆分会命中使用@Transactional注解时事务不生效的经典场景,很多新手非常容易犯这个错误。@Transactional注解的声明式事务是通过spring aop起作用的,而spring aop需要生成代理对象,直接在同一个类中方法调用使用的还是原始对象,事务不生效。其他几个常见的事务不生效的场景为:

  • @Transactional 应用在非 public 修饰的方法上

  • @Transactional 注解属性 propagation 设置错误

  • @Transactional 注解属性 rollbackFor 设置错误

  • 同一个类中方法调用,导致@Transactional失效

  • 异常被catch捕获导致@Transactional失效

正确的拆分方法应该使用下面两种:

  1. 可以将方法放入另一个类,如新增 manager层,通过spring注入,这样符合了在对象之间调用的条件。

@Service
public class OrderService{@Autowiredprivate OrderManager orderManager;public void createOrder(OrderCreateDTO createDTO){query();validate();orderManager.saveData(createDTO);}
}@Service
public class OrderManager{@Autowiredprivate OrderDao orderDao;@Transactional(rollbackFor = Throwable.class)public void saveData(OrderCreateDTO createDTO){orderDao.saveData(createDTO);}
}
  1. 启动类添加@EnableAspectJAutoProxy(exposeProxy = true),方法内使用AopContext.currentProxy()获得代理类,使用事务。

SpringBootApplication.java@EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
public class SpringBootApplication {}
OrderService.javapublic void createOrder(OrderCreateDTO createDTO){OrderService orderService = (OrderService)AopContext.currentProxy();orderService.saveData(createDTO);
}

小结

使用@Transactional注解在开发时确实很方便,但是稍微不注意就可能出现长事务问题。所以对于复杂业务逻辑,我这里更建议你使用编程式事务来管理事务,当然,如果你非要使用@Transactional,可以根据上文提到的两种方案进行方法拆分。

690fefe6885cf541a5c2d3aea7649681.gif

往期推荐

3cb31b2fc7e17c36a641f0b1fb3b15b6.png

Spring Boot 如何解决多个定时任务阻塞问题?


775eba2da42fafd7c5c3273d1b012bc7.png

Objects.equals有坑


9f69f8733dd7beddc5413859302d4f0c.png

Java 18 正式发布,默认 UTF-8,finalize 被弃用,别再乱用了!


19fc63214d17a7d52862d71b55891e39.gif

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

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

相关文章

python函数实例化_用Python实例化函数

python函数实例化In terms of Mathematics and Computer science, currying is the approach/technique by which we can break multiple-argument function to single argument function. 从数学和计算机科学的角度来看, 柯里化是一种方法/技术,通过它我…

京东二面:MySQL 主从延迟、读写分离 7 种解决方案!

我们都知道互联网数据有个特性,大部分场景都是 读多写少,比如:微博、微信、淘宝电商,按照 二八原则,读流量占比甚至能达到 90%结合这个特性,我们对底层的数据库架构也会做相应调整。采用 读写分离处理过程&…

array_keys_PHP array_keys()函数与示例

array_keysPHP array_keys()函数 (PHP array_keys() function) array_keys() function is used to get the keys of an array, it accepts an array as an argument and returns a new array containing keys. array_keys()函数用于获取数组的键,它接受一个数组作为…

再见Postman,这款API神器更好用!

代码未动,文档先行其实大家都知道 API 文档先行的重要性,但是在实践过程中往往会遇到很多困难。程序员最讨厌的两件事:1. 写文档,2. 别人不写文档。大多数开发人员不愿意写 API 文档的原因是写文档短期收益远低于付出的成本&#…

strtolower_PHP strtolower()函数与示例

strtolowerPHP strtolower()函数 (PHP strtolower() function) strtolower() function is a string function, it accepts the string and returns an lowercase string. strtolower()函数是一个字符串函数,它接受该字符串并返回小写字符串。 Syntax: 句法&#xf…

如何保证数据库和缓存双写一致性?

前言数据库和缓存(比如:redis)双写数据一致性问题,是一个跟开发语言无关的公共问题。尤其在高并发的场景下,这个问题变得更加严重。我很负责的告诉大家,该问题无论在面试,还是工作中遇到的概率非…

面试官:AtomicInteger是如何保证线程安全?

blog.csdn.net/nanhuaibeian/article/details/120936139一、为什么引入 AtomicInteger ?谈到线程安全,会首先想到了synchronized 和 Lock,但是这种方式又有一个名字,叫做互斥锁,一次只能有一个持有锁的线程进入,再加上…

机器学习 训练验证测试_测试前验证| 机器学习

机器学习 训练验证测试In my previous article, we have discussed about the need to train and test our model and we wrote a code to split the given data into training and test sets. 在上一篇文章中,我们讨论了训练和测试模型的必要性,并编写了…

如何判断线程池已经执行完所有任务了?

作者 | 磊哥来源 | Java面试真题解析(ID:aimianshi666)转载请联系授权(微信ID:GG_Stone)很多场景下,我们需要等待线程池的所有任务都执行完,然后再进行下一步操作。对于线程 Thread …

IRCTC的完整形式是什么?

IRCTC:印度铁路餐饮和旅游公司 (IRCTC: Indian Railways Catering and Tourism Corporation) IRCTC is an abbreviation of Indian Railways Catering and Tourism Corporation. It is a subsidiary of the Indian Railway established by the Ministry of Railways…

分布式锁的 3 种实现方案!

前言 大家好,我是磊哥。今天跟大家探讨一下分布式锁的设计与实现。希望对大家有帮助,如果有不正确的地方,欢迎指出,一起学习,一起进步哈~分布式锁概述数据库分布式锁Redis分布式锁Zookeeper分布式锁三种分布式锁对比1.…

java学习笔记16--异常

java学习笔记16--异常 异常 异常时导致程序中断运行的一种指令流,如果不对异常进行正确的处理,则可能导致程序的中断执行,造成不必要的损失, 所以在程序的设计中必须要考虑各种异常的发生,并正确的做好相应的处理&am…

ruby hash添加数据_如何在Ruby中向Hash添加元素?

ruby hash添加数据Before going through the ways to add elements to the hash instances, let us understand what could be called as a hash element. So, Hash is the collection of keys and their values. For example, 在介绍向哈希实例添加元素的方法之前,…

线程安全问题的 3 种解决方案!

作者 | 磊哥来源 | Java面试真题解析(ID:aimianshi666)转载请联系授权(微信ID:GG_Stone)线程安全是指某个方法或某段代码,在多线程中能够正确的执行,不会出现数据不一致或数据污染的…

黑色30s高并发IIS设置

在这篇博文中,我们抛开对阿里云的怀疑,完全从ASP.NET的角度进行分析,看能不能找到针对问题现象的更合理的解释。 “黑色30秒”问题现象的主要特征是:排队的请求(Requests Queued)突增,到达HTTP.…

我们可以覆盖Java中的main()方法吗?

The question is that "Can we override main() method in Java?" 问题是“我们可以覆盖Java中的main()方法吗?” No, we cant override the main() method in java. 不,我们不能覆盖java中的main()方法 。 First, we will understand what …

一文读懂MySQL查询语句的执行过程

需要从数据库检索某些符合要求的数据,我们很容易写出 Select A B C FROM T WHERE ID XX 这样的SQL,那么当我们向数据库发送这样一个请求时,数据库到底做了什么?我们今天以MYSQL为例,揭示一下MySQL数据库的查询过程&a…

angularJS的$http.post请求,.net后台接收不到参数值的解决方案

JS通用部分var shoppingCartModule angular.module(starter, [ionic], function ($httpProvider) {// Use x-www-form-urlencoded Content-Type$httpProvider.defaults.headers.post[Content-Type] application/x-www-form-urlencoded;charsetutf-8;/*** The workhorse; conve…

带有示例的Python列表reverse()方法

列出reverse()方法 (List reverse() Method) reverse() method is used to reverse the elements of the list, the method is called with this list (list in which we have to reverse the elements) and it reverses all elements in the list. reverse()方法用于反转列表中…

复杂度O(n)倒转链表

1 public class ListNode {2 int val;3 ListNode next;4 ListNode(int x) { val x; }5 ListNode(){}6 7 public static ListNode revese(ListNode input)8 {9 ListNode head new ListNode();//头插法的头 10 ListNode cur in…