@Transactional生效和失效测试及传播机制

12 篇文章 0 订阅

spring事务(注解 @Transactional )失效的12种场景
一.概念
@Transactional是springboot框架中的声明式事务管理注解,是现在编程中普遍使用的事务管理注解。
通过该标注spring可以知道在什么地方启用数据库事务功能。

但是关于这个注解,如果不明白其原理,是有很多坑的,会导致使用时失效。
很多文章也只是千篇一律把其概念列了一下,并没有告诉我们究竟怎么使用是对的,怎么使用是错的。
所以我先把 @Transactional使用的注意点和规范列出来,大家先明白其用法,再去追究原理。

二.用法
@Transactional注解可以标注在类或者方法上:

  • 如果是标注在方法上,该方法必须是public才会生效。如果标注的方法是private则不会生效,但编译时也不会报错,所以这点需要我们自己去规避;

  • 如果标注在类上,则代表该类的所有公共非静态的方法都将使用事务功能。

根据@Transactional标注的层级讲解:

  • 直接标注在Controller类的方法上,则该方法内所有的逻辑都开启事务。(不推荐)
    实际上在一个Controller方法里面,会包含很多非数据库操作的逻辑,比如校验逻辑,发第三方消息等。我们要尽量避免长事物,提升性能,所以一般很少会直接在Controller方法上标注@Transactional。

  • 一般推荐标注在Service层级

1.private和public本类测试1

package com.kaka.mysql.controller;

import com.kaka.mysql.dao.entity.Test;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Date;

@Slf4j
@RestController
@AllArgsConstructor
@RequestMapping("/tran")
public class TransactionalController {



    @GetMapping("/test")
    private boolean A() {
        Test test = new Test();
        test.setTestDate(LocalDate.now());
        test.setTestTime(LocalTime.now());
        test.setTestTimestap(new Date());
        test.setName("a");
        test.setMoney(BigDecimal.valueOf(10.999));
        test.setTotalMoney(test.getMoney().add(BigDecimal.valueOf(3)));
        boolean insert = test.insert();
        /**
         * B 插入字段为 3的数据
         */
        boolean insertb=insertB();

        Test test2 = new Test();
        test2.setTestDate(LocalDate.now());
        test2.setTestTime(LocalTime.now());
        test2.setTestTimestap(new Date());
        test2.setName("aa");
        test2.setMoney(BigDecimal.valueOf(10.999));
        test2.setTotalMoney(test2.getMoney().add(BigDecimal.valueOf(3)));
        boolean insert2 = test2.insert();


        return insert&&insertb;
    }

    @Transactional
    public boolean insertB() {
        Test test = new Test();
        test.setTestDate(LocalDate.now());
        test.setTestTime(LocalTime.now());
        test.setTestTimestap(new Date());
        test.setName("b");
        test.setMoney(BigDecimal.valueOf(10.999));
        test.setTotalMoney(test.getMoney().add(BigDecimal.valueOf(3)));
        boolean insert = test.insert();
        this.insertC();
        int bbb = 1/0;
        Test test2 = new Test();
        test2.setTestDate(LocalDate.now());
        test2.setTestTime(LocalTime.now());
        test2.setTestTimestap(new Date());
        test2.setName("bb");
        test2.setMoney(BigDecimal.valueOf(10.999));
        test2.setTotalMoney(test2.getMoney().add(BigDecimal.valueOf(3)));
        boolean insert1 = test2.insert();
        return insert&&insert1;
    }

    @Transactional
    public boolean insertC() {
        Test test = new Test();
        test.setTestDate(LocalDate.now());
        test.setTestTime(LocalTime.now());
        test.setTestTimestap(new Date());
        test.setName("c");
        test.setMoney(BigDecimal.valueOf(10.999));
        test.setTotalMoney(test.getMoney().add(BigDecimal.valueOf(3)));
        boolean insert = test.insert();
        int bbb = 1/0;
        Test test2 = new Test();
        test2.setTestDate(LocalDate.now());
        test2.setTestTime(LocalTime.now());
        test2.setTestTimestap(new Date());
        test2.setName("cc");
        test2.setMoney(BigDecimal.valueOf(10.999));
        test2.setTotalMoney(test2.getMoney().add(BigDecimal.valueOf(3)));
        boolean insert1 = test2.insert();
        return insert&&insert1;
    }
}

