在 Spring boot 中使用事务
背景
当我们的项目中,对一系列事件想要它要么全成功,要么全失败,这个时候,就可以使用事务了;
简单测试
现在,先来个简单测试,测试源码在最后会进行分享;
这个一个简单的测试类,当执行到第六句语句时,这个时候发生了异常,这个时候,前5条语句已经插入到数据库中了,但是,后面的语句还没有执行,违反了事务的性质;
数据库表数据如下:
那么,想要满足事务,其实很简单,在该方法上添加一个@Transactional注解即可
此时,再把表中数据删除,重新测试;这时候,数据会进行回滚;
数据表中也没有相应的数据了; 但是,这里发现一个问题,在测试类中,好像加入了@Transactional以后,不管有没有报错,都会回滚(卡在这里一个多小时),如果想在测试类中,关闭回滚功能,使用@Rollback(false)即可 这样的话,就所有的数据都不会进行回滚,会产生干扰;
结论:在测试事务相关的功能时,不能放在测试类中进行测试;
而真正开发业务逻辑时,我们通常在service接口中,使用@Transactional来对各个业务逻辑进行事务管理的配置;例如:
注意:根据@Transactional 的说明 只会对RuntimeException() 和 Error()异常进行回滚;
这里解释一下什么是RuntimeException()异常,像ArrayIndexOutOfBoundsException(数组越界)异常的父类就是RuntimeException()异常,而他的主要体现是编译器检查不出来,不会爆红让你去处理它;
测试成功:发现数据库中没有数据被插入;
测试二:抛出一个Exception 异常
测试结果如下: 发现事务回滚失败;
那么,如何解决这个问题呢? 指定回滚时的异常@Transactional(rollbackFor = {Exception.class});
这个时候,就可以发现事务回滚成功;
但是,这需要抛出一个异常,不能return 了,为了解决这个问题,可以设置手动回滚;
测试结果:
数据库中没有数据,测试类结果如下:符合预期;
事务的隔离级别
我们知道事务之间是有隔离级别的,在Isolation枚举类中定义了五个表示隔离级别的值:
public enum Isolation {
DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),
READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),
READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),
REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),
SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);
}
DEFAULT:这是个默认值,表示使用底层数据库的默认隔离级别,一般来说都是READ_COMMITTED。
READ_UNCOMMITTED: 该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。
READ_COMMITTED:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。
REPEATABLE_READ:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。
SERIALIZABLE:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
通过,注解可以配置事务隔离级别
事务超时时间
由于,长事务可能会占用很多行锁,占用太多系统资源,所以,我们可以给他设置一个超时时间(单位为秒);注解配置如下:
经过测试,当超时时间到了以后,事务重新回滚;
事务传播行为
什么叫事务传播行为? 既然是传播,那么至少需要两个事务才可以发生传播,单个事务不存在传播这个行为。
事务传播行为,指的是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。
例如: 事务methodA()调用了事务methodB()方法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己再开启一个新事务运行,这就是由methodB的事务传播行为决定的。
由于,Spring中AOP的特性,所以,在使用事务的时候,假设,methodA()和methodB()不在一个类上;
1.PEQUIRED : 如果,当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
代码演示:
这是,methodA()的代码
这是 methodB()的代码,在methodB()中,调用了methodA()
结果是:超时报错 事务回滚;很显然,这里使用了事务默认的传播行为Propagation.REQUIRED 如果,当前存在事务,则加入该事务,如果没有当前没有事务,则创建一个新的事务;
就是,先在MethodB()中创建事务,然后,MethodA()加入到MethodB()的事务中;所以,导致了超时报错;
2.SUPPORTS
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB();
// do something
}
// 注意:两个事务不是一个类
// 事务属性为SUPPORTS
@Transactional(propagation = Propagtiojn.SUPPORTS)
public void methodB() {
// do something
}
如果,单纯的调用methodB时,methodB方法是非事务的执行。当调用methodA时,methodB则会加入methodA的事务中,事务地执行;
3.MANDATORY
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB();
// do something
}
// 注意:两个事务不是一个类
// 事务属性为SUPPORTS
@Transactional(propagation = Propagtiojn.MANDATORY)
public void methodB() {
// do something
}
如果,单纯的调用methodB时,因为,当前上下文没有活动的事务,所以,会抛出一个异常 new IllegalTransactionStateException()。当调用methodA时,methodB则会加入methodA的事务中,事务地执行;
4.REQUIRES_NEW
创建一个新的事务,如果当前存在事务,则把当前事务挂起;
@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
doSomeThingA();
methodA();
doSomeThingB();
// do something else
}
// 事务属性为REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodA() {
// do something
}
当调用methodB()时,相当于调用:
{
TransactionManager tm = null;
try{
//获得一个JTA事务管理器
tm = getTransactionManager();
tm.begin();//开启一个新的事务
Transaction ts1 = tm.getTransaction();
doSomeThingA();
tm.suspend();//挂起当前事务
try{
tm.begin();//重新开启第二个事务
Transaction ts2 = tm.getTransaction();
methodA();
ts2.commit();//提交第二个事务
} Catch(RunTimeException ex) {
ts2.rollback();//回滚第二个事务
} finally {
//释放资源
}
//methodA执行完后,恢复第一个事务
tm.resume(ts1);
doSomeThingB();
ts1.commit();//提交第一个事务
} catch(RunTimeException ex) {
ts1.rollback();//回滚第一个事务
} finally {
//释放资源
}
}
下面来演示一下 methodA()事务执行成功,methodB()事务执行失败(回滚)的情况; methodA() methodB()
因为,事务B被挂起,所以事务B会超时,事务B会失败,但是,因为事务A是新建立的,它可以成功;所以,在数据库中,应该插入methodA 的字段,结果如下: 测试成功
5.NOT_SUPORTED
以非事务方式运行,如果当前存在事务,则把当前事务挂起。
6. NEVER
以非事务方式运行,如果当前存在事务,则抛出异常。
7. NESTED
如果当前存在事务,会作为当前事务的嵌套事务来运行;如果当前没有事务,该值就等价于REQUIRED;
@Transactional(propagation = Propagation.REQUIRED)
methodB(){
doSomeThingA();
methodA();
doSomeThingB();
}
@Transactional(propagation = Propagation.NEWSTED)
methodA(){
……
}
如果,单独调用methodA(),则按照REQUIRED属性执行。如果,调用methodB(),那么就相当于下面效果
{
Connection con = null;
Savepoint savepoint = null;
try{
con = getConnection();
con.setAutoCommit(false);
doSomeThingA();
savepoint = con2.setSavepoint();
try{
methodA();
} catch(RuntimeException ex) {
con.rollback(savepoint);
} finally {
//释放资源
}
doSomeThingB();
con.commit();
} catch(RuntimeException ex) {
con.rollback();
} finally {
//释放资源
}
}
在调用methodA方法之前,会调用setSavepoint方法,保存当前状态到savepoiint。如果,methodA方法调用失败,这回滚methodA,不会影响methodB() ,但是需要注意的是,此时,事务并没有进行提交,如果,后续的代码dosomethingB()调用失败,这回滚methodA()方法的所有操作。
总结:嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作,而内存事务操作失败时,并不会引起外层事务的回滚。
测试:
methodA():
methodB():
经过测试,事务A执行是成功的,事务B执行失败,最终,把事务A的内容也进行回滚,数据库中什么数据都没有;
NESTED 和 REQUIRES_NEW的区别
使用REQUIRES_NEW时,内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行提交以后,外层事务不能对其回滚;感觉除了外层事务由于内层事务挂起,容易导致时间超时外,两者就没啥联系了。同时它需要JTA事务管理器的支持。
使用NESTED时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。
REQUIRES_NEW 启动一个新的,不依赖于环境的“内部”事务,这个事务不依赖于外部事务,它拥有自己的隔离范围,自己的锁等等。当内部事务开始执行时,外部事务将被挂起,内部事务结束时,外部事务将继续执行。
另一方面,NESTED开始一个“嵌套的”事务,它是已经存在的事务的一个真正的子事务,嵌套事务开始执行时,将取得一个savepoint。如果,这个嵌套事务失败,我们将回滚到此savepoint.嵌套事务是外部事务的一部分,只有外部事务结束后它才会被提交。
gitee 代码
推荐阅读
-
用 C# 在 JSON 字符串中使用转义字符
-
Spring Boot 会确定轨道数据是否经过设定的打孔点,并在 PGSQL 中将这些点拼接成一条线,以确定点是否在该线上或该线 50 米范围内
-
在 Spring boot 中使用事务
-
用 3 个步骤脱敏 Spring Boot 日志
-
用 3 个步骤脱敏 Spring Boot 日志
-
Hystrix 应用程序:如何在 Spring Boot 中使用 Hystrix?
-
在 Spring Boot 中禁用 Swagger 的三种方法
-
基于 Spring Boot+Vue 的大学办公室行政事务管理系统
-
在 Spring 中使用异步注解 @Async,其使用原则和可能导致的问题
-
在 Spring MVC 中使用列表接收数组数据时出错