Spring 事务和事务传播机制

发布于:2025-09-12 ⋅ 阅读:(18) ⋅ 点赞:(0)

Spring 事务和事务传播机制



什么是事务

事务是一组操作的集合,是一个不可分割的操作

事务会把所有的操作作为一个整体,一起向数据库提交或者是撤销操作请求。所以这组操作要么同时成功,要么同时失败

事务操作

事务的操作主要有三步:

  1. 开启事务:start transaction/begin(一组操作前开启事务)。
  2. 提交事务:commit(这组操作全部成功,提交事务)。
  3. 回滚事务:rollback(这组操作中间任何一个操作出现异常,回滚事务)。
-- 开启事务
start transaction;

-- 提交事务
commit;

-- 回滚事务
rollback;

Spring 事务的实现

前面课程我们讲了MySQL的事务操作,Spring对事务也进行了实现

Spring中的事务操作分为两类:

  1. 编程式事务(手动写代码操作事务)
  2. 声明式事务(利用注解自动开启和提交事务)

Spring编程式事务

Spring编程式事务:

  1. 开启事务
  2. 提交事务
  3. 回滚食物

SpringBoot内置了两个对象:

  • DataSourceTransactionManager:事务管理器,用来获取事务(开启事务)、提交或回滚事务
  • TransactionDefinition:是事务的属性,在获取事务的时候需要将TransactionDefinition传递进去从而获得一个事务TransactionStatus
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
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;

@RequestMapping("/user")
@RestController
public class UserController {

    // JDBC 事务管理器
    @Autowired
    private DataSourceTransactionManager dataSourceTransactionManager;

    // 定义事务属性
    @Autowired
    private TransactionDefinition transactionDefinition;

    @Autowired
    private UserService userService;

    @RequestMapping("/registry")
    public String registry(String name, String password) {
        // 开启事务
        TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);

        // 用户注册
        userService.registryUser(name, password);

        // 提交事务
        dataSourceTransactionManager.commit(transactionStatus);
        // 回滚事务
        // dataSourceTransactionManager.rollback(transactionStatus);
        return "注册成功";
    }
}

以上的代码,虽然可以实现事务,但是操作也很繁琐。接下来我们学习声明式事务

Spring 声明式事务 @Transactional

声明式事务很简单

  • 首先先添加依赖
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
</dependency>
  • 在需要事务的方法上添加 @Transactional 注解就可以实现了。无需手动开启事务和提交事务,进入方法时自动开启事务,方法执行完会自动提交事务,如果中途发生了没有处理的异常会自动回滚事务。

我们来看代码实现:

@RequestMapping("/trans")
@RestController
public class TransactionalController {
    @Autowired
    private UserService userService;

    @Transactional
    @RequestMapping("/registry")
    public String registry(String name, String password) {
        // 用户注册
        userService.registryUser(name, password);
        return "注册成功";
    }
}

注意⚠️:
当出现异常的时候,日志显示数据会插入成功,但是数据库没有新增数据,事务进行了回滚

@Transactional作用

@Transactional 可以用来修饰方法或类:

  • 修饰方法时:只有修饰 public 方法时才生效(修饰其他方法时不会报错,也不生效)[推荐]。
  • 修饰类时:对 @Transactional 修饰的类中所有的 public 方法都生效。

方法/类被 @Transactional 注解修饰时,在目标方法执行开始之前,会自动开启事务,方法执行结束之后,自动提交事务

如果在方法执行过程中,出现异常,且异常未被捕获,就进行事务回滚操作

如果异常被程序捕获,方法就被认为是成功执行,依然会提交事务

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional
    public void addUser(String username, String password) {
        try {
            String sql = "INSERT INTO users (username, password) VALUES (?,?)";
            jdbcTemplate.update(sql, username, password);
            throw new RuntimeException("模拟异常"); 
        } catch (Exception e) {
            // 异常被捕获,事务依然会提交
            System.out.println("捕获到异常,但事务会提交"); 
        }
    }
}

在addUser方法中,虽然抛出了运行时异常,但是异常被try-catch块捕获了。这种情况下,Spring 会认为方法成功执行,依然会提交事务,插入数据库的操作不会回滚

事务回滚有以下两种方法:

  1. 重新抛出异常
@Transactional
@RequestMapping("/registry")
public String registry(String name, String password) {
    //用户注册
    userService.registryUser(name, password);

    log.info("用户数据插入成功");
    //对异常进行捕获
    try {
        //强制程序抛出异常
        int a = 10 / 0;
    } catch (Exception e) {
        //将异常重新抛出去
        throw e;
    }
    return "注册成功";
}
  1. 手动回滚事务
    使用 TransactionAspectSupport.currentTransactionStatus() 得到当前的事务,并使用 setRollbackOnly 设置回滚。
