2024-12-18  阅读(72)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/mianshi/baodian/detail/6965795710

什么是 Spring 事务传播机制?

Spring 事务传播机制是指,包含多个事务的方法在相互调用时,事务是如何在这些方法间传播的。

例如:methodA事务方法调用methodB事务方法时,methodB是继承methodA的事务运行呢,还是为自己开启一个新事务运行,这就是由methodB 的事务传播行为决定的。

Spring 的事务传播机制

Spring 事务传播机制使用 @Transactional(propagation=Propagation.REQUIRED) 来定义,Spring 一共定义了 7 中传播行为:

  1. REQUIRED:默认的事务传播级别。如果当前没有事务,Spring 会创建一个新的事务;如果已经存在事务,则加入该事务。这意味着被调用的方法会在调用者的事务上下文中运行。
  2. SUPPORTS:如果当前存在事务,方法会在这个事务中运行;如果没有事务,则方法在非事务的环境中执行。
  3. MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,它会抛出异常。这种传播行为要求调用方法必须在一个现有的事务中运行。
  4. REQUIRES_NEW:总会创建一个新的事务,如果当前存在事务,则将当前事务挂起。也就是说不管外部方法是否开启事务,REQUIRES_NEW 修饰的方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。
  5. NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  6. NEVER:以非事务方式运行,如果当前存在事务,它会抛出异常。这种传播行为确保方法永远不在事务环境中运行。
  7. NESTED:如果当前存在事务,则在嵌套事务内运行。如果当前事务回滚,嵌套事务也会回滚,但嵌套事务的回滚不会导致当前事务回滚。如果当前没有事务,则其行为像REQUIRED。

七种传播行为,可以分为三大类,如下图:

现在我们来详细看看这七种传播行为。为了下面的演示,我们需要新建两张表 :

create table user(
    `id` int(11) NOT NULL AUTO_INCREMENT comment 'id',
     name  varchar(100) not null comment '用户名',
    PRIMARY KEY (id)
)ENGINE=InnoDB AUTO_INCREMENT=10000 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC comment '用户信息表';

create table blog(
    `id` int(11) NOT NULL AUTO_INCREMENT comment 'id',
     title  varchar(100) not null comment '标题',
    PRIMARY KEY (id)
)ENGINE=InnoDB AUTO_INCREMENT=10000 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC comment '博客信息表';

表够简单吧。^_^....

利用如下代码来验证:

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private BlogService blogService;
  
    // 这里添加不同的事务传播行为 
    @Transactional(propagation = Propagation.XXXX)
    public void addUser() {
        User user = new User();
        user.setName("大明哥");
        userMapper.insertSelective(user);

        blogService.addBlog();
    }
}

@Service
public class BlogService {

    @Autowired
    private BlogMapper blogMapper;

     // 这里添加不同的事务传播行为 
    @Transactional(propagation = Propagation.XXXX)
    public void addBlog() {
        Blog blog = new Blog();
        blog.setTitle("Java 面试 600 讲");
        blogMapper.insertSelective(blog);
    }
}

由于需要观察 Spring 管理事务的日志,需要将 Spring 日志相关的级别调整为 DEBUG,如下:

<Logger name="org.springframework.transaction.interceptor" level="DEBUG"/>

大明哥分析前面 5 种,NEVERNESTED 就不分析了,使用场景没有相对会少些。

REQUIRED

如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。

