前言

在之前的文章中已经对Spring中的事务做了详细的分析了,这篇文章我们来聊一聊平常工作时使用事务可能出现的一些问题(本文主要针对使用@Transactional进行事务管理的方式进行讨论)以及对应的解决方案

  1. 事务失效

  2. 事务回滚相关问题

  3. 读写分离跟事务结合使用时的问题

事务失效

事务失效我们一般要从两个方面排查问题

数据库层面

数据库层面,数据库使用的存储引擎是否支持事务?默认情况下MySQL数据库使用的是Innodb存储引擎(5.5版本之后),它是支持事务的,但是如果你的表特地修改了存储引擎,例如,你通过下面的语句修改了表使用的存储引擎为MyISAM,而MyISAM又是不支持事务的

alter table table_name engine=myisam;

这样就会出现“事务失效”的问题了

「解决方案」:修改存储引擎为Innodb

业务代码层面

业务层面的代码是否有问题,这就有很多种可能了

  1. 我们要使用Spring的声明式事务,那么需要执行事务的Bean是否已经交由了Spring管理?在代码中的体现就是类上是否有@ServiceComponent等一系列注解

「解决方案」:将Bean交由Spring进行管理(添加@Service注解)

  1. @Transactional注解是否被放在了合适的位置。在上篇文章中我们对Spring中事务失效的原理做了详细的分析,其中也分析了Spring内部是如何解析@Transactional注解的,我们稍微回顾下代码:

注解解析

❝代码位于:AbstractFallbackTransactionAttributeSource#computeTransactionAttribute中❞

也就是说,默认情况下你无法使用@Transactional对一个非public的方法进行事务管理

「解决方案」:修改需要事务管理的方法为public

  1. 出现了自调用。什么是自调用呢?我们看个例子

@Service
public class DmzService {public void saveAB(A a, B b) {saveA(a);saveB(b);}@Transactionalpublic void saveA(A a) {dao.saveA(a);}@Transactionalpublic void saveB(B b){dao.saveB(a);}
}

上面三个方法都在同一个类DmzService中,其中saveAB方法中调用了本类中的saveAsaveB方法,这就是自调用。在上面的例子中saveAsaveB上的事务会失效

那么自调用为什么会导致事务失效呢?我们知道Spring中事务的实现是依赖于AOP的,当容器在创建dmzService这个Bean时,发现这个类中存在了被@Transactional标注的方法(修饰符为public)那么就需要为这个类创建一个代理对象并放入到容器中,创建的代理对象等价于下面这个类

public class DmzServiceProxy {private DmzService dmzService;public DmzServiceProxy(DmzService dmzService) {this.dmzService = dmzService;}public void saveAB(A a, B b) {dmzService.saveAB(a, b);}public void saveA(A a) {try {// 开启事务startTransaction();dmzService.saveA(a);} catch (Exception e) {// 出现异常回滚事务rollbackTransaction();}// 提交事务commitTransaction();}public void saveB(B b) {try {// 开启事务startTransaction();dmzService.saveB(b);} catch (Exception e) {// 出现异常回滚事务rollbackTransaction();}// 提交事务commitTransaction();}
}

上面是一段伪代码,通过startTransactionrollbackTransactioncommitTransaction这三个方法模拟代理类实现的逻辑。因为目标类DmzService中的saveAsaveB方法上存在@Transactional注解,所以会对这两个方法进行拦截并嵌入事务管理的逻辑,同时saveAB方法上没有@Transactional,相当于代理类直接调用了目标类中的方法。

我们会发现当通过代理类调用saveAB时整个方法的调用链如下:

实际上我们在调用saveAsaveB时调用的是目标类中的方法,这种清空下,事务当然会失效。

常见的自调用导致的事务失效还有一个例子,如下:

@Service
public class DmzService {@Transactionalpublic void save(A a, B b) {saveB(b);}@Transactional(propagation = Propagation.REQUIRES_NEW)public void saveB(B b){dao.saveB(a);}
}

当我们调用save方法时,我们预期的执行流程是这样的

也就是说两个事务之间互不干扰,每个事务都有自己的开启、回滚、提交操作。

但根据之前的分析我们知道,实际上在调用saveB方法时,是直接调用的目标类中的saveB方法,在saveB方法前后并不会有事务的开启或者提交、回滚等操作,实际的流程是下面这样的

由于saveB方法实际上是由dmzService也就是目标类自己调用的,所以在saveB方法的前后并不会执行事务的相关操作。这也是自调用带来问题的根本原因:「自调用时,调用的是目标类中的方法而不是代理类中的方法」

「解决方案」