测试结果:
1.当A为private修饰时,B和C的事务生效,当A为public时,B和C事务失效
2.加事务注解的方法A必须为public否则事务不生效,A内调用的方法,不管是private还是public,只要A加了就都生效
2.private和public本类测试2
测试代码:

package com.kaka.mysql.controller;

import com.kaka.mysql.dao.entity.Test;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Date;

@Slf4j
@RestController
@AllArgsConstructor
@RequestMapping("/tran")
public class TransactionalController {



    @GetMapping("/test")
    private boolean A() {
        Test test = new Test();
        test.setTestDate(LocalDate.now());
        test.setTestTime(LocalTime.now());
        test.setTestTimestap(new Date());
        test.setName("a");
        test.setMoney(BigDecimal.valueOf(10.999));
        test.setTotalMoney(test.getMoney().add(BigDecimal.valueOf(3)));
        boolean insert = test.insert();
        /**
         * B 插入字段为 3的数据
         */
        boolean insertb=insertB();

        Test test2 = new Test();
        test2.setTestDate(LocalDate.now());
        test2.setTestTime(LocalTime.now());
        test2.setTestTimestap(new Date());
        test2.setName("aa");
        test2.setMoney(BigDecimal.valueOf(10.999));
        test2.setTotalMoney(test2.getMoney().add(BigDecimal.valueOf(3)));
        boolean insert2 = test2.insert();


        return insert&&insertb;
    }

    @Transactional
    public boolean insertB() {
        Test test = new Test();
        test.setTestDate(LocalDate.now());
        test.setTestTime(LocalTime.now());
        test.setTestTimestap(new Date());
        test.setName("b");
        test.setMoney(BigDecimal.valueOf(10.999));
        test.setTotalMoney(test.getMoney().add(BigDecimal.valueOf(3)));
        boolean insert = test.insert();
        this.insertC();
        int bbb = 1/0;
        Test test2 = new Test();
        test2.setTestDate(LocalDate.now());
        test2.setTestTime(LocalTime.now());
        test2.setTestTimestap(new Date());
        test2.setName("bb");
        test2.setMoney(BigDecimal.valueOf(10.999));
        test2.setTotalMoney(test2.getMoney().add(BigDecimal.valueOf(3)));
        boolean insert1 = test2.insert();
        return insert&&insert1;
    }


    private boolean insertC() {
        Test test = new Test();
        test.setTestDate(LocalDate.now());
        test.setTestTime(LocalTime.now());
        test.setTestTimestap(new Date());
        test.setName("c");
        test.setMoney(BigDecimal.valueOf(10.999));
        test.setTotalMoney(test.getMoney().add(BigDecimal.valueOf(3)));
        boolean insert = test.insert();
//        int bbb = 1/0;
        Test test2 = new Test();
        test2.setTestDate(LocalDate.now());
        test2.setTestTime(LocalTime.now());
        test2.setTestTimestap(new Date());
        test2.setName("cc");
        test2.setMoney(BigDecimal.valueOf(10.999));
        test2.setTotalMoney(test2.getMoney().add(BigDecimal.valueOf(3)));
        boolean insert1 = test2.insert();
        return insert&&insert1;
    }
}

测试结果:
当A被private修饰时,B和C方法事务生效,当A被public修饰时,B和C事务都不生效
3.private和public不同类测试
TransactionalController

package com.kaka.mysql.controller;

import com.kaka.mysql.dao.entity.Test;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Date;

@Slf4j
@RestController
@AllArgsConstructor
@RequestMapping("/tran")
public class TransactionalController {

    private final Transactional2Controller transactional2Controller;

    @GetMapping("/test")
    public boolean A() {
        Test test = new Test();
        test.setTestDate(LocalDate.now());
        test.setTestTime(LocalTime.now());
        test.setTestTimestap(new Date());
        test.setName("a");
        test.setMoney(BigDecimal.valueOf(10.999));
        test.setTotalMoney(test.getMoney().add(BigDecimal.valueOf(3)));
        boolean insert = test.insert();
//        boolean insertb=insertB();
        transactional2Controller.insertB();

        Test test2 = new Test();
        test2.setTestDate(LocalDate.now());
        test2.setTestTime(LocalTime.now());
        test2.setTestTimestap(new Date());
        test2.setName("aa");
        test2.setMoney(BigDecimal.valueOf(10.999));
        test2.setTotalMoney(test2.getMoney().add(BigDecimal.valueOf(3)));
        boolean insert2 = test2.insert();


        return insert;
    }
}