例如 :

    @Transactional(propagation = Propagation.REQUIRED)
    public void methodA() {
        // 调用 B
        methodB();
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void methodB() {

    }

如果我们单独执行 methodB(),由于当前上下文不存在事务,则会开启一个新的事务。如果我们调用 methodA(),由于当前上下文不存在事务,所以会开启一个新的事物,再调用 methodB()methodB() 发现当前上下文已经存在了事务,则会加入到 methodA() 的事务中。

我们将上面的示例代码的传播行为调整为 REQUIRED,执行代码,打印如下日志:

  • 1、表示创建了一个事务,传播行为为 PROPAGATION_REQUIRED,事务隔离级别为 ISOLATION_DEFAULTaddUser() 开启事务。
  • 2、表示加入已经存在的事务。addUser() 创建了事务,然后调用 addBlog()addBlog() 发现当前已经有事务了 ,直接加入即可
  • 3、提交事务

如果在方法执行过程中发生了异常,并且这个异常被标记为触发回滚,整个事务都会回滚。比如我们在 addBlog() 方法中加入 int i = 1 / 10;,如下:

    @Transactional(propagation = Propagation.REQUIRED)
    public void addBlog() {
        Blog blog = new Blog();
        blog.setTitle("Java 面试 600 讲");
        int i = 1 / 0;
        blogMapper.insertSelective(blog);
    }

执行,日志如下:

addUser() 在正常提交,但是 addBlog() 抛出 ArithmeticException 异常,导致整个事务回滚。

我们知道异常分为运行时异常(RuntimeException)、检查型异常(Exception)和错误(Error),如果我们不使用 rollbackFor 或者 noRollbackFor 来定义事务回滚行为,那么 Spring 事务只会在遇到运行时异常(RuntimeException)和错误(Error)时触发回滚。我们将 addBlog() 调整为这样:

    @Transactional(propagation = Propagation.REQUIRED)
    public void addBlog() throws Exception {
        Blog blog = new Blog();
        blog.setTitle("Java 面试 600 讲");
        blogMapper.insertSelective(blog);
        throw new Exception("抛个异常玩玩..");
    }

执行,日志如下:

抛出 Exception ,事务依然正常提交。

SUPPORTS

如果当前存在事务,方法会在这个事务中运行;如果没有事务,则方法在非事务的环境中执行。

例如:

    @Transactional(propagation = Propagation.REQUIRED)
    public void methodA() {
        // 调用 B
        methodB();
    }

    @Transactional(propagation = Propagation.SUPPORTS)
    public void methodB() {

    }

如果我们单纯调用 methodB() ,则它会以非事务的方式运行。如果我们调用 methodA(),由于 methodA() 有事务,所以 methodB() 会加入到 methodA() 的使用中运行。

下面我们实战演示下。将 addBlog() 的事务传播行为调整为 SUPPORTS

    @Transactional(propagation = Propagation.SUPPORTS)
    public void addBlog() throws Exception {
      //...
    }

我们单独调用下 addBlog(),日志如下:

没有任务事务相关的日志。

我们在通过 addUser() 来调用 addBlog(),日志如下:

对于事务回滚,如果没有事务的话,及时出现了异常也不会回滚。但是如果有事务的话,则回滚由异常类型和 rollbackFor 配置相关。

MANDATORY

如果当前存在事务,则加入该事务;如果当前没有事务,它会抛出异常。他要求调用方必须在一个现有的事务中运行。

例如:

    @Transactional(propagation = Propagation.REQUIRED)
    public void methodA() {
        // 调用 B
        methodB();
    }

    @Transactional(propagation = Propagation.MANDATORY)
    public void methodB() {

    }

如果我们单独调用 methodB() ,则会抛出 IllegalTransactionStateException 异常。如果通过 methodA() 调用 methodB(),由于 methodA() 存在事务中,所以 methodB() 会加入当前事务中。

我们来实例演示下,将 addBlog() 的事务传播行为调整为 MANDATORY

    @Transactional(propagation = Propagation.MANDATORY)
    public void addBlog() throws Exception {
      //...
    }

先单独调用 addBlog(),运行日志如下:

调用方法没有事务,则抛出 IllegalTransactionStateException 异常。

我们在通过 addUser() 来调用,运行日志如下:

addUser() 有事务,addBlog() 加入到 addUser() 中的事务来。

REQUIRES_NEW

总会创建一个新的事务,如果当前存在事务,则将当前事务挂起。也就是说不管外部方法是否开启事务,REQUIRES_NEW 修饰的方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。

例如:

    @Transactional(propagation = Propagation.REQUIRED)
    public void methodA() {
        // 调用 B
        methodB();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {

    }

无论你怎么调用 methodB(),它都会新开一个自己的事务。

下面代码演示下。我们将 addBlog() 的事务传播行为调整为 REQUIRES_NEW

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addBlog() throws Exception {
       //  ...
    }

单独调用 addBlog(),日志如下:

单独调用它会创建一个新的事务,传播行为为 REQUIRES_NEW

如果通过 addUser() 去调用呢?日志如下:

  • 1、addUser() 事务传播行为为 REQUIRED,调用它时由于当前没有事务,所以它会另开一个事务。
  • 2、addUser() 调用 addBlog(),尽管 addBlog() 当前处于事务当中,但是由于它的的事务传播行为是 REQUIRES_NEW ,所以它会重新开启一个新的事务
    • Suspending current transaction:表示挂起当前事务 ,即将 addUser() 的事务暂停。
    • creating new transaction with name:表示创建一个新的事务。
  • 3、addBlog() 提交事务
  • 4、addUser() 提交事务
    • Resuming suspended transaction after completion of inner transaction:恢复暂停的事务。

上面提到 REQUIRES_NEW 与当前事务是相互独立,互不干扰的。我们让 addBlog() 抛个异常看看 :

你会发现两个事务都回滚了,什么原因?因为 addBlog() 抛了异常后,addUser() 对这个异常没有hold 住,也在抛异常,事务肯定会回滚。那如果 addUser() 抛出异常呢?日志如下:

addBlog() 依然提交了事务,但是 addUser() 回滚了事务。

NOT_SUPPORTED

以非事务方式运行,如果当前存在事务,则把当前事务挂起。

直接演示吧。将 addBlog() 事务传播行为调整为 NOT_SUPPORTED,单独调用 addBlog() ,日志如下:

单独调用,没有事务。如果通过 addUser() 调用呢 ?


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文