Spring Boot业务代码中使用@Transactional事务失效总结

1、概述

我们知道 Spring 声明式事务功能提供了极其方便的事务配置方式,配合 Spring Boot 的自动配置,大多数 Spring Boot 项目只需要在方法上标记 @Transactional注解,即可一键开启方法的事务性配置。当然后端开发人员对数据库事务这个概念并不陌生,也知道如果整体考虑多个数据库操作要么成功要么失败时,需要通过数据库事务来实现多个操作的一致性和原子性。如下所示:

 @Override@Transactional(rollbackFor = Exception.class)public void addUser(UserParam param) {User user = PtcBeanUtils.copy(param, User.class);userDAO.insert(user);if (!CollectionUtils.isEmpty(param.getRoleIds())) {userRoleService.addUserRole(user.getId(), param.getRoleIds());}}

大多数开发仅限于为方法标记 @Transactional来开启声明式事务,认为就可以高枕无忧了,不会去关注事务是否有效、出错后事务是否正确回滚,也不会考虑复杂的业务代码中涉及多个子业务逻辑时,怎么正确处理事务。事务没有被正确处理,一般来说不会过于影响正常流程,也不容易在测试阶段被发现。但当系统越来越复杂、压力越来越大之后,就会带来大量的数据不一致问题,随后就是大量的人工介入查看和修复数据。

正是因为声明式事务@Transactional使用简单,所以很多开发人员不注重细节点,但是@Transactional条条框框还蛮多的,可谓是细节点拉满,如果不注意也不小心就会掉进坑里,今天就让我们一起来了解使用细节,把坑填平咯

2、@Transactional理解 

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {@AliasFor("transactionManager")String value() default "";@AliasFor("value")String transactionManager() default "";Propagation propagation() default Propagation.REQUIRED;Isolation isolation() default Isolation.DEFAULT;int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;boolean readOnly() default false;Class<? extends Throwable>[] rollbackFor() default {};String[] rollbackForClassName() default {};Class<? extends Throwable>[] noRollbackFor() default {};String[] noRollbackForClassName() default {};}

 从上面看出@Transactional既可以作用于类上,也可以作用于方法上,作用于类: 表示所有该类的public方法都配置相同的事务属性信息。接下来再看看其属性:

 propagation: 设置事务的传播行为,主要解决是A方法调用B方法时,事务的传播方式问题的,默认值为 Propagation.REQUIRED,其他属性值信息如下:

事务传播行为

解释

REQUIRED(默认值)A调用B,B需要事务,如果A有事务B就加入A的事务中,如果A没有事务,B就自己创建一个事务
REQUIRED_NEWA调用B,B需要新事务,如果A有事务就挂起,B自己创建一个新的事务
SUPPORTSA调用B,B有无事务无所谓,A有事务就加入到A事务中,A无事务B就以非事务方式执行
NOT_SUPPORTSA调用B,B以无事务方式执行,A如有事务则挂起
NEVERA调用B,B以无事务方式执行,A如有事务则抛出异常
MANDATORYA调用B,B要加入A的事务中,如果A无事务就抛出异常
NESTEDA调用B,B创建一个新事务,A有事务就作为嵌套事务存在,A没事务就以创建的新事务执行

isolation : 事务的隔离级别,默认值为 Isolation.DEFAULT。指定事务的隔离级别,事务并发存在三大问题:脏读、不可重复读、幻读/虚读。可以通过设置事务的隔离级别来保证并发问题的出现,常用的是READ_COMMITTED 和REPEATABLE_READ 

isolation属性

解释

DEFAULT

默认隔离级别,取决于当前数据库隔离级别,例如MySQL默认隔离级别是REPEATABLE_READ

READ_UNCOMMITTED

A事务可以读取到B事务尚未提交的事务记录,不能解决任何并发问题,安全性最低,性能最高

READ_COMMITTED

A事务只能读取到其他事务已经提交的记录,不能读取到未提交的记录。可以解决脏读问题,但是不能解决不可重复读和幻读

REPEATABLE_READ

A事务多次从数据库读取某条记录结果一致,可以解决不可重复读,不可以解决幻读

SERIALIZABLE

串行化,可以解决任何并发问题,安全性最高,但是性能最低

