大厂面试拦路虎!Spring 事务传播行为实现全揭秘

Spring 事务传播行为

一、事务传播行为的定义

事务传播行为(Transaction Propagation Behavior),指的是当一个事务方法调用另一个方法时,事务在两者之间的传递方式。

具体而言,假设存在事务方法A调用方法B的场景,方法B的事务执行方式,取决于方法B自身设定的事务传播行为。

在Spring框架中,TransactionDefinition接口定义了7种不同类型的事务传播行为,它们各自有着独特的作用和应用场景。

二、深入解析七种事务传播行为

深入解析七种事务传播行为

Spring为开发者提供了7种不同的事务传播行为选项,每种行为都有其特定的用途和运行机制:

  1. REQUIRED(默认):如果当前已经存在一个事务,那么后续方法会加入到这个已有的事务当中;若当前不存在事务,系统则会自动创建一个全新的事务来执行方法。
  2. SUPPORTS:当检测到当前有事务存在时,方法会参与到该事务中;若当前没有事务,方法将以非事务的形式运行。
  3. MANDATORY:此传播行为要求当前必须存在事务,方法才会加入执行;若当前不存在事务,系统将抛出异常,以确保方法在事务环境下执行。
  4. REQUIRES_NEW:无论当前是否存在事务,方法都会创建一个全新的事务来执行。如果当前已经存在事务,系统会暂时将其挂起,待新事务完成后再恢复。
  5. NOT_SUPPORTED:方法始终以非事务方式执行。若当前存在事务,该事务会被挂起,直至方法执行完毕。
  6. NEVER:方法禁止在事务环境下执行。一旦检测到当前存在事务,系统会立即抛出异常,以此保证方法以非事务方式运行。
  7. NESTED:如果当前存在事务,方法会在嵌套事务中执行。当当前没有事务时,则按照REQUIRED属性的规则创建新事务执行。嵌套事务是一种特殊结构,它能够独立进行回滚操作,但提交时需依赖外部事务的整体状态 。

三、实现原理

事务传播行为的实现依托于Spring事务管理器的接口抽象实现——AbstractPlatformTransactionManager中的getTransaction方法。

当调用带有@Transactional注解的方法时,Spring会借助AOP(面向切面编程)技术拦截该方法调用,并对方法上的事务传播行为注解进行检查。具体实现步骤如下:

  1. 获取事务:在getTransaction方法里,首先会调用doGetTransaction方法来获取当前的事务信息。
  2. 判断事务状态:接着,通过isExistingTransaction方法判断当前是否存在活动事务。
  3. 事务处理决策:若存在事务,会依据具体的事务传播行为决定后续处理方式。

比如,若传播行为为REQUIRED,方法会加入当前已有的事务;若为REQUIRES_NEW,则会创建一个新事务,并将当前事务挂起。

四、事务传播机制使用与演示

接下来我们演示一下事务传播机制的使用,以下面 3 个最典型的事务传播级别为例:

  • 支持当前事务的 REQUIRED;
  • 不支持当前事务的 REQUIRES_NEW;
  • 嵌套事务 NESTED。

下来我们分别来看。

事务传播机制的示例,需要用到以下两张表:

-- 用户表
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
  `password` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
  `createtime` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC;

-- 日志表
CREATE TABLE `log` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `content` text NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

创建一个 Spring Boot 项目,核心业务代码有 3 个:UserController、UserServcie 以及 LogService。在 UserController 里面调用 UserService 添加用户,并调用 LogService 添加日志。

REQUIRED 使用演示

REQUIRED 支持当前事务。 UserController 实现代码如下,其中 save 方法开启了事务:

@RestController
public class UserController {
    @Resource
    private UserService userService;
    @Resource
    private LogService logService;

    @RequestMapping("/save")
    @Transactional
    public Object save(User user) {
        // 插入用户操作
        userService.save(user);
        // 插入日志
        logService.saveLog("用户插入:" + user.getName());
        return true;
    }
}

UserService 实现代码如下:

@Service
public class UserService {
    @Resource
    private UserMapper userMapper;

    @Transactional(propagation = Propagation.REQUIRED)
    public int save(User user) {
        return userMapper.save(user);
    }
}