@Transactional
@RequestMapping("/registry")
public String registry(String name, String password) {
    //用户注册
    userService.registryUser(name, password);
    log.info("用户数据插入成功");
    //对异常进行捕获
    try {
        //强制程序抛出异常
        int a = 10 / 0;
    } catch (Exception e) {
        // 手动回滚事务
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
    return "注册成功";
}

@Transactional详解

我们主要学习@Transactional注解当中的三个常见属性:

  1. rollbackFor:异常回滚属性,指定能够触发事务回滚的异常类型。可以指定多个异常类型
  2. Isolation:事务的隔离级别。默认值为Isolation.DEFAULT
  3. propagation:事务的传播机制。默认值为Propagation.REQUIRED

rollbackFor

@Transactional 默认只有在遇到异常和报错的时候才会回滚,不运行的情况下异常不回滚,即Exception的子类中,除了RuntimeException及其子类

在这里插入图片描述
如果我们需要所有异常都回滚,需要来配置 @Transactional 注解当中的 rollbackFor 属性,通过 rollbackFor 这个属性指定出现何种异常类型时事务进行回滚

@Transactional(rollbackFor = Exception.class)
@RequestMapping("/r2")
public String r2(String name, String password) throws IOException {
    //用户注册
    userService.registryUser(name, password);
    log.info("用户数据插入成功");
    if (true) {
        throw new IOException();
    }
    return "r2";
}

结论:

  • 默认运行遇到异常RuntimeException和报错才会回滚
  • 如果需要指定异常类型回滚,需要rollbackFor属性指定

事务隔离级别

MySQL事务隔离级别

  1. 读未提交(READ UNCOMMITTED):读未提交,也叫未提交读。该隔离级别的事务可以看到其他事务中未提交的数据

因为其他事务未提交的数据可能会发生回滚,但是该隔离级别却可以读到,我们把该级别读到的数据称之为脏数据,这个问题称之为脏读

  1. 读提交(READ COMMITTED):读已提交,也叫提交读。该隔离级别的事务能读取到已经提交事务的数据

该隔离级别不会有脏读的问题。但由于在事务的执行中可以读取到其他事务提交的结果,所以在不同时间的相同 SQL 查询可能会得到不同的结果,这种现象叫做不可重复读

  1. 可重复读(REPEATABLE READ):事务不会读到其他事务对已有数据的修改,即使其他事务已提交。也就可以确保同一事务多次查询的结果一致,但是其他事务新插入的数据,是可以感知到的。这也就引发了幻读问题。可重复读,是 MySQL 的默认事务隔离级别

比如此级别的事务正在执行时,另一个事务成功的插入了某条数据,但因为它每次查询的结果都是一样的,所以会导致查询不到这条数据,自己重复插入时又失败(因为唯一约束的原因)。明明在事务中查询不到这条信息,但自己就是插入不进去,这个现象叫幻读

  1. 串行化(SERIALIZABLE):序列化,事务最高隔离级别。它会强制事务排序,使之不会发生冲突,从而解决了脏读、不可重复读和幻读问题,但因为执行效率低,所以真正使用的场景并不多
事务隔离级别 脏读 不可重复读 幻读
读未提交(READ UNCOMMITTED)
读已提交(READ COMMITTED) ×
可重复读(REPEATABLE READ) × ×
串行化(SERIALIZABLE) × × ×

Spring 事务隔离级别

Spring中事务隔离级别有5种:

  1. Isolation.DEFAULT:以连接的数据库的事务隔离级别为主
  2. Isolation.READ_UNCOMMITTED:读未提交,对应SQL标准中 READ UNCOMMITTED
  3. Isolation.READ_COMMITTED:读已提交,对应SQL标准中 READ COMMITTED
  4. Isolation.REPEATABLE_READ:可重复读,对应SQL标准中 REPEATABLE READ
  5. Isolation.SERIALIZABLE:串行化,对应SQL标准中 SERIALIZABLE

事务隔离级别枚举类

public enum Isolation {
    DEFAULT(-1),
    READ_UNCOMMITTED(1),
    READ_COMMITTED(2),
    REPEATABLE_READ(4),
    SERIALIZABLE(8);

    private final int value;

    private Isolation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}

事务隔离级别设置示例

Spring 中事务隔离级别可以通过 @Transactional 中的 isolation 属性进行设置:

@Transactional(isolation = Isolation.READ_COMMITTED)
@RequestMapping("/r3")
public String r3(String name, String password) throws IOException {
    //... 代码省略
    return "r3";
}

Spring 事务传播机制

什么是事务传播机制

事务传播机制就是:多个事务方法存在调用关系时,事务是如何在这些方法间进行传播的

事务隔离级别解决的是多个事务同时调用一个数据库的问题

在这里插入图片描述

事务传播机制解决的是一个事务在多个节点(多个方法)之间的传递问题

在这里插入图片描述

事务传播机制有哪些

@Transactional 注解支持事务传播机制的设置,通过 propagation 属性来指定传播行为

Spring 事务传播机制有以下 7 种:

  1. Propagation.REQUIRED:默认的事务传播级别。如果当前存在事务,则加入该事务。如果当前没有事务,则创建一个新的事务
  2. Propagation.SUPPORTS:如果当前存在事务,则加入该事务。如果当前没有事务,则以非事务的方式继续运行
  3. Propagation.MANDATORY:强制性。如果当前存在事务,则加入该事务。如果当前没有事务,则抛出异常
  4. Propagation.REQUIRES_NEW:创建一个新的事务。如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法都会新开启自己的事务,且开启的事务相互独立,互不干扰
  5. Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起(不用)
  6. Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常
  7. Propagation.NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行。如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED

概念解释:

比如一对新人要结婚了,关于是否需要房子:

  1. Propagation.REQUIRED:需要有房子。如果你有房,我们就一起住,如果你没房,我们就一起买房。(如果当前存在事务,则加入该事务。如果当前没有事务,则创建一个新的事务)
  2. Propagation.SUPPORTS:可以有房子。如果你有房,那就一起住。如果没房,那就租房。(如果当前存在事务,则加入该事务。如果当前没有事务,则以非事务的方式继续运行)
  3. Propagation.MANDATORY:必须有房子。要求必须有房,如果没房就不结婚。(如果当前存在事务,则加入该事务。如果当前没有事务,则抛出异常)
  4. Propagation.REQUIRES_NEW:必须买新房。不管你有没有房,必须要两个人一起买房。即使有房也不住。(创建一个新的事务。如果当前存在事务,则把当前事务挂起)
  5. Propagation.NOT_SUPPORTED:不需要房。不管你有没有房,我都不住,必须租房。(以非事务方式运行,如果当前存在事务,则把当前事务挂起)
  6. Propagation.NEVER:不能有房子。(以非事务方式运行,如果当前存在事务,则抛出异常)
  7. Propagation.NESTED:如果你没房,就一起买房。如果你有房,我们就以房子为根据地,做点小生意。(如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行。如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED

Spring事务传播机制使用场景

REQUIRED(加入事务)

看下面代码实现:

  1. 用户注册,插入一条数据
  2. 记录操作日志,插入一条数据(出现异常)
    观察 propagation = Propagation.REQUIRED 的执行结果
@RequestMapping("/propagar")
@RestController
public class PropagationController {
    @Autowired
    private UserService userService;
    @Autowired
    private LogService logService;

    @Transactional(propagation = Propagation.REQUIRED)
    @RequestMapping("/p1")
    public String p1(String name, String password) {
        // 用户注册
        userService.registryUser(name, password);
        logService.insertLog(name, "用户注册");
        return "p1";
    }
}

对应的 UserServiceLogService 都添加上 @Transactional(propagation = Propagation.REQUIRED)

@Service
public class UserService {
    @Autowired
    private UserInfoMapper userInfoMapper;

    @Transactional(propagation = Propagation.REQUIRED)
    public void registryUser(String name, String password) {
        // 插入用户信息
        userInfoMapper.insert(name, password);
    }
}
@Service
public class LogService {
    @Autowired
    private LogInfoMapper logInfoMapper;

    @Transactional(propagation = Propagation.REQUIRED)
    public void insertLog(String name, String op) {
        int a = 10 / 0;
        // 记录用户操作
        logInfoMapper.insertLog(name, "用户注册");
    }
}

运行程序,发现数据库没有插入任何数据。
流程描述:

  1. p1 方法开始事务
  2. 用户注册,插入一条数据(执行成功)(和 p1 使用同一个事务)
  3. 记录操作日志,插入一条数据(出现异常,执行失败)(和 p1 使用同一个事务)
  4. 因为步骤3出现异常,事务回滚,步骤2和3使用同一个事务,所以步骤2的数据也回滚了。

REQUIRES_NEW(新建事务)

将上述 UserServiceLogService 中相关方法事务传播机制改为 Propagation.REQUIRES_NEW

@Service
public class UserService {
    @Autowired
    private UserInfoMapper userInfoMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void registryUser(String name, String password) {
        // 插入用户信息
        userInfoMapper.insert(name, password);
    }
}
@Service
public class LogService {
    @Autowired
    private LogInfoMapper logInfoMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void insertLog(String name, String op) {
        int a = 10 / 0;
        // 记录用户操作
        logInfoMapper.insertLog(name, "用户注册");
    }
}

运行程序,发现用户数据插入成功了,日志表数据插入失败。
LogService 方法中的事务不影响 UserService 中的事务。
当我们不希望事务之间相互影响时,可以使用该传播行为。

NEVER(不支持当前事务,抛异常)

修改 UserService 中对应方法的事务传播机制为 Propagation.NEVER

@Service
public class UserService {
    @Autowired
    private UserInfoMapper userInfoMapper;

    @Transactional(propagation = Propagation.NEVER)
    public void registryUser(String name, String password) {
        // 插入用户信息
        userInfoMapper.insert(name, password);
    }
}

程序执行报错,没有数据插入。

NESTED(嵌套事务)

将上述 UserServiceLogService 中相关方法事务传播机制改为 Propagation.NESTED

@Service
public class UserService {
    @Autowired
    private UserInfoMapper userInfoMapper;

    @Transactional(propagation = Propagation.NESTED)
    public void registryUser(String name, String password) {
        // 插入用户信息
        userInfoMapper.insert(name, password);
    }
}
@Service
public class LogService {
    @Autowired
    private LogInfoMapper logInfoMapper;

    @Transactional(propagation = Propagation.NESTED)
    public void insertLog(String name, String op) {
        int a = 10 / 0;
        // 记录用户操作
        logInfoMapper.insertLog(name, "用户注册");
    }
}

运行程序,发现没有任何数据插入。
流程描述:

  1. Controllerp1 方法开始事务
  2. UserService 用户注册,插入一条数据(嵌套 p1 事务)
  3. LogService 记录操作日志,插入一条数据(出现异常,执行失败)(嵌套 p1 事务,回滚当前事务,数据添加失败)
  4. 由于是嵌套事务,LogService 出现异常之后,往上找调用它的方法和事务,所以用户注册也失败了
  5. 最终结果是两个数据都没有添加

NESTED 和 REQUIRED 有什么区别?

我们在 LogService 进行当前事务回滚,修改 LogService 代码如下:

@Service
public class LogService {
    @Autowired
    private LogInfoMapper logInfoMapper;

    @Transactional(propagation = Propagation.NESTED)
    public void insertLog(String name, String op) {
        try {
            int a = 10 / 0;
        } catch (Exception e) {
            // 回滚当前事务
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
        // 记录用户操作
        logInfoMapper.insertLog(name, "用户注册");
    }
}

重新运行程序,发现用户表数据添加成功,日志表添加失败。
LogService 中的事务已经回滚,但是嵌套事务不会回滚嵌套之前的事务,也就是说嵌套事务可以实现部分事务回滚。

对比 REQUIRED,把 NESTED 传播机制改为 REQUIRED,修改代码如下:

@Service
public class UserService {
    @Autowired
    private UserInfoMapper userInfoMapper;

    @Transactional(propagation = Propagation.REQUIRED)
    public void registryUser(String name, String password) {
        // 插入用户信息
        userInfoMapper.insert(name, password);
    }
}
@Service
public class LogService {
    @Autowired
    private LogInfoMapper logInfoMapper;

    @Transactional(propagation = Propagation.REQUIRED)
    public void insertLog(String name, String op) {
        try {
            int a = 10 / 0;
        } catch (Exception e) {
            // 回滚当前事务
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
        // 记录用户操作
        logInfoMapper.insertLog(name, "用户注册");
    }
}

重新运行程序,发现用户表和日志表的数据都添加失败了。
REQUIRED 如果回滚就是回滚所有事务,不能实现部分事务的回滚(因为属于同一个事务)。

NESTED 和 REQUIRED 区别

  • 整个事务如果全部执行成功,二者的结果是一样的。
  • 如果事务一部分执行成功,REQUIRED 加入事务会导致整个事务全部回滚,NESTED 嵌套事务可以实现局部回滚,不影响上一个方法中执行的结果。

嵌套事务之所以能够实现部分事务的回滚,是因为事务中有一个保存点(savepoint)的概念,嵌套事务进入之后相当于新建了一个保存点,而滚回时只回滚到当前保存点。

REQUIRED 是加入到当前事务中,并没有创建事务的保存点,因此出现了回滚就是整个事务回滚,这就是嵌套事务和加入事务的区别。


网站公告

今日签到

点亮在社区的每一天
去签到