前言
什么是事务?根据 维基百科事务 介绍,数据库事务(简称:事务)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。简单来说,事务就是将一系列操作当成一个不可拆分的执行逻辑单元,这些要么都成功,要么都失败。事务具有4个属性:原子性、一致性、隔离性、持久性。称为ACID特性。
Spring 事务
在使用 进行开发过程中,一般都会使用 来进行事务的控制,接下来就来看下 使用事务的详细过程,包括事务的传播方式等。本文根据官方文档的介绍,结合例子来进行说明。
事务支持两种方式,编程式事务和声明式事务,下面的栗子会使用声明式事务来举例,即使用注解的方式.
栗子
首先来看个简单栗子,后面再来对事务的一些属性进行详细的分析和介绍。
1. 首先来看下两个数据库表结构 表和 表:
user 表:
address表:
2.Spring 配置文件启动事务:
3.定义需要事务执行的方法:
这里使用 来操作数据库
4.在上述的配置文件中配置该 bean:
5.单元测试:
6.运行结果会抛出异常:
7.此时数据库的数据还是原来的。
上述的栗子中,在 方法加上了事务注解 ,当该方法抛出异常的时候,数据库会进行回滚,数据插入失败。
事务的原理
事务是使用 来实现的,在Spring AOP 注解方式源码解析和Spring AOP 创建代理的源码解析文章中,了解到,在执行目标方法之前和之后,我们可以进行一些增强操作,而这恰恰可以符合事务的使用情况,在目标方法执行成功后,提交事务,失败的时候,回滚事务。当然,我们还可以通过来自定义事务的行为。从概念上讲,在事务代理上调用方法看起来如下所示:
当客户端调用的时候,调用的是代理对象,在执行目标方法之前,会创建事务,即事务增强,之后会执行我们自定义的增强行为(如果有的话,可以在事务增强之前或之后执行),之后执行目标方法,执行目标方法之后,又会执行自定义增强和事务增强,事务要么提交要么回滚,之后再返回给客户端;这就是它的一个流程,和 分析 的流程是一致的。
Spring 事务详解
事务只会对 方法有效,对 , 和 的方法,事务不会有效,在 模式下,如果是对象内部的方法自我调用,则调用的内部方法事务也不会生效.
下面通过栗子来验证下方法的自我调用看下事务是否生效
现在 方法调用 方法, 方法不加事务,而 方法添加了事务,此时客户端调用 方法:
虽然 方法抛出了异常,但是不会回滚,数据还是成功的插入:
这是为什么呢?因为 方法通过 来调用 方法,而 代表的是目标对象而不是代理对象,所以 不会被代理,也就不会被事务控制,即事务不生效;这种情况下,怎么使 也被事务进行控制呢?当然不能使用 来调用了,而是使用 代理对象 来调用: ,但是,使用这种方式有个前提,需要把我们的代理对象暴露出来,放到 中,即在 配置文件 配置 属性,即 ,在Spring AOP 注解方式源码解析中已经分析过该属性。如下所示:
再运行上面的测试代码,会发现,事务回滚了,数据插入失败。
如果我们在 方法和 方法都加上注解,且通过 来调用 方法,事务会不会生效呢?
运行上面的测试代码,事务是生效的,这是因为它们属于同一个事务,且 方法被事务进行管理。
注解可以放在接口上,接口方法上,类上,类的 public 方法上,但是 建议的是 尽量放在类或类方法上,而不建议放在接口或接口方法;这是为什么呢?我们知道,事务是通过 来实现的,而 是通过动态代理来实现的,而 使用的动态代理主要有 和 ,主要代理接口,这时 把 放在接口或接口方法上,事务是有效的;而 是代理类,通过继承的方式来实现,这时把 放在接口或接口方法上,则事务就不会生效:
针对该配置方式,把事务注解放在接口上,则只会对 JDK 代理有效
把事务注解放在实现类上,则对 CGLIB 和 JDK 代理都有效
使用 代理,需要配置 属性为:
的属性
标签用来表示开启事务功能,它有 4 个属性:
transaction-manager :事务管理器的名称,默认为 transactionManager,因为可以不写,如果管理器的名称不是这个才需要写。
mode : 模式,两种,proxy 模式和 aspectj 模式,proxy 仅适用于通过代理进入的方法调用,aspectj 适用于任何类型的方法调用
proxy-target-class : 使用 CGLIB 进行代理,代理类而不是代理接口
order:代理顺序
@Transactional 属性
有很多属性来控制事务的行为,共 9 个属性
事务的名称就是方法的全限定名,无法设置
事务的传播方式
接下来看下事务的传播方式,事务的传播方式在 事务中非常重要,需要理解清楚,否则有时候事务不回滚不知道问题出在哪里。
事务的传播方式使用 属性来表示,它是一个枚举类型,共有 7 个,即事务的传播方式有 7 种:
下面以栗子的方式来验证这几种传播方式,数据库的相关表结构还是文章开头的 表和 表,代码如下:
即就是在 的 方法里面调用 的 方法
REQUIRED
这种传播方式,它是需要在事务中运行的,如果事务不存在,则创建一个新事务
在 方法加上注解 ,如下所示:
因为 没要添加事务,所以 会创建一个新事务,运行上面的测试代码,日志如下(IDEA 把 log4j 日志输出到文件查看):
可以看到,第一步,执行插入操作,插入成功,并没有创建事务,第二步,创建事务,事务名称为 ,执行 插入,插入成功,又因为 抛出异常,所以 插入进行回滚,回滚的数据库连接是 ,即执行 插入的连接,并没有回滚 插入的连接,所以结果是 正常插入,而 插入失败。为什么 没有进行回滚呢,因为 它又没有在事务中运行,自然就不会回滚了。
在 和 方法都加上注解 ,如下所示:
运行测试代码,日志如下:
可以看到,首先会创建事务,名称为 的全限定名,获取数据库连接 ,之后会在该连接中执行 和 的插入操作,即在同一个事务中, 插入抛出异常,进行回滚,回滚的是连接 ,所以 和 的操作都会被回滚,都插入失败。
如果在 调用 的时候,进行异常的捕获, 会进行回滚嘛?如下:
运行日志如下:
可以看到,它们还是在同一个事务中运行,同一个连接中进行插入,回滚的是同一个连接,所以都会插入失败,即使进行了异常捕获。
总结:所以 这种传播方式,必须要在事务中运行,不存在事务在,则创建一个,即使进行的异常的捕获,外部还是会进行回滚,这是因为虽然在每个方法都加上了事务注解,看起来是独立的事务,可是都会映射到底层数据库中的同一个物理事务中,所以只要有一个进行了回滚,则都会进行回滚。
REQUIRES_NEW
·required_new·,需要创建一个新事务,如果已存在事务,则把当前事务挂起
在 方法加上注解
在 方法加上注解
运行日志如下:
可以看到,创建了两个事务,获取了两个数据库连接 和 ,当执行 插入的时候,会把已经存在的事务挂起,新创建一个事务进行运行,当 插入抛出异常的时候,这两个事务都进行了回滚,所以都会插入失败。
在 调用 的时候,进行异常捕获,则会怎么样呢?
运行日志如下:
可以看到,还是创建两个事务,获取了两个连接,进行异常捕获了之后,只会回滚一个事务,
总结: 它是创建了一个新的事务进行运行,它们是完全独立的事务范围,对应到底层数据库的物理事务也是不同的,所以它们可以独立提交或回滚,外部事务不受内部事务的回滚状态的影响;对于上述栗子来说,如果 抛异常且 不进行异常捕获,则两个事务都会进行回滚,如果 进行了异常捕获,则 可以进行提交的,它们是两个独立的事务;如果 执行成功,外层的 执行失败,则 会回滚,则内部执行成功的 不会回滚。
NESTED
,嵌套事务,它是外部事务的一个子事务,新建一个子事务进行运行;它们并不是独立,如果外部事务提交,则嵌套事务也会提交,外部事务回滚,则嵌套事务也会回滚。嵌套事务主要支持 保存点。
在方法加上注解
在方法加上注解
运行日志如下:
可以看到,新建了两个事务,一个是 嵌套事务,而且只是获取了一个数据库连接 ,在同一个连接中执行两条,当 出现异常进行回滚的时候,只是回滚到 保存到,由于外层的 没有进行异常捕获,所以外部事务回滚,即回滚连接 。
如果在外层进行异常捕获,则外层的 会插入成功嘛?
运行日志如下:
可以看到,内层的事务会回滚到 保存点,而外层的事务则可以正常提交,结果就是 插入失败, 插入成功。
现在如果内层的 执行成功,不抛异常,外层的 抛异常,则 内层的 会回滚嘛?
运行日志如下:
如果外部事务回滚了,内部事务也会回滚,因为它们属于同一个底层数据库的物理事务。
总结:嵌套事务, 它是已经存在事务的子事务. 嵌套事务开始执行时, 它将取得一个. 如果这个嵌套事务失败, 将回滚到此. 嵌套事务是外部事务的一部分, 只有外部事务结束后它才会被提交. 使用 有限制,它只支持 ,且数据库要支持 保存点,还要 的驱动在以上
SUPPORTS
supports,支持事务,如果没有事务,则以非事务的方式运行
NOT_SUPPORTED
not_supported,不支持事务,以非事务的方式运行,如果存在事务,则挂起
NEVER
never,不支持事务,如果存在事务,则抛出异常
MANDATORY
mandatory,支持事务,如果没有事务,则抛出异常
这几种传播方式比较好理解,就不把栗子贴出来了。
事务的隔离级别
事务的隔离级别使用 表示,它是一个枚举
总结
1. Spring 事务原理:
Spring 事务是通过 Spring AOP 来实现的,还可以通过 AOP 来自定义事务的行为。
2. 事务注解 @Transactional
事务注解 可以放在接口上,接口方法上,类上,类的方法上,如果放在接口或接口方法上,则只会对 有效,对 无效,我们知道,事务是通过 来实现的,而 是通过动态代理来实现的,而 使用的动态代理主要有 和 ,主要代理接口,这时 把 放在接口或接口方法上,事务是有效的;而 是代理类,通过继承的方式来实现,这时把 放在接口或接口方法上,则事务就不会生效的。
3. 内部调用事务不生效的解决方法
一是把该方法放到其他的对象中,不过不太实用,二是不通过 来调用方法,而是通过代理来调用,如 ,但是,使用这种方式有个前提,需要把我们的代理对象暴露出来,放到 中,即在 配置文件配置 属性,即
4. 事务的传播方式,7 种
REQUIRED : 必须要在事务中运行,不存在事务在,则创建一个,即使进行的异常的捕获,外部还是会进行回滚,这是因为虽然在每个方法都加上了事务注解,看起来是独立的事务,可是都会映射到底层数据库中的同一个物理事务中,所以只要有一个进行了回滚,则都会进行回滚。
REQUIRES_NEW 它是创建了一个新的事务进行运行,它们是完全独立的事务范围,对应到底层数据库的物理事务也是不同的,所以它们可以独立提交或回滚,外部事务不受内部事务的回滚状态的影响.
NESTED 嵌套事务, 它是已经存在事务的子事务. 嵌套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 将回滚到此 savepoint. 嵌套事务是外部事务的一部分, 只有外部事务结束后它才会被提交. 使用 NESTED 有限制,它只支持 JDBC,且数据库要支持 savepoint 保存点,还要 JDBC 的驱动在3.0以上。
5. 事务的隔离级别
领取专属 10元无门槛券
私享最新 技术干货