一. 什么是Spring事务
Spring 事务是 Spring 框架提供的一种机制,用于管理数据库操作或其他资源的一组相关操作,以确保它们在一个原子、一致、可靠和隔离的执行单元内进行。事务用于维护数据的完整性并支持并发访问数据库时的数据一致性。
Spring 事务的主要特点包括:
- 原子性(Atomicity):事务中的所有操作要么全部成功执行,要么全部不执行。如果事务中的任何一个操作失败,则整个事务将被回滚到初始状态,以保持数据的一致性。
- 一致性(Consistency):事务在执行之前和之后都必须使数据库保持一致状态。这意味着事务中的操作会遵循预定义的业务规则,以确保数据的有效性和完整性。
- 隔离性(Isolation):事务的隔离级别定义了事务之间的相互影响程度。Spring 支持多个隔离级别,例如读未提交、读已提交、可重复读和串行化。通过设置适当的隔离级别,可以控制事务之间的并发读写操作带来的数据不一致问题。
- 可靠性(Durability):事务在成功提交后,对数据的更新将永久保存到数据库中,即使系统发生故障或重启,数据也不会丢失。
Spring 事务通过 AOP(面向切面编程)实现,Spring事务操作分为了两种方式:
- 编程式事务(⼿动写代码操作事务)。
- 声明式事务(利⽤注解⾃动开启和提交事务)。
使用 Spring 事务可以将多个数据库操作或其他资源访问操作组织成一个逻辑单元,并确保这些操作要么全部成功执行,要么全部回滚。这有助于提高应用程序的可靠性、一致性和并发性。
其实在之前的MySQL部分,我们对事务已经有了初步的了解,忘记的小伙伴可以查看: MySQL:索引事务
二. Spring中事务的实现
1. Spring编程式事务
Spring编程式事务是是一种通过编写代码显式管理事务的方法,而不依赖于注解或配置文件。它提供了更细粒度的事务控制,允许在方法级别或代码块级别指定事务的起始、提交和回滚。 Spring手动操作事务和MySQL 操作事务类似,主要有三个操作步骤:
- 开启事务
- 提交事务
- 回滚事务
SpringBoot
内置了两个对象,DataSourceTransactionManager
用来获取事务(开启事务)、提交或回滚事务的,而TransactionDefinition
是事务的属性,在获取事务的时候需要将
TransactionDefinition
传递进去从而获得一个事务TransactionStatus
,实现代码如下:
package com.example.demo.controller;
import com.example.demo.service.UserService;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
//编程式事务:
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
//Jdbc事务管理器
@Resource
private DataSourceTransactionManager transactionManager;
//定义事务属性
@Resource
private TransactionDefinition transactionDefinition;
@RequestMapping("/del")
public int del(Integer id){
if(id == null || id < 0){
return 0;
}
//1. 开启事务
TransactionStatus transactionStatus = null;
int result = 0;
try{
transactionStatus = transactionManager.getTransaction((transactionDefinition));
//业务操作,删除用户
result = userService.del(id);
System.out.println("删除: " result);
//2. 提交事务/回滚事务
transactionManager.commit(transactionStatus);//提交事务
}catch (Exception e){
if(transactionStatus != null){
transactionManager.rollback(transactionStatus);//回滚事务
}
}
return result;
}
}
启动程序,然后在浏览器中输入:即为删除id为1的admin
可以看到管理员成功被删除. 若要实现回滚操作:
代码语言:javascript复制@RequestMapping("/del")
public int del(Integer id){
if(id == null || id < 0){
return 0;
}
//1. 开启事务
TransactionStatus transactionStatus = null;
int result = 0;
transactionStatus = transactionManager.getTransaction((transactionDefinition));
//业务操作,删除用户
result = userService.del(id);
System.out.println("删除: " result);
//2. 提交事务/回滚事务
// transactionManager.commit(transactionStatus);//提交事务
transactionManager.rollback(transactionStatus);//回滚事务
return result;
}
我们可以看到,虽然编程式事务可以实现事务,但是操作很繁琐,接下来,我们可以使用较为简单的声明式事务.
2. 声明式事务
声明式事务是利用注解自动开启和提交事务.
声明式事务的实现很简单,只需在方法上加入@Transactional
注解就可以实现,无需手动开启事务和提交事务,进入方法时自动开启事务,方法执行完会自动提交事务,如果途中发生了没有处理的异常就会自动回滚事务.
package com.example.demo.controller;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user2")
public class UserController2 {
@Autowired
private UserService userService;
@Transactional(timeout = 3) // 在方法开始之前开启事务,方法正常执行结束之后提交事务,如果执行途中发生异常,则回滚事务
@RequestMapping("/del")
public int del(Integer id) {
if (id == null || id <= 0) return 0;
return userService.del(id);
}
}
运行成功:
如果代码中有异常的话:
代码语言:javascript复制 @Transactional(timeout = 3) // 在方法开始之前开启事务,方法正常执行结束之后提交事务,如果执行途中发生异常,则回滚事务
@RequestMapping("/del")
public int del(Integer id) {
if (id == null || id <= 0) return 0;
int num = 10/0;
return userService.del(id);
}
2.1 trycatch下事务不会自动回滚的解决方案
但是如果加上Try catch
来处理异常,此时事务就不会回滚了,数据也就正式被删除了.但是出现这种情况,它就是出现了异常,我们是想要它进行回滚操作的.我们有两种处理办法:
1. 手动抛出异常
@Transactional(timeout = 3) // 在方法开始之前开启事务,方法正常执行结束之后提交事务,如果执行途中发生异常,则回滚事务
@RequestMapping("/del")
public int del(Integer id) {
if (id == null || id <= 0) return 0;
int result = 0;
try{
result = userService.del(id);
System.out.println(result);
int num = 10/0;
}catch (Exception e){
throw e;//手动将异常抛出
}
return result;
}
2. 手动回滚事务
代码语言:javascript复制 @Transactional(timeout = 3) // 在方法开始之前开启事务,方法正常执行结束之后提交事务,如果执行途中发生异常,则回滚事务
@RequestMapping("/del")
public int del(Integer id) {
if (id == null || id <= 0) return 0;
int result = 0;
try{
result = userService.del(id);
System.out.println(result);
int num = 10/0;
}catch (Exception e){
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return result;
}
TransactionAspectSupport.currentTransactionStatus()
用于获取到当前事务,setRollbackOnly()
是用于回滚的方法.
2.2 @Transactional 作用范围
@Transactional 可以用来修饰方法或类:
- 修饰方法时:需要注意只能应用到 public方法上,否则不生效。推荐此种用法。
- 修饰类时:表明该注解对该类中所有的public方法都生效。
2.3 @Transactional 参数说明
参数 | 作用 |
---|---|
value | 当配置了多个事务管理器时,可以使用该属性指定选择哪个事务管理器. |
transactionManager | 当配置了多个事务管理器时,可以使用该属性指定选择哪个事务管理器. |
propagation | 事务的传播行为,默认值为Propagation.REQUIRED |
isolation | 事务的隔离级别,默认值为lsolation.DEFAULT |
timeout | 事务的超时时间,默认值为-1.如果超过该时间限制但事务还没有完成,则自动回滚事务. |
readOnly | 指定事务是否为只读事务,默认值为false;为了忽略那些不需要事务的方法,比如读取数据,可以设置read-only为true. |
rollbackFor | 用于指定能够触发事务回滚的异常类型,可以指定多个异常类型. |
rollbackForClassName | 用于指定能够触发事务回滚的异常类型,可以指定多个异常类型. |
noRollbackFor | 抛出指定的异常类型,不回滚事务,也可以指定多个异常类型. |
noRollbackForClassName | 抛出指定的异常类型,不回滚事务,也可以指定多个异常类型. |
2.4 @Transactional 工作原理
@Transactional
的工作原理如下:
- 在方法执行前,Spring 根据
@Transactional
注解的配置来创建一个事务上下文,并将其与当前线程关联起来。 - 当方法开始执行时,Spring 检查当前线程是否已经关联了一个事务上下文。如果没有关联,则根据
@Transactional
注解的配置,启动一个新的事务。 - 方法执行过程中,如果没有发生异常,则 Spring 会在方法执行结束后提交事务,将对数据库的更改持久化到数据库。
- 如果方法执行过程中抛出了异常,Spring 将捕获该异常并判断是否需要回滚事务。根据
@Transactional
注解的配置,如果异常属于回滚规则中指定的类型,则发起事务回滚操作,撤销已经执行的数据库操作。 - 最后,Spring 根据事务的提交或回滚结果,进行相应的清理工作,关闭事务上下文。
需要注意的是:@Transactional 注解只对公共方法生效,因此仅能应用于公共的非静态方法.
除了直接将 @Transactional
注解应用于方法上,还可以将其应用于类上。在这种情况下,所有在该类中声明的方法都将具有相同的事务属性。
@Transactional
注解通过简化事务管理过程,提供了一种声明式的事务管理方式,使开发者能够更轻松地实现对数据库操作的事务性控制。
总之,@Transactional
是基于AOP
实现的,AOP又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用JDK的动态代理,如果目标对象没有实现了接口,会使用CGLIB动态代理。@Transactional
在开始执行业务之前,通过代理先开启事务,在执行成功之后再提交事务。如果中途遇到的异常,则回滚事务。
三. 事务的隔离级别
1. 事务的四大特性
事务的四大特性(ACID)是指原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),它们是关系型数据库管理系统(RDBMS)中用于保证数据操作的可靠性和一致性的基本属性。
- 原子性(Atomicity):事务是一个原子操作单元,要么全部执行成功,要么全部失败回滚。它是不可分割的最小操作单位,将多个操作视为一个整体进行处理。如果事务中的任何一项操作失败,所有已经执行的操作将被撤销,数据库回到事务开始前的状态。
- 一致性(Consistency):事务的执行使数据库从一个一致状态转移到另一个一致状态。在事务开始之前和事务结束之后,数据库的完整性约束没有被破坏。这意味着事务在执行过程中不能破坏数据库的完整性、一致性约束以及业务规则。
- 隔离性(Isolation):多个并发事务之间应该相互隔离,每个事务的执行都应该与其他事务隔离开来,互不干扰。事务隔离级别定义了多个事务同时运行时各自可见的数据以及彼此之间的互动程度。常见的隔离级别有读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。
- 持久性(Durability):事务一旦提交,它对数据库中的数据变更就是永久性的,即使发生系统崩溃或电源故障等意外情况,数据库也能够保证已提交的事务不会丢失。持久性是通过将事务日志记录到磁盘或其他持久化介质来实现的。
这四个特性确保了事务的安全性、可靠性和一致性。它们是关系型数据库管理系统保证数据操作的基本要求,同时也是在设计应用程序时需要考虑和遵循的事务规范。 这四种特性中,只有隔离性(隔离级别)是可以设置的。
2. Spring中设置事务隔离级别
之前在MySQL中我们学过的事务隔离级别有四种.忘记的小伙伴可以查看:MySQL:索引事务.
此处我们要讲解的是Spring中的事务隔离级别,在 Spring 中,可以通过 @Transactional
注解的isolation
属性来设置事务的隔离级别。isolation
属性接受一个枚举值,用于指定所需的隔离级别。
Spring中的事务隔离级别分为五种,分别是:
DEFAULT
:使用底层数据库的默认隔离级别。READ_UNCOMMITTED
:读取未提交的数据。最低的隔离级别,在此级别下,一个事务可以读取到其他事务尚未提交的数据。READ_COMMITTED
:读取已提交的数据。该级别下,一个事务只能读取到其他事务已经提交的数据,避免了脏读。REPEATABLE_READ
:可重复读。在此级别下,事务开始时读取的数据集合将被固定,即使其他事务修改了数据,当前事务仍然看到最初读取的数据集合。防止了脏读和不可重复读。SERIALIZABLE
:串行化。在此级别下,事务是按顺序一个接一个地执行,避免了脏读、不可重复读和幻读。
具体实现格式如下:
代码语言:javascript复制@Transactional(isolation = Isolation.READ_COMMITTED)
public void doSomething() {
// 事务操作
}
四. Spring事务传播机制
1. 事务传播机制
事务传播机制是指在多个事务操作嵌套执行时,各事务之间如何相互关联和影响的规则。
2. 为什么需要事务传播机制
事务传播机制的存在是为了**处理多个事务操作之间的关系和影响,以确保数据的一致性和完整性。**以下是一些需要事务传播机制的情况:
- 嵌套调用:当一个事务方法内部调用另一个事务方法时,事务传播机制可以控制这两个事务之间的关系。通过适当的传播行为配置,可以使内部方法加入当前事务、创建新的事务或以非事务方式执行,从而确保多个事务操作在正确的事务上下文中执行。
- 业务流程:在一个业务流程中可能涉及多个事务操作,比如订单处理过程中的库存扣减、支付确认和物流更新等。事务传播机制可以确保这些事务操作在合适的事务边界内进行,避免数据不一致的情况发生。
- 多线程并发:在多线程环境下,不同的线程可能同时执行事务操作。事务传播机制可以在并发执行的情况下维护事务的隔离性和原子性,避免数据冲突和并发问题。
- 异常处理:在事务中,如果出现异常,事务传播机制可以决定是继续传播异常还是回滚事务。通过配置合适的传播行为,可以在异常发生时采取适当的处理方式,以保证数据的一致性。
总之,事务传播机制允许我们在多个事务操作中控制事务的行为方式,确保数据的正确处理和一致性。通过合理配置事务传播行为,可以满足不同的应用场景下对事务处理的需求。
3. 事务传播机制有哪些
Spring框架提供了七种事务传播行为,用于控制事务方法的执行方式: (包含例子:假设你正在计划一次旅行。你需要预订机票、酒店和租车,并确保这些操作在旅行期间的可用性和连贯性。)
-
REQUIRED
(默认):如果当前已经存在事务,则加入该事务中执行;如果当前没有事务,则创建一个新的事务。这是最常用的传播行为。 当你预订机票时,系统首先检查目标航班的余票情况,然后从你的账户中扣除相应金额作为机票费用。如果你的预订操作已经在一个事务中,那么检查余票和扣款将加入到该事务中。如果没有事务,则创建一个新的事务来执行这两个操作。 -
SUPPORTS
:如果当前已经存在事务,则加入该事务中执行;如果当前没有事务,则以非事务方式执行。适合于不需要强制事务的场景。 当你查询酒店房间的可用性时,系统根据你选择的日期和地点返回相关信息。如果你的 查询操作已经在一个事务中,那么查询房间可用性将加入到该事务中。如果没有事务,则以非事务方式执行查询。 -
MANDATORY
:如果当前已经存在事务,则加入该事务中执行;如果当前没有事务,则抛出异常。适合于必须依赖事务的场景。 当你要修改预订的租车时间时,系统需要验证该租车订单是否存在。在租车订单修改方法中,通过判断当前是否存在事务来决定是否允许修改操作。如果你已经在事务中,则允许修改;如果没有事务,则抛出异常。 -
REQUIRES_NEW
:无论当前是否存在事务,都会挂起当前事务,创建一个新的事务执行。适合于需要独立的事务执行的场景。(事务挂起是指在执行一个事务的过程中,暂时中断该事务的执行,并将其保存在一个临时状态下,以便执行其他事务。当挂起的事务恢复执行时,会从挂起的点继续执行。) 当你取消酒店预订时,需要先将预订状态设为取消,并返还预付款项。无论你当前是否处于一个事务中,这两个操作都将在一个新的事务中执行,并将原来的事务挂起。 -
NOT_SUPPORTED
:以非事务方式执行操作,如果当前存在事务,则将其挂起。适合于不需要事务支持的场景。 当你查询机票价格时,系统根据出发地和目的地返回相关信息。如果你当前处于一个事务中,该查询操作将以非事务方式执行,而不会干扰原有事务的状态。 -
NEVER
:以非事务方式执行操作,如果当前存在事务,则抛出异常。适合于不能在事务中执行的场景。 当你申请旅行保险时,需要确保该操作不会在任何事务中执行。如果你当前处于一个事务中,则抛出异常并禁止执行保险申请操作,以确保操作的独立性。 -
NESTED
:如果当前已经存在事务,则在嵌套事务中执行;如果当前没有事务,则创建一个新的事务。嵌套事务有自己的保存点,并可以回滚到保存点。适合于需要嵌套事务支持的场景。 当你进行机票改签操作时,需要先将原机票设为无效,然后生成一张新的机票。如果你当前已经在一个事务中,那么这两个操作将在嵌套事务中执行,嵌套事务有自己的保存点,并可以回滚到保存点。如果没有事务,则创建一个新的事务来执行这两个操作。
例如:
代码语言:javascript复制@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
// 事务操作
methodB();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// 事务操作
}
在上面的例子中,methodA()
使用了默认的传播行为REQUIRED
,如果当前存在事务,则methodB()
将加入该事务;而methodB()
使用了传播行为REQUIRES_NEW
,无论当前是否存在事务,都将创建一个新的事务来执行。
通过选择合适的事务传播行为并进行配置,可以确保在多个事务操作中正确地管理事务的行为和关系,从而维护数据的一致性和完整性。
嵌套事务和加入事务的区别: 嵌套事务和加入事务都是事务传播机制的不同实现方式,它们之间有以下区别:
- 事务范围:
- 嵌套事务是内部事务运行在外部事务的范围内,可以看作是外部事务的子事务,嵌套事务可以独立提交或回滚。
- 加入事务是将一个方法或代码片段加入到已有的事务中,与外部事务共享同一个事务范围,不能单独提交或回滚。
- 事务行为:
- 嵌套事务具有独立的事务保存点,可以在内部事务中进行回滚操作,而不会影响外部事务的状态。内部事务执行成功后,可以选择提交到外部事务或者继续嵌套使用。
- 加入事务没有独立的事务保存点,它的所有操作都与外部事务绑定,只有外部事务提交时,才会将加入事务的操作一同提交。
- 使用场景:
- 嵌套事务适用于需要在一个事务中执行多个独立的子操作,并且子操作可以单独回滚的情况。如果需要某个子操作的失败不会影响其他子操作,可以使用嵌套事务。
- 加入事务适用于需要将一段代码或方法加入到已有事务中执行的情况,例如在一个服务方法中调用其他服务方法,希望它们共享同一个事务。通过加入事务,可以确保这些方法在一个事务中执行,并且要么都成功提交,要么都回滚。