timeout : 事务的超时时间,默认值为 -1。如果超过该时间限制但事务还没有完成,则自动回滚事务。

readOnly: 指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。

rollbackFor: 用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。

noRollbackFor: 抛出指定的异常类型,不回滚事务,也可以指定多个异常类型。

 3、@Transactional失效场景、原因及修正方式

3.1 同一个类中的方法通过this调用导致失效

public void addUser(UserParam param) {User user = PtcBeanUtils.copy(param, User.class);// 新增用户userDAO.insert(user);// 添加用户角色this.addUserRole(user.getId(), param.getRoleIds());log.info("执行结束了");}@Transactional(rollbackFor = Exception.class)public void addUserRole(Long userId, List<Long> roleIds) {if (CollectionUtils.isEmpty(roleIds)) {return;}List<UserRole> userRoles = new ArrayList<>();roleIds.forEach(roleId -> {UserRole userRole = new UserRole();userRole.setUserId(userId);userRole.setRoleId(roleId);userRoles.add(userRole);});userRoleDAO.insertBatch(userRoles);throw new RuntimeException("发生异常咯");}

执行#addUser()会发现事务控制失效,发生异常事务并没有回滚,用户和角色绑定都插入成功了。

这里,我给出@Transactional生效原则 1,必须通过代理过的类从外部调用目标方法才能生效.

Spring 是通过 AOP 技术对方法进行增强实现事务控制的,要调用增强过的方法必然是调用代理后的对象,而这里this是原生对象,并不是代理,自然就没有事务控制了。 

解决方法:

1、将addUser()方法开启事务即加上@Transactional(rollbackFor = Exception.class),这里本就该开启。里面的方式自动延用主方法的事务配置

2、将this换成代理的userService, 可以自己注入自己@Resource private UserService userService,当然也可以不用注入,直接在Spring容器中获取userService这个bean

3、通过开启aop的代理来获取当前service对象,然后来获取对象,思路和第二种方式类似

3.2 异常被catch“吃掉了”导致@Transactional失效

如下所示:

@Transactional(rollbackFor = Exception.class)public void addUser(UserParam param) {try {User user = PtcBeanUtils.copy(param, User.class);// 完成一些逻辑处理.......// 添加用户角色this.addUserRole(user.getId(), param.getRoleIds());log.info("执行结束了");} catch (Exception e) {log.error(e.getMessage());}}@Transactional(rollbackFor = Exception.class)public void addUserRole(Long userId, List<Long> roleIds) {if (CollectionUtils.isEmpty(roleIds)) {return;}List<UserRole> userRoles = new ArrayList<>();roleIds.forEach(roleId -> {UserRole userRole = new UserRole();userRole.setUserId(userId);userRole.setRoleId(roleId);userRoles.add(userRole);});userRoleDAO.insertBatch(userRoles);throw new RuntimeException("发生异常咯");}

 @Transactional生效原则2:只有异常传播出了标记了 @Transactional 注解的方法,事务才能回滚。之前我们总结过 基于AOP事务控制实现原理说过在 Spring的 TransactionAspectSupport里有个 invokeWithinTransaction 方法,里面就是处理事务的逻辑。可以看到,只有捕获到异常才能进行后续事务处理:

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,final InvocationCallback invocation) throws Throwable {......try {// This is an around advice: Invoke the next interceptor in the chain.// This will normally result in a target object being invoked.retVal = invocation.proceedWithInvocation();}catch (Throwable ex) {// target invocation exception// 捕获到异常,进行回滚操作,如果我们在业务方法已经捕获掉异常,这里就捕获不到了,自然就不会回滚了completeTransactionAfterThrowing(txInfo, ex);throw ex;}finally {cleanupTransactionInfo(txInfo);}......return result;}}

可以看到,只有捕获到异常时才进行回滚操作,如果我们在业务方法已经捕获掉异常,这里就捕获不到了,自然就不会回滚了。

解决方法:

就是对异常捕获尽量做到局部针对操作,不要笼统把整个方法的代码逻辑都包括进行,这样异常就抛出去了。如果是实在要对代码块进行catch,就用手动回滚的方式去恢复数据(也可以理解为TCC模式)

3.3 @Transactional 属性 rollbackFor 设置错误,导致异常不满足回滚条件

@Transactionalpublic void addUser(UserParam param) {User user = PtcBeanUtils.copy(param, User.class);.......// 添加用户角色this.addUserRole(user.getId(), param.getRoleIds());log.info("执行结束了");}public void addUserRole(Long userId, List<Long> roleIds) throws Exception {if (CollectionUtils.isEmpty(roleIds)) {return;}List<UserRole> userRoles = new ArrayList<>();roleIds.forEach(roleId -> {UserRole userRole = new UserRole();userRole.setUserId(userId);userRole.setRoleId(roleId);userRoles.add(userRole);});userRoleDAO.insertBatch(userRoles);throw new Exception("发生异常咯");}

这里#addUser()使用@transactional,但没有设置rollbackFor属性,且#addUserRole()抛出的异常是exception,不是RuntimeException,这样事务也失效了,因为默认情况下,出现 RuntimeException(非受检异常)或 Error 的时候,Spring才会回滚事务

从上面3.2小节的completeTransactionAfterThrowing(txInfo, ex);进去完成回滚操作会判断异常类型是否满足规定,DefaultTransactionAttribute 类能看到如下代码块,可以发现相关证据,通过注释也能看到 Spring 这么做的原因,大概的意思是受检异常一般是业务异常,或者说是类似另一种方法的返回值,出现这样的异常可能业务还能完成,所以不会主动回滚;而Error 或 RuntimeException 代表了非预期的结果,应该回滚:

 public boolean rollbackOn(Throwable ex) {return (ex instanceof RuntimeException || ex instanceof Error);}

 解决方法:

设置rollbackFor:@Transactional(rollbackFor = Exception.class)。这一点同样很通用,就如果下面的方法抛出的是exception的子类用这个来接收统一处理也是可以的。

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

@Transactional(rollbackFor = Exception.class)private void addUserRole(Long userId, List<Long> roleIds) {if (CollectionUtils.isEmpty(roleIds)) {return;}List<UserRole> userRoles = new ArrayList<>();roleIds.forEach(roleId -> {UserRole userRole = new UserRole();userRole.setUserId(userId);userRole.setRoleId(roleId);userRoles.add(userRole);});userRoleDAO.insertBatch(userRoles);throw new RuntimeException("发生异常咯");}

 首先在编译阶段,idea也会提示爆红。提示private的问题。

原因是因为Spring通过CGLIB动态代理来增强生产代理对象,CGLIB 通过继承方式实现代理类,private 方法在子类不可见,自然也就无法进行事务增强。会调用到AbstractFallbackTransactionAttributeSource的computeTransactionAttribute()方法

解决方法:改成public

 3.5 @Transactional 注解传播属性 propagation 设置错误

如上面我们新增的用户的同时要添加用户角色,但是假如我们希望即使添加角色错误了,还可以正常新增用户。 会发现都是同时成功或是同时失败。

原因是,主方法添加用户的逻辑和子方法添加用户角色的逻辑是同一个事务,子逻辑标记了事务需要回滚,主逻辑自然也不能提交了。

解决方法:添加一个新的事务即可。这个场景在mybatisplus的@DB切换多数据源的事务时候也会出现(如果有遇到过)

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)

3.6 @Transactional长事务导致生产事故 

很多开发都觉得Spring的声明式事务使用非常简单,即@Transactional,所以从来不注重细节。当 Spring 遇到该注解时,会自动从数据库连接池中获取 connection,并开启事务然后绑定到 ThreadLocal 上,对于@Transactional注解包裹的整个方法都是使用同一个connection连接

如果我们出现了耗时的操作,比如第三方接口调用、业务逻辑复杂、大批量数据处理等就会导致我们我们占用这个connection的时间会很长,数据库连接一直被占用不释放。一旦类似操作过多,就会导致数据库连接池耗尽。这就是典型的长事务问题

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

  1. 数据库连接池被占满,应用无法获取连接资源;
  2. 容易引发数据库死锁;
  3. 数据库回滚时间长;
  4. 在主从架构中会导致主从延时变大。

服务系统开始出现故障:数据库监控平台一直收到告警短信,数据库连接不足,出现大量死锁;日志显示调用流程引擎接口出现大量超时;同时一直提示CannotGetJdbcConnectionException,数据库连接池连接占满。

要想解决这个问题其实也不难,只需要对方法进行拆分,将不需要事务管理的逻辑与事务操作分开,这样就可以有效控制事务的时长从而避免长事务。当然对一个方法逻辑拆分成多个子方法很有可能造成上面叙述的事务不生效的情况

Spring的声明式事务使用@Transactional注解在开发时确实很方便,但是稍有不慎使用不当就会导致事务失效数据不一致、甚至是系统数据库性能问题。所以上面满满的干货总结都是出自日常工作中碰到的,有效帮你避坑。

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

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

相关文章

模型评估指标

1.回归模型 回归模型常常使用MSE均方误差&#xff0c;预测值与真实值之间的平均差距 2.分类模型 2.1 Accuracy正确率 分类正确的数目的占比 但在类别不平衡的情况下&#xff0c;模型可能倾向于预测占多数的类别&#xff0c;导致Acc高但对少数类别的预测效果其实比较差的。…

Vue脚手架 Vue CLI安装

目录 0.为什么要安装Vue CLI脚手架 1.配置方法 1.全局安装 (一次) 2.查看Vue版本&#xff08;一次&#xff09; 报错&#xff1a;出现禁止运行脚本 3.创建项目架子&#xff08;可多次&#xff09; 报错npm err! 问题&#xff1a;已知npm换过国内源&#xff0c;且进度条…

go-zero目录结构和说明

. ├── code-of-conduct.md 行为准则 ├── CONTRIBUTING.md 贡献指南 ├── core 框架的核心组件 │ ├── bloom 布隆过滤器&#xff0c;用于检测一个元素是否在一个集合中 │ ├── breaker 熔断器&am…

MicroPython相关教程

WebRepl MicroPython-ESP32之WebRepl-1Z实验室 - 简书 https://www.jianshu.com/p/c2ddd4fd05be ESP32上面webrepl的开启与连接 - 简书 https://www.jianshu.com/p/f4163eae4a05 Esp32安装micropython和配置webrepl记录备忘 - 哔哩哔哩 https://www.bilibili.com/read/cv121…

工资计算_分支结构 C语言xdoj63

问题描述 小明的公司每个月给小明发工资&#xff0c;而小明拿到的工资为交完个人所得税之后的工资。假设他一个月的税前工资为S元&#xff0c;则他应交的个人所得税按如下公式计算&#xff1a; 1&#xff09; 个人所得税起征点为3500元&#xff0c;若S不超过3500&#xff0c;则…

数据挖掘目标(客户价值分析)

import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as snsIn [2]: datapd.read_csv(r../教师文件/air_data.csv)In [3]: data.head()Out[3]: Start_timeEnd_timeFareCityAgeFlight_countAvg_discountFlight_mileage02011/08/182014/0…

android 13.0 app应用安装黑名单

1.概述 在13.0系统rom定制化开发中,客户需求要实现应用安装黑名单功能,在白名单之中的应用可以安装,其他的app不准安装,实现一个 控制app安装的功能,这需要从app安装流程入手就可以实现功能 PMS就是负责管理app安装的,功能就添加在这里就可以了,接下来看具体实现这个功能…

uniapp播放 m3u8格式视频 兼容pc和移动端

支持全自动播放、设置参数 自己摸索出来的,花了一天时间,给点订阅支持下,订阅后,不懂的地方可以私聊我。 代码实现 代码实现 1.安装dplayer组件 npm i dplayer2. static/index.html下引入 hls 引入hls.min.js 可以存放在static项目hls下面<script src="/static…

PyGame图形绘制函数详解

文章目录 五种图形矩形圆形 五种图形 除了直线之外&#xff0c;pygame中提供了多种图形绘制函数&#xff0c;除了必要的绘图窗口、颜色以及放在最后的线条宽度之外&#xff0c;它们的参数如下表所示 函数图形参数/类型rect矩形Rectellipse椭圆Rectarc椭圆弧Rect, st, edcircl…

Pytorch当中的.detach()操作是什么意思

.detach() 是 PyTorch 中用于从计算图中分离张量的方法。当我们在PyTorch中进行张量运算时&#xff0c;操作会构建一个计算图来跟踪计算历史&#xff0c;这个计算图用于自动求导和反向传播来计算梯度。 使用.detach()方法可以将一个张量从当前的计算图中分离出来&#xff0c;使…

如何实现填表后分配序列号、活动抢票抽奖、自助分配座位号?

&#x1f4f1;发布者想要实现让用户在填表后自动分配序列号、座位号&#xff0c;或制作活动抢票抽奖系统&#xff0c;该如何实现&#xff1f; &#x1f4cc;使用教程 &#x1f4d6;案例1&#xff1a;制作活动抽奖系统 使用预置数据分配的随机分配功能&#xff0c;以活动抽奖为例…

《C++新经典设计模式》之第9章 命令模式

《C新经典设计模式》之第9章 命令模式 命令模式.cpp 命令模式.cpp #include <iostream> #include <list> #include <memory> using namespace std;// 5种角色 // Receiver&#xff08;接收者&#xff09;&#xff0c;Cook&#xff0c;提供请求业务的处理接口…

标书整体实施方案

一、标书整体实施方案概述 标书整体实施方案是一种全面、系统的方法&#xff0c;旨在确保标书的顺利完成和成功实施。它涵盖了从项目背景到招标文件分析&#xff0c;再到投标文件编制和投标的全过程。实施方案不仅提供了明确的工作流程和分工&#xff0c;还为整个标书制作团队…

针对网页html中插入动图gif不能循环播放只播放一次的解决方案

针对网页html中插入动图gif不能循环播放只播放一次的解决方案 原因分析解决方案 原因分析 使用图片编辑软件制作的过程中未启用“循环播放”功能&#xff0c;这里以Photoshop为例&#xff0c;演示设置GIF图片循环播放的操作流程&#xff1a;所需材料&#xff1a;PS。第一步&am…

MongoDB——模糊查询的两种方法

方法一&#xff1a;类似于结构性数据库的like db.users.find({fname: /zhangsan/}); 对应mysql的like用法&#xff1a;select * from users where fname like %zhangsan%; &#xff08;1&#xff09;如果要模糊查询以什么开头&#xff0c;方法如下&#xff1a; db.users.fi…

图灵日记之Leetcode删除有序数组中的重复项合并两个有序数组移除链表元素

题目 删除有序数组中的重复项题目入口题目内容思路代码c版本c嘎嘎版本 合并两个有序数组题目链接题目内容思路代码c版本(c嘎嘎版本与c版本内容一样) 移除链表元素题目链接题目内容思路1代码1思路2代码2思路3代码3 删除有序数组中的重复项 题目入口 题目内容 给你一个 非严格…

抖音ip地址切换什么原因

在如今的互联网世界中&#xff0c;抖音已经成为了一个非常受欢迎的应用程序&#xff0c;让人们可以通过短视频来分享生活点滴。然而&#xff0c;有时候我们可能会遇到一些问题&#xff0c;比如在使用抖音时需要进行IP地址切换。那么&#xff0c;为什么要切换IP地址呢&#xff1…

拼多多买家页面批量导出订单excel

拼多多买家页面批量导出订单excel 由于拼多多不支持订单导出excel清算起来很麻烦&#xff0c;就自己写了一个页面批量导出脚本代码。 首先打开拼多多手机端网站&#xff1a;https://mobile.pinduoduo.com/ 登录后点击我的订单打开f12审查元素 在控制台引入jquery&#xff0c;引…

IIC需要外部上拉电阻

由于I2C接口采用Open Drain机制&#xff0c;器件本身只能输出低电平&#xff0c;无法主动输出高电平&#xff0c;只能通过外部上拉电阻RP将信号线拉至高电平。 因此I2C总线上的上拉电阻是必须的&#xff01; 所以开发板上面的IIC如果没有外接上拉电阻&#xff0c;是无法使用该I…

【go语言开发】本地缓存的使用,从简单到复杂写一个本地缓存,并对比常用的开源库

本文主要介绍go语言中本地缓存的使用&#xff0c;首先由简单到复杂手写3个本地缓存示例&#xff0c;使用内置的sync&#xff0c;map等数据结构封装cache&#xff0c;然后介绍常见的一些开源库&#xff0c;以及对比常用的开源库 文章目录 前言手写本地缓存CacheNormalCacheExCac…