  1. 自己注入自己,然后显示的调用,例如:

    @Service
    public class DmzService {// 自己注入自己@AutowiredDmzService dmzService;@Transactionalpublic void save(A a, B b) {dmzService.saveB(b);}@Transactional(propagation = Propagation.REQUIRES_NEW)public void saveB(B b){dao.saveB(a);}
    }
    

    这种方案看起来不是很优雅

    利用AopContext,如下:

    @Service
    public class DmzService {@Transactionalpublic void save(A a, B b) {((DmzService) AopContext.currentProxy()).saveB(b);}@Transactional(propagation = Propagation.REQUIRES_NEW)public void saveB(B b){dao.saveB(a);}
    }
    

    ❝使用上面这种解决方案需要注意的是,需要在配置类上新增一个配置

    // exposeProxy=true代表将代理类放入到线程上下文中,默认是false
    @EnableAspectJAutoProxy(exposeProxy = true)

    个人比较喜欢的是第二种方式

这里我们做个来做个小总结

总结

一图胜千言

事务失效的原因

事务回滚相关问题

回滚相关的问题可以被总结为两句话

  1. 想回滚的时候事务却提交了

  2. 想提交的时候被标记成只能回滚了(rollback only)

先看第一种情况:「想回滚的时候事务却提交了」。这种情况往往是程序员对Spring中事务的rollbackFor属性不够了解导致的。

❝Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务,已经执行的SQL会提交掉。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定rollbackFor属性。❞

对应代码其实我们上篇文章也分析过了,如下:

回滚代码

❝以上代码位于:TransactionAspectSupport#completeTransactionAfterThrowing方法中❞

默认情况下,只有出现RuntimeException或者Error才会回滚

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

所以,如果你想在出现了非RuntimeException或者Error时也回滚,请指定回滚时的异常,例如:

@Transactional(rollbackFor = Exception.class)

第二种情况:「想提交的时候被标记成只能回滚了(rollback only)」

对应的异常信息如下:

Transaction rolled back because it has been marked as rollback-only

我们先来看个例子吧

@Service
public class DmzService {@AutowiredIndexService indexService;@Transactionalpublic void testRollbackOnly() {try {indexService.a();} catch (ClassNotFoundException e) {System.out.println("catch");}}
}@Service
public class IndexService {@Transactional(rollbackFor = Exception.class)public void a() throws ClassNotFoundException{// ......throw new ClassNotFoundException();}
}

在上面这个例子中,DmzServicetestRollbackOnly方法跟IndexServicea方法都开启了事务,并且事务的传播级别为required,所以当我们在testRollbackOnly中调用IndexServicea方法时这两个方法应当是共用的一个事务。按照这种思路,虽然IndexServicea方法抛出了异常,但是我们在testRollbackOnly将异常捕获了,那么这个事务应该是可以正常提交的,为什么会抛出异常呢?

如果你看过我之前的源码分析的文章应该知道,在处理回滚时有这么一段代码

rollBackOnly设置

在提交时又做了下面这个判断(这个方法我删掉了一些不重要的代码)

commit_rollbackOnly

可以看到当提交时发现事务已经被标记为rollbackOnly后会进入回滚处理中,并且unexpected传入的为true。在处理回滚时又有下面这段代码

抛出异常

最后在这里抛出了这个异常。

❝以上代码均位于AbstractPlatformTransactionManager中❞

总结起来,「主要的原因就是因为内部事务回滚时将整个大事务做了一个rollbackOnly的标记」,所以即使我们在外部事务中catch了抛出的异常,整个事务仍然无法正常提交,并且如果你希望正常提交,Spring还会抛出一个异常。

「解决方案」:

这个解决方案要依赖业务而定,你要明确你想要的结果是什么

