Spring 中的事务
本文代码已经同步到码云 ,欢迎大家 star https://gitee.com/njitzyd/JavaDemoCollection
事务简介
事务指逻辑上的一组操作,组成这组操作的各个单元,要不全部成功,要不全部不成功,事务有四个基本属性ACID.
-
原子性(Atomicity): 事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
-
一致性(Consistency): 事务开始前和结束后,数据库的完整性约束没有被破坏。比如A向B转账,不可能A扣了钱,B却没收到。
-
隔离性(Isolation): 同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
-
持久性(Durability): 事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
Spring事务属性
Spring事务属性对应TransactionDefinition类里面的各个方法。TransactionDefinition类方法如下所示:
public interface TransactionDefinition {
/**
* 返回事务传播行为
*/
int getPropagationBehavior();
/**
* 返回事务的隔离级别,事务管理器根据它来控制另外一个事务可以看到本事务内的哪些数据
*/
int getIsolationLevel();
/**
* 事务超时时间,事务必须在多少秒之内完成
*/
int getTimeout();
/**
* 事务是否只读,事务管理器能够根据这个返回值进行优化,确保事务是只读的
*/
boolean isReadOnly();
/**
* 事务名字
*/
@Nullable
String getName();
}
事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。事务属性包含了5个方面:传播行为、隔离规则、回滚规则、事务超时、是否只读。
传播类型
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。Spring定义了七种传播行为:
传播行为 | 含义 |
---|---|
TransactionDefinition.PROPAGATION_REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务,则加入到这个事务中。这是最常见的选择。 |
TransactionDefinition.PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
TransactionDefinition.PROPAGATION_MANDATORY | 表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常 |
TransactionDefinition.PROPAGATION_REQUIRED_NEW | 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。 |
TransactionDefinition.PROPAGATION_NOT_SUPPORTED | 表示该方法不应该运行在事务中。如果当前存在事务,就把当前事务挂起。 |
TransactionDefinition.PROPAGATION_NEVER | 表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常 |
TransactionDefinition.PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 |
隔离规则
隔离级别定义了一个事务可能受其他并发事务影响的程度。
在实际开发过程中,我们绝大部分的事务都是有并发情况。多个事务并发运行,经常会操作相同的数据来完成各自的任务。在这种情况下可能会导致以下的问题:
-
脏读(Dirty reads)—— 事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据。
-
不可重复读(Nonrepeatable read)—— 事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
-
幻读(Phantom read)—— 系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
咱们已经知道了在并发状态下可能产生: 脏读、不可重复读、幻读的情况。因此我们需要将事务与事务之间隔离。根据隔离的方式来避免事务并发状态下脏读、不可重复读、幻读的产生。Spring中定义了五种隔离规则:
隔离级别 | 含义 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
TransactionDefinition.ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别 | |||
TransactionDefinition.ISOLATION_READ_UNCOMMITTED | 允许读取尚未提交的数据变更(最低的隔离级别) | 是 | 是 | 是 |
TransactionDefinition.ISOLATION_READ_COMMITTED | 允许读取并发事务已经提交的数据 | 否 | 是 | 是 |
TransactionDefinition.ISOLATION_REPEATABLE_READ | 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改 | 否 | 否 | 是 |
TransactionDefinition.ISOLATION_SERIALIZABLE | 最高的隔离级别,完全服从ACID的隔离级别,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的 | 否 | 否 | 否 |
ISOLATION_SERIALIZABLE 隔离规则类型在开发中很少用到。举个很简单的例子。咱们使用了ISOLATION_SERIALIZABLE规则。A,B两个事务操作同一个数据表并发过来了。A先执行。A事务这个时候会把表给锁住,B事务执行的时候直接报错。
- 事务隔离级别为ISOLATION_READ_UNCOMMITTED时,写数据只会锁住相应的行。
- 事务隔离级别为可ISOLATION_REPEATABLE_READ时,如果检索条件有索引(包括主键索引)的时候,默认加锁方式是next-key锁;如果检索条件没有索引,更新数据时会锁住整张表。一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读。
- 事务隔离级别为ISOLATION_SERIALIZABLE时,读写数据都会锁住整张表。
- 隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也就越大。
回滚规则
事务回滚规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,只有未检查异常(RuntimeException和Error类型的异常)会导致事务回滚。而在遇到检查型异常时不会回滚。 但是你可以声明事务在遇到特定的检查型异常时像遇到运行期异常那样回滚。同样,你还可以声明事务遇到特定的异常不回滚,即使这些异常是运行期异常。
事务超时
为了使应用程序很好地运行,事务不能运行太长的时间。因为事务可能涉及对后端数据库的锁定,也会占用数据库资源。事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束。
是否只读
如果在一个事务中所有关于数据库的操作都是只读的,也就是说,这些操作只读取数据库中的数据,而并不更新数据, 这个时候我们应该给该事务设置只读属性,这样可以帮助数据库引擎优化事务。提升效率。
@ Transactional注解
简介
注解是不能继承的!!!
在Spring中, Java方法的事务传播类型通过 @Transactional 注解进行指明, 并通过该注解的 propagation 属性指明事务传播的具体类型.
@Transactional 注解的使用非常灵活, 可以注解在服务接口上, 也可以注解在服务类的方法上, 还可以注解在Spring Repository的接口方法上。在之前的基于xml配置文件还要写很多比如横切关注点等,但是现在在基于注解的方式就很容易,但是需要注意的是使用**@Transactional**注解的时候需要注意事务有没有起作用,有没有回滚机制,在事务中调用其他的事务的传播属性。
@Transactional 注解的属性信息
属性名 | 说明 |
---|---|
name | 当在配置文件中有多个 TransactionManager , 可以用该属性指定选择哪个事务管理器。 |
propagation | 事务的传播行为,默认值为 REQUIRED。 |
isolation | 事务的隔离度,默认值采用 DEFAULT。 |
timeout | 事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。 |
read-only | 指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。 |
rollback-for | 用于指定能够触发事务回滚的异常类型,如果有多个异常类型需要指定,各类型之间可以通过逗号分隔。 |
no-rollback- for | 抛出 no-rollback-for 指定的异常类型,不回滚事务。 |
平时使用最多的就是rollback-for属性,下面介绍下这个配置。
rollbackFor
在使用阿里巴巴代码检查规范进行扫描的时候,在**@Transactional注解的地方都会提示使用@Transactional注解指定rollbackFor属性或者在方法中显示的rollback**。也就是说在使用这个注解的时候需要去指定
首先分析一下异常的分类
- Error是一定会回滚的
- 这里Exception是异常,他又分为运行时异常RuntimeException和非运行时异常
可查的异常(checked exceptions):Exception下除了RuntimeException外的异常
不可查的异常(unchecked exceptions):RuntimeException及其子类和错误(Error)
如果不对运行时异常进行处理,那么出现运行时异常之后,要么是线程中止,要么是主程序终止。
如果不想终止,则必须捕获所有的运行时异常,决不让这个处理线程退出。队列里面出现异常数据了,正常的处理应该是把异常数据舍弃,然后记录日志。不应该由于异常数据而影响下面对正常数据的处理。
非运行时异常是RuntimeException以外的异常,类型上都属于Exception类及其子类。如IOException、SQLException等以及用户自定义的Exception异常。对于这种异常,JAVA编译器强制要求我们必需对出现的这些异常进行catch并处理,否则程序就不能编译通过。所以,面对这种异常不管我们是否愿意,只能自己去写一大堆catch块去处理可能的异常。
Spring默认的机制
注意: 如果异常被try{}catch{}了,事务就不回滚了,如果想让事务回滚必须再往外抛异常!!!try{}catch{throw Exception}。
默认情况下,如果只是在方法上使用注解@Transactional,如果在事务中抛出了未检查异常(继承自 RuntimeException 的异常)或者 Error,则 Spring 将回滚事务;除此之外,Spring 不会回滚事务。
如果在事务中抛出其他类型的异常,并期望 Spring 能够回滚事务,可以指定 rollbackFor。比如:
@Transactional(rollbackFor= Exception.class)
这样在抛出Exception以及他的子类的异常的时候,事务会回滚。
Spring事务自调用的问题
在 Spring 的 AOP 代理下,只有目标方法由外部调用,目标方法才由 Spring 生成的代理对象来管理,这会造成自调用问题。若同一类中的其他没有@Transactional 注解的方法内部调用有@Transactional 注解的方法,有@Transactional 注解的方法的事务被忽略,不会发生回滚。
使用测试
测试下配置是否起作用,两个测试问题
首先搭建基本的测试环境,依赖,数据库,jpa,然后自定义一个异常,这些都不再多说,比较简单,源代码都放在码云上的spring-transaction
模块中,大家可以下载下来看,这里主要放出关键的测试逻辑。
测试一:是出现异常就会回滚还是说必须我们手动的去抛出异常才回滚?
/**
* 测试回滚是异常自己出现就会回滚还是说需要自己手动才会回滚
* @param id
* @return
*/
@Transactional(rollbackFor = Exception.class)
public String testWhenRollback(Long id){
User user = new User();
user.setUsername("hello");
user.setPassword("hello");
User save = userRepository.save(user);
System.out.println("自己出/现异常"+(1/0));
return "sucess";
}
结论:只要出现符合的异常就会回滚,不是说必须我们手动去抛出。
测试二:rollbackFor 如果指定了自己的异常,那么原来默认的那些还会回滚吗?
/**
* 测试,如果指定自己定义的异常后,那么其他的异常还能正常的回滚吗
* @param id
* @return
*/
@Transactional(rollbackFor = DemoException.class)
public String testRollbackType(Long id){
User user = new User();
user.setUsername("hello");
user.setPassword("hello");
User save = userRepository.save(user);
System.out.println("自己出现异常"+(1/0));
return "sucess";
}
结论:指定自己的异常后,那些RunTimeException以及Error还是会回滚的。
总结
在平时的使用过程中,我们需要注意一下几点。
- rollback-for 属性的配置,一般在项目中都是自己自定义异常继承RunTimeException,然后这个属性都配置的Exception.class.
- 注解的位置,建议放在类的方法上而不是接口或者类上,而且一定注意放在方法时方法必须是public
- 如果异常被try catch了,一定记得要手动回滚或者再抛出异常,否则不会回滚!!!