Transactional2Controller

package com.kaka.mysql.controller;

import com.kaka.mysql.dao.entity.Test;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Date;

@Component
public class Transactional2Controller {


    @Transactional
    public boolean insertB() {
        Test test = new Test();
        test.setTestDate(LocalDate.now());
        test.setTestTime(LocalTime.now());
        test.setTestTimestap(new Date());
        test.setName("b");
        test.setMoney(BigDecimal.valueOf(10.999));
        test.setTotalMoney(test.getMoney().add(BigDecimal.valueOf(3)));
        boolean insert = test.insert();
        this.insertC();
        int bbb = 1/0;
        Test test2 = new Test();
        test2.setTestDate(LocalDate.now());
        test2.setTestTime(LocalTime.now());
        test2.setTestTimestap(new Date());
        test2.setName("bb");
        test2.setMoney(BigDecimal.valueOf(10.999));
        test2.setTotalMoney(test2.getMoney().add(BigDecimal.valueOf(3)));
        boolean insert1 = test2.insert();
        return insert&&insert1;
    }


    private boolean insertC() {
        Test test = new Test();
        test.setTestDate(LocalDate.now());
        test.setTestTime(LocalTime.now());
        test.setTestTimestap(new Date());
        test.setName("c");
        test.setMoney(BigDecimal.valueOf(10.999));
        test.setTotalMoney(test.getMoney().add(BigDecimal.valueOf(3)));
        boolean insert = test.insert();
//        int bbb = 1/0;
        Test test2 = new Test();
        test2.setTestDate(LocalDate.now());
        test2.setTestTime(LocalTime.now());
        test2.setTestTimestap(new Date());
        test2.setName("cc");
        test2.setMoney(BigDecimal.valueOf(10.999));
        test2.setTotalMoney(test2.getMoney().add(BigDecimal.valueOf(3)));
        boolean insert1 = test2.insert();
        return insert&&insert1;
    }
}

测试结果:
无论A被private还是被public修饰,B的事务都会生效
参考博客:
@Transactional生效场景和自调用失效场景
Spring 的事务实现原理和传播机制
spring 事务传播机制
spring 事务传播机制
spring 事务 传播机制 描述的 事务方法直接相互调用,父子事物开启,挂起,回滚 等的处理方式。 绿色的 那几个 我认为比较重要。

1 , @Transactional(propagation=Propagation.REQUIRED) 默认值 等于 @Transactional

有父方法传递过来的 事务环境,就在使用父方法的事务环境,如果父方法没有事务环境,那么就用新开启一个事物。

备注:非事务执行的意思是单条sql就提交。

例子设置: A 方法调用 B,C两个子方法。C执行后在C的方法中抛出异常

情景1 条件:A 什么都不加,B 什么都不加,C,Propagation.REQUIRED ,

结果:C 执行以后抛出异常。结果 B能写入 ,C写入的被回滚。

解析:A,B都是非事物环境,C起了一个新的事务。

情景2 条件:A 加 @Transactional ,B 什么都不加,C,Propagation.REQUIRED ,

结论:B C 的操作都回滚了

解析:B,C都是用的 A开启的事务环境,C的时候抛出异常, 全部回滚。

关键字:有就用,没有就新建一个

2 ,@Transactional( propagation=Propagation.SUPPORTS)

如果父方法没有事物换机,那么当前方法就使用非事务环境执行,如果 有,就使用父方法的事务环境。 这个和只写一个@Transactional 几乎一样

例子设置: A 方法调用 B,C两个子方法。C执行后在C的方法中抛出异常

情景1 条件:A 什么都不加,B 什么都不加,C,Propagation.SUPPORTS,

结果:B,C 都能写入成功

解析:ABC,都是非事务环境

情景2 条件:A @Transactional ,B 什么都不加,C,Propagation.SUPPORTS,

结果:B,C 都能写入后都回滚了,

解析:ABC,都处于A开启的事务环境。

关键字:有就用,没有就不用

3 @Transactional(propagation=Propagation.MANDATORY)

当前方法必须处于事物环境才能执行。

例子设置: A 方法调用 B,C两个子方法。

情景1 条件:A 什么都不加,B 什么都不加,C,Propagation.MANDATORY,