  1. 内部事务发生异常,外部事务catch异常后,内部事务自行回滚,不影响外部事务

❝将内部事务的传播级别设置为nested/requires_new均可。在我们的例子中就是做如下修改:

// @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
@Transactional(rollbackFor = Exception.class,propagation = Propagation.NESTED)
public void a() throws ClassNotFoundException{// ......throw new ClassNotFoundException();
}

虽然这两者都能得到上面的结果,但是它们之间还是有不同的。当传播级别为requires_new时,两个事务完全没有联系,各自都有自己的事务管理机制(开启事务、关闭事务、回滚事务)。但是传播级别为nested时,实际上只存在一个事务,只是在调用a方法时设置了一个保存点,当a方法回滚时,实际上是回滚到保存点上,并且当外部事务提交时,内部事务才会提交,外部事务如果回滚,内部事务会跟着回滚。

  1. 内部事务发生异常时,外部事务catch异常后,内外两个事务都回滚,但是方法不抛出异常

@Transactional
public void testRollbackOnly() {try {indexService.a();} catch (ClassNotFoundException e) {// 加上这句代码TransactionInterceptor.currentTransactionStatus().setRollbackOnly();}
}

通过显示的设置事务的状态为RollbackOnly。这样当提交事务时会进入下面这段代码

显示回滚

最大的区别在于处理回滚时第二个参数传入的是false,这意味着回滚是回滚是预期之中的,所以在处理完回滚后并不会抛出异常。

 

读写分离跟事务结合使用时的问题

读写分离一般有两种实现方式

  1. 配置多数据源

  2. 依赖中间件,如MyCat

如果是配置了多数据源的方式实现了读写分离,那么需要注意的是:「如果开启了一个读写事务,那么必须使用写节点」「如果是一个只读事务,那么可以使用读节点」

如果是依赖于MyCat等中间件那么需要注意:「只要开启了事务,事务内的SQL都会使用写节点(依赖于具体中间件的实现,也有可能会允许使用读节点,具体策略需要自行跟DB团队确认)」

基于上面的结论,我们在使用事务时应该更加谨慎,在没有必要开启事务时尽量不要开启。

❝一般我们会在配置文件配置某些约定的方法名字前缀开启不同的事务(或者不开启),但现在随着注解事务的流行,好多开发人员(或者架构师)搭建框架的时候在service类上加上了@Transactional注解,导致整个类都是开启事务的,这样严重影响数据库执行的效率,更重要的是开发人员不重视、或者不知道在查询类的方法上面自己加上@Transactional(propagation=Propagation.NOT_SUPPORTED)就会导致,所有的查询方法实际并没有走从库,导致主库压力过大。❞

其次,关于如果没有对只读事务做优化的话(优化意味着将只读事务路由到读节点),那么@Transactional注解中的readOnly属性就应该要慎用。我们使用readOnly的原本目的是为了将事务标记为只读,这样当MySQL服务端检测到是一个只读事务后就可以做优化,少分配一些资源(例如:只读事务不需要回滚,所以不需要分配undo log段)。但是当配置了读写分离后,可能会可能会导致只读事务内所有的SQL都被路由到了主库,读写分离也就失去了意义。

总结

本文为事务专栏最后一篇啦!这篇文章主要是总结了工作中事务相关的常见问题,想让大家少走点弯路!希望大家可以认真读完哦,有什么问题可以直接在后台私信我或者加我微信!

这篇文章也是整个Spring系列的最后一篇文章,之后可能会出一篇源码阅读心得,跟大家聊聊如何学习源码。

另外今年也给自己定了个小目标,就是完成SSM框架源码的阅读。目前来说Spring是完成,接下来就是SpringMVC跟MyBatis。

在分析MyBatis前,会从JDBC源码出发,然后就是MyBatis对配置的解析、MyBatis执行流程、MyBatis的缓存、MyBatis的事务管理以及MyBatis的插件机制。

在学习SpringMVC前,会从TomCat出发,先讲清楚TomCat的原理,我们再来看SpringMVC。整个来说相比于Spring源码,我觉得应该不算特别难。

有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号

好文章,我在看❤️

一个@Transaction哪里来这么多坑?相关推荐

  1. 多方法调用 一个出错 集体回滚_一个@Transaction哪里来这么多坑?

    前言 在之前的文章中已经对Spring中的事务做了详细的分析了,这篇文章我们来聊一聊平常工作时使用事务可能出现的一些问题(本文主要针对使用@Transactional进行事务管理的方式进行讨论)以及对 ...

  2. 求助:现在有一个可以进体制“养老”的坑,我该不该跳?

    对不起,在当下互联网人生活的环境中,并没有可以"养老"的坑. 对不起,在当下互联网人生活的环境中,也没有绝对"稳定"的企业. 技术人的职业发展,以"适 ...

  3. 一个单片机ADC的挖坑填坑之旅

    [导读] 本文来解析一个盆友在使用STM32采集电池电压踩过的坑.以STM32F4 的ADC属于逐次逼近SAR 型ADC为例进行分析,参考STM32F405xx  Datasheet,对于如何编写AD ...

  4. 一个memset函数使用时的坑

    平时使用memset函数都是用来初始化字符串,从来没有注意过它具体是怎么实现的,最近无意中发现原来这个函数里面还是有坑的. 我尝试用它来初始化整型数组 int array[10]; memset(ar ...

  5. mysql left join第一个_MySQL 之 LEFT JOIN 避坑指南

    现象 left join在我们使用mysql查询的过程中可谓非常常见,比如博客里一篇文章有多少条评论.商城里一个货物有多少评论.一条评论有多少个赞等等.但是由于对join.on.where等关键字的不 ...

  6. 32f4怎样同时采集两个adc_一个单片机ADC的挖坑填坑之旅

    欢迎FPGA工程师加入官方微信技术群 点击蓝字关注我们FPGA之家-中国最好最大的FPGA纯工程师社群 [导读] 本文来解析一个盆友在使用STM32采集电池电压踩过的坑.以STM32F4 的ADC属于 ...

  7. 这是一个沙雕题III(坑题)

    链接:https://ac.nowcoder.com/acm/contest/289/K 来源:牛客网 题目描述 因为现在的新生太强了,都学会了"dp",所以就有了这样一个&quo ...

  8. 记录下使用阿里云的ces centos的linux系统搭建一个socks5代理步骤和踩坑过程

    文章目录 一.配置: 二.搭建步骤: 1.通过yum安装ss5 依赖包 2.安装ss5 ①.首先下载ss5 ②.解压安装: 3.启动: 4.修改配置: 5.修改端口号(默认1080,但是容易被人家扫描 ...

  9. 一个音乐播放器的踩坑实践

    前言 这是这个系列的第二篇文章,和第一篇文章相同的是Demo中的资源文件和一些关键代码是搜索和学习得来的.一是因为没有相关的资源文件,譬如音乐文件.歌词文件.歌曲封面等:二是着实有点力有未逮的感觉(p ...

最新文章

  1. 『追捕盗贼 Tarjan算法』
  2. Android 使用Jsoup解析Html
  3. 【Python】五子棋项目记录
  4. ip和端口的本质与作用,网络协议栈
  5. php中奖概率算法,可用于刮刮卡,大转盘等抽奖算法
  6. 【FLink】FLink学习遇到的好文章
  7. javascript实现页面自动刷新和自动跳转代码
  8. avr单片机教程 csdn_从古老的attiny85升级到新的AVR 1系列attiny412教程
  9. 怎样用计算机进入手机驱动,电脑没有手机驱动_怎么安装手机驱动_好特教程
  10. html圆角输入框内放大镜,如何用CSS制作一个圆形放大镜
  11. android shell卸载应用程序,adb shell删除系统apk
  12. 计算机录制视频的方法,怎么录制电脑屏幕视频步骤(电脑录屏的方法有4种)...
  13. css3实现旋转的立方体
  14. 基于thinkphp开源cms 对比
  15. hdu2201 熊猫阿波的故事
  16. Android虚拟化
  17. 【C语言典例】——day6:猴子吃桃
  18. FME在测量平差上面的运用
  19. 你上面写的代码用什么编程软件?
  20. 红米K50电竞版上手体验

热门文章

  1. pandas 替换 某列大于_Pandas简单入门 1
  2. 九章云极DataCanvas完成C轮融资:定义标准化AI基础架构未来
  3. abaqus分析用户手册单元卷_ABAQUS与你我的约定
  4. Spring boot配置文件两种方式
  5. (软件工程复习核心重点)第四章总体设计习题
  6. caffe使用训练好的模型对自己的一张图片进行测试
  7. 【glibc源码分析】--strcpy.c 字符串复制
  8. UWP Acrylic Material
  9. IoC反转控制初步认识
  10. [BZOJ3781]小B的询问