LogService 实现代码如下:

@Service
public class LogService {
    @Resource
    private LogMapper logMapper;

    @Transactional(propagation = Propagation.REQUIRED)
    public int saveLog(String content) {
        // 出现异常
        int i = 10 / 0;
        return logMapper.saveLog(content);
    }
}

执行结果:程序报错,两张表中都没有插入任何数据。

执行流程描述:

  1. 首先 UserService 中的添加用户方法正常执行完成。
  2. LogService 保存日志程序报错,因为使用的是 UserController 中的全局事务,所以整个事务回滚,步骤 1 中的操作也跟着回滚。
  3. 所以数据库中没有添加任何数据。

REQUIRED_NEW 使用演示

REQUIRED_NEW 不支持当前事务。 UserController 实现代码:

@RequestMapping("/save")
@Transactional
public Object save(User user) {
    // 插入用户操作
    userService.save(user);
    // 插入日志
    logService.saveLog("用户插入:" + user.getName());
    return true;
}

UserService 实现代码:

@Service
public class UserService {
    @Resource
    private UserMapper userMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int save(User user) {
        System.out.println("执行 save 方法.");
        return userMapper.save(user);
    }
}

LogService 实现代码:

@Service
public class LogService {
    @Resource
    private LogMapper logMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int saveLog(String content) {
        // 出现异常
        int i = 10 / 0;
        return logMapper.saveLog(content);
    }
}

程序执行结果:

User 表中成功添加了一条用户数据,Log 表执行失败,没有加入任何数据,但它并没有影响到 UserController 中的事务执行。

通过以上结果可以看出:LogService 中使用的是单独的事务,虽然 LogService 中的事务执行失败了,但并没有影响 UserController 和 UserService 中的事务。

NESTED 使用演示

NESTED 是嵌套事务。 UserController 实现代码如下:

@RequestMapping("/save")
@Transactional
public Object save(User user) {
    // 插入用户操作
    userService.save(user);
    return true;
}

UserService 实现代码如下:

@Transactional(propagation = Propagation.NESTED)
public int save(User user) {
    int result = userMapper.save(user);
    System.out.println("执行 save 方法.");
    // 插入日志
    logService.saveLog("用户插入:" + user.getName());
    return result;
}

LogService 实现代码如下:

@Transactional(propagation = Propagation.NESTED)
public int saveLog(String content) {
    // 出现异常
    int i = 10 / 0;
    return logMapper.saveLog(content);
}

最终执行结果,用户表和日志表都没有添加任何数据。

执行流程描述:

  1. UserController 中调用了 UserService 的添加用户方法,UserService 使用 NESTED 循环嵌套事务,并成功执行了添加用户的方法。
  2. UserService 中调用了 LogService 的添加方法,LogService 使用了 NESTED 循环嵌套事务,但在方法执行中出现的异常,因此回滚了当前事务。
  3. 因为 UserService 使用的是嵌套事务,所以发生回滚的事务是全局的,也就是说 UserService 中的添加用户方法也被回滚了,最终执行结果是用户表和日志表都没有添加任何数据。

五、深度解读

事务传播行为是Spring框架的一项极为强大的机制,它赋予开发者在嵌套调用事务方法时,灵活掌控事务运行逻辑的能力。

借助合理选用不同的事务传播行为,不仅能够确保数据的完整性与一致性,还能显著提升应用程序的可靠性和健壮性。

在实际开发场景中,事务传播行为的选择并非一概而论,而是紧密依赖于具体的业务需求。

比如,当需要在一个事务中执行多个彼此独立的操作,且这些操作能够独立回滚时,NESTED传播行为便是理想之选;若要在事务外部执行一些非事务性操作,NOT_SUPPORTED传播行为则能很好地满足这一需求。

总的来说,Spring的事务传播行为为实现复杂的事务管理提供了灵活且强大的支持。通过深入理解这些机制,并将其合理运用到实际开发中,开发者能够打造出更为健壮、可靠的企业级应用。

文章来源: https://study.disign.me/article/202509/5.java-spring-transaction.md

发布时间: 2025-02-25

作者: 技术书栈编辑