结果:执行C的时候抛出异常( No existing transaction found for transaction marked with propagation ‘mandatory’ )

解析:C 需要事物环境才能执行

情景2 条件:A @Transactional ,B 什么都不加,C,Propagation.MANDATORY,

结果: 正常执行到

解析:C 需要事物环境才能执行

关键字:必须要父类给我一个事务环境

4 @Transactional(propagation=Propagation.REQUIRES_NEW)

必定要新开启一个事务。

例子设置: A 方法调用 B,C两个子方法。C执行后在C的方法中抛出异常

情景1 条件:A 什么都不加,B 什么都不加,C,Propagation.REQUIRES_NEW,

结果:B 能写入,C 写入后被回滚了。

解析:在父类没有事务环境的情况下,C启动了一个新的事务

情景2 条件:A @Transactional ,B 什么都不加,C,Propagation.REQUIRES_NEW,

结果: B,C都没有写入数据

解析: 明显C 让父类事务一起回滚了。C如果 回滚,父类事物也会回滚。

情景3 条件:A @Transactional ,B 什么都不加,C,Propagation.REQUIRES_NEW, 去掉 C执行后在C的方法中抛出异常,在A中 执行完C后抛出异常。

结果: B的数据没写进去,C的数据写进去;额

解析: B 使用的A开启的事物 ,C 开启了一个新事物。并且C执行完成以后就提交了,A 里面的异常让B一起回滚了。

关键字:新建一个,父回滚子不回滚,子回滚,父也会滚。 并且子事物是在 子方法完成的时候提交的。

5 @Transactional(propagation=Propagation.NOT_SUPPORTED)

当前方法一定不回用在事务环境执行,如果父类有事务,那么就先把它挂起。 和 REQUIRED 是相反的。

例子设置: A 方法调用 B,C两个子方法。C执行后在C的方法中抛出异常

情景1 条件:A @Transactional,B 什么都不加,C,Propagation.NOT_SUPPORTED,

结果:B没有写入,C能写入。

解析:B 写入了,然后被 C里面的 抛出的异常 给触发回退了,C 里面是非事执行,能够正常写入。

关键字:不会在事务环境执行

6 @Transactional(propagation=Propagation.NEVER)

和 MANDATORY 相反 ,这个方法只能执行在非事务环境,如果父类有事物,就抛出异常

例子设置: A 方法调用 B,C两个子方法

情景1 条件:@Transactional ,B 什么都不加,C,Propagation.NEVER,

结果:抛出异常(Existing transaction found for transaction marked with propagation ‘never’)

解析:NEVER 的父方法不能有 事物环境。

关键字:父层不能有事务

7 @Transactional(propagation=Propagation.NESTED)

内嵌事物,在没有外部事务 的时候 和 REQUIRED 一样, 这个和 Propagation.REQUIRES_NEW 容易混淆 ,Propagation.REQUIRES_NEW 是挂起父事物,然后新启动一个事物,新的事务完成后先提交,新事物回滚,父事物一起回滚,

Propagation.NESTED 是在 父环境有事物的时候,会做一个保存点,然后在 如果 当前方法抛出异常,就回滚到保存点,如果 父事物 回滚,当前方法里面的写操作也回滚,而且本质上只有一个事物,所以在父方法事物提交的时候提交。

例子设置: A 方法调用 B,C两个子方法,C方法内部执行完成以后抛出异常

情景1 条件:@Transactional ,B 什么都不加,C,Propagation.NEVER,

结果: B 没有写入成功,C也没有写入成功。

解析:.这个结果和 REQUIRES_NEW 类似,不做解释,后面进一步确认。

情景2 条件:@Transactional ,B 什么都不加,C,Propagation.NEVER, ,但是抛异常的位置改成A方法内部,C方法调用以后。

结果: B 没有写入成功,C也没有写入成功。

解析:综合和上面2个例子。NESTED 内层事物回滚,外层也会滚,外层回滚内层也会回滚。并且内层事物提交的 时间节点是和外层事务 一起提交。

关键字:父子同步回滚,父子一起提交

备注:sharding JDBC 不支持 @Transactional(propagation=Propagation.NESTED)

备注2:如果方法前面不写 @Transactional ,如果这个方法被有事务环境的 方法调用。 那么这个方法会使用 父类的 事务环境。如果父类没有就每条单独提交。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值