Spring 事务管理简介(一)


在这里插入图片描述

概述

事务原本是数据库中的概念,用于数据访问层。但一般情况下,需要将事务提升到业务层,即 Service 层。这样做是为了能够使用事务的特性来管理具体的业务。

在 Spring 中通常可以通过以下三种方式来实现对事务的管理:

  • 使用 Spring 的事务代理工厂管理事务(已过时)
  • 使用 Spring 的事务注解管理事务
  • 使用 AspectJ 的 AOP 配置管理事务

Spring 事务管理 API

Spring 的事务管理,主要用到两个事务相关的接口。

事务管理器接口

事务管理器是 PlatformTransactionManager 接口对象。其主要用于完成事务的提交、回滚,及获取事务的状态信息。

该接口定义了 3 个事务方法:

  • void commit(TransactionStatus status):事务的提交
  • TransactionStatus getTransaction(TransactionDefinition definition):获取事务的状态
  • void rollback(TranscationStatus status):事务的回滚

常用的两个实现类

PlatformTransactionManager 接口有两个常用的实现类:

  • DataSourceTransactionManager:使用 JDBC 或 MyBatis 进行持久化数据时使用。
  • HibernateTransactionManager:使用 Hibernate 进行持久化数据时使用。

Spring 的回滚方式

Spring 事务的默认回滚方式是:发生运行时异常回滚 所有不要使用try{}cath(){}捕获 否则事务无效

事务定义接口

事务定义接口 TransactionDefinition 中定义了事务描述相关的三类常量:事务隔离级别事务传播行为事务默认超时时限,及对它们的操作。

事务的四种隔离级别

  • DEFAULT:采用 DB 默认的事务隔离级别。MySql 默认为 REPEATABLE_READ;Oracle 默认为:READ_COMMITTED;
  • READ_UNCOMMITTED:读未提交。未解决任何并发问题。
  • READ_COMMITTED:读已提交。解决脏读,存在不可重复读与幻读。
  • REPEATABLE_READ:可重复读。解决脏读、不可重复读。存在幻读。
  • SERIALIZABLE:串行化。不存在并发问题。

事务的七种传播行为

所谓事务传播行为是指,处于不同事务中的方法在相互调用时,执行期间事务的维护情况。如,A 事务中的方法 a() 调用 B 事务中的方法 b(),在调用执行期间事务的维护情况,就称为事务传播行为。事务传播行为是加在方法上的。

  • REQUIRED:指定的方法必须在事务内执行。若当前存在事务,就加入到当前事务中;若当前没有事务,则创建一个新事务。这种传播行为是最常见的选择,也是 Spring 默认的事务传播行为。
  • SUPPORTS:指定的方法支持当前事务,但若当前没有事务,也可以以非事务方式执行。
  • MANDATORY:指定的方法必须在当前事务内执行,若当前没有事务,则直接抛出异常。
  • REQUIRES_NEW:总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事务执行完毕。
  • NOT_SUPPORTED:指定的方法不能在事务环境中执行,若当前存在事务,就将当前事务挂起。
  • NEVER:指定的方法不能在事务环境下执行,若当前存在事务,就直接抛出异常。
  • NESTED:指定的方法必须在事务内执行。若当前存在事务,则在嵌套事务内执行;若当前没有事务,则创建一个新事务。Propagation.NESTED 修饰的内部方法属于外部事务的子事务,外围主事务回滚,子事务一定回滚,而内部子事务可以单独回滚而不影响外围主事务和其他子事务。

注意:这7种传播行为有个前提,他们的事务管理器是同一个的时候,才会有上面描述中的表现行为。

REQUIRED,REQUIRES_NEW,NESTED这几个算是比较特殊的,比较常用的,对比

1、REQUIRED和NESTED修饰的内部方法都属于外围方法事务,如果外围方法抛出异常,这两种方法的事务都会被回滚。但是REQUIRED是加入外围方法事务,所以和外围事务同属于一个事务,一旦REQUIRED事务抛出异常被回滚,外围方法事务也将被回滚。而NESTED是外围方法的子事务,有单独的保存点,所以NESTED方法抛出异常被回滚,不会影响到外围方法的事务。

2、REQUIRES_NEW和NESTED都可以做到内部方法事务回滚而不影响外围方法事务。但是因为NESTED是嵌套事务,所以外围方法回滚之后,作为外围方法事务的子事务也会被回滚。而REQUIRES_NEW是通过开启新的事务实现的,内部事务和外围事务是两个事务,外围事务回滚不会影响内部事务。

针对事务传播类型,我们要弄明白的是 4 个点:

  • 子事务与父事务的关系,是否会启动一个新的事务?
  • 子事务异常时,父事务是否会回滚?
  • 父事务异常时,子事务是否会回滚?
  • 父事务捕捉异常后,父事务是否还会回滚?

准备

表结构
tb_order
在这里插入图片描述
tb_user
在这里插入图片描述

pom

<dependencies>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-test</artifactId>
      </dependency>

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
      </dependency>

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-jdbc</artifactId>
      </dependency>

      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
      </dependency>

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-aop</artifactId>
      </dependency>

      <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
      </dependency>
  </dependencies>

配置类

@Configuration
public class DataSourceConfig {
    @Bean
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/study_db?characterEncoding=UTF-8&serverTimezone=GMT%2B8");
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

实体类

@Data
@Builder
public class Order {
    private Long id;
    private Long userId;
    private String name;
    private BigDecimal price;
    private Integer num;
}

@Data
@Builder
public class User {
    private Long id;
    private String username;
    private String address;
}

mapper

@Repository
public class OrderMapper {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void saveOrder(Order order) {
        jdbcTemplate.update("insert into tb_order(user_id, name,price,num) VALUES (?,?,?,?)",
                order.getUserId(),
                order.getName(),
                order.getPrice(),
                order.getNum());
    }
}


@Repository
public class UserMapper {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void saveUser(User user) {
        jdbcTemplate.update("insert into tb_user(username, address) VALUES (?,?)", user.getUsername(), user.getAddress());
    }
}

Service

1、UserService

public interface UserService {
    void saveUser();
}

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public void saveUser() {
        userMapper.saveUser(User.builder().username("封于修").address("长沙天心区").build());
        throw new RuntimeException();
    }
}

2、OrderService

public interface OrderService {
    void saveOrder();
}

@Service
public class OrderServiceImpl implements OrderService {


    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private UserService userService;

    @Override
    public void saveOrder() {
       // 保存订单信息
       save();
       // int i = 1/0;
        // 保存用户信息
       userService.saveUser();
    }


   public void save() {
        orderMapper.saveOrder(Order.builder().userId(100L)
                .price(BigDecimal.valueOf(100)).name("Iphone14 pro max").num(2).build());
    }
}

Controller

@RestController
@RequestMapping("/api")
public class TxController {

    @Autowired
    OrderService orderService;

    @GetMapping("/test")
    public String test1() {
        orderService.saveOrder();
        return "success";
    }
}

运行项目。浏览器访问地址:localhost:8080/api/tx,正常的话应该是接口请求成功。
在这里插入图片描述
tb_order、tb_user添加一条数据,测试 Spring 事务传播行为的 Demo 就准备完毕了

快速入门

无事务控制: OrderServiceImpl 中添加除0异常, 访问接口localhost:8080/api/tx,tb_order插入成功,tb_user插入失败。这种在业务中是不允许出现的。

在这里插入图片描述
Spring声明式事务控制: 方法上添加 @Transactional注解控制,tb_order、tb_user都没有插入数据。这说明事务起作用了

在这里插入图片描述

REQUIRED

REQUIRED 是 Spring 默认的事务传播类型,该传播类型的特点是:当前方法存在事务时,子方法加入该事务。此时父子方法共用一个事务,无论父子方法哪个发生异常回滚,整个事务都回滚。即使父方法捕捉了异常,也是会回滚。而当前方法不存在事务时,子方法新建一个事务。为了验证 REQUIRED 事务传播类型的特点,我们来做几个测试。

还是上面 saveOrder和 saveUser的例子。当 saveOrder不开启事务,saveUser开启事务,这时候 saveUser就是独立的事务,而 saveOrder并不在事务之中。因此当 saveUser发生异常回滚时,saveOrder 中的内容就不会被回滚。用如下的代码就可以验证我们所说的。

public void saveOrder() {
   // 保存订单信息
   save();
    // 保存用户信息
   userService.saveUser();
}

@Override
@Transactional
public void saveUser() {
    userMapper.saveUser(User.builder().username("封于修").address("长沙天心区").build());
    throw new RuntimeException();
}

最终的结果是:tb_order插入了数据,tb_user没有插入数据,符合了我们的猜想。

当 saveOrder开启事务,saveUser也开启事务。按照我们的结论,此时 saveUser会加入 saveOrder的事务。此时,我们验证当父子事务分别回滚时,另外一个事务是否会回滚。

我们先验证第一个:当父方法事务回滚时,子方法事务是否会回滚?

@Transactional
public void saveOrder() {
   // 保存订单信息
   save();
    // 保存用户信息
   userService.saveUser();
   throw new RuntimeException();
}


@Transactional
public void saveUser() {
    userMapper.saveUser(User.builder().username("封于修").address("长沙天心区").build());
}

结果是:tb_order和 tb_user都没有插入数据,即:父事务回滚时,子事务也回滚了。

我们继续验证第二个:当子方法事务回滚时,父方法事务是否会回滚?

@Transactional
public void saveOrder() {
   // 保存订单信息
   save();
    // 保存用户信息
   userService.saveUser();
}


@Transactional
public void saveUser() {
    userMapper.saveUser(User.builder().username("封于修").address("长沙天心区").build());
    throw new RuntimeException();
}

结果是:tb_order和 tb_user都没有插入数据,即:子事务回滚时,父事务也回滚了。

我们继续验证第三个:当子方法事务回滚时,父方法捕捉了异常,父方法事务是否会回滚?

@Transactional
public void saveOrder() {
    // 保存订单信息
    save();
    try {
        // 保存用户信息
        userService.saveUser();
    } catch (Exception e) {
        System.out.println("saveUser发生异常exp....");
    }
    System.out.println("执行后续代码.......");
}


@Transactional
public void saveUser() {
    userMapper.saveUser(User.builder().username("封于修").address("长沙天心区").build());
    throw new RuntimeException();
}

结果是:tb_order和 tb_user都没有插入数据,即:子事务回滚时,父事务也回滚了。所以说,这也进一步验证了我们之前所说的:REQUIRED 传播类型,它是父子方法共用同一个事务的。
在这里插入图片描述

原因: 这种错一般发生在嵌套事务中,即内层事务saveUser()出错,但是由于是否提交事务的操作由外层事务saveOrder()触发,于是乎内层事务只能做个标记,来设置当前事务只能回滚。紧接着它想抛出错误,但是由于被 try catch 了,于是乎正常执行后续的逻辑,等执行到最后,外层要提交事务了,发现当前事务已经被打了回滚的标记,所以提交失败,报了上面的错。

REQUIRES_NEW

REQUIRES_NEW 也是常用的一个传播类型,该传播类型的特点是:无论当前方法是否存在事务,子方法都新建一个事务。此时父子方法的事务时独立的,它们都不会相互影响。但父方法需要注意子方法抛出的异常,避免因子方法抛出异常,而导致父方法回滚。 为了验证 REQUIRES_NEW 事务传播类型的特点,我们来做几个测试。

首先,我们来验证一下:当父方法事务发生异常时,子方法事务是否会回滚?

@Transactional
public void saveOrder() {
   // 保存订单信息
   save();
    // 保存用户信息
   userService.saveUser();
   throw new RuntimeException();
}


@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveUser() {
    userMapper.saveUser(User.builder().username("封于修").address("长沙天心区").build());
   
}

结果是:tb_order没有插入数据,tb_user插入了数据,即:父方法事务回滚了,但子方法事务没回滚。这可以证明父子方法的事务是独立的,不相互影响。

下面,我们来看看:当子方法事务发生异常时,父方法事务是否会回滚?

@Transactional
public void saveOrder() {
   // 保存订单信息
   save();
    // 保存用户信息
   userService.saveUser();
   
}


@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveUser() {
   userMapper.saveUser(User.builder().username("封于修").address("长沙天心区").build());
   throw new RuntimeException();
}

结果是:tb_order没有插入了数据,tb_user没有插入数据。

从这个结果来看,貌似是子方法事务回滚,导致父方法事务也回滚了。但我们不是说父子事务都是独立的,不会相互影响么?怎么结果与此相反呢?

其实是因为子方法抛出了异常,而父方法并没有做异常捕捉,此时父方法同时也抛出异常了,于是 Spring 就会将父方法事务也回滚了。如果我们在父方法中捕捉异常,那么父方法的事务就不会回滚了,修改之后的代码如下所示。

@Transactional
public void saveOrder() {
   // 保存订单信息
   save();
  // 捕捉异常
   try {
	    // 保存用户信息
	   userService.saveUser();
    } catch (Exception e) {
        e.printStackTrace();
    }  
}


@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveUser() {
   userMapper.saveUser(User.builder().username("封于修").address("长沙天心区").build());
   throw new RuntimeException();
}

结果是:tb_order插入了数据,tb_user没有插入数据。这正符合我们刚刚所说的:父子事务是独立的,并不会相互影响。

这其实就是我们上面所说的:父方法需要注意子方法抛出的异常,避免因子方法抛出异常,而导致父方法回滚。因为如果执行过程中发生 RuntimeException 异常和 Error 的话,那么 Spring 事务是会自动回滚的。

NESTED

NESTED 也是常用的一个传播类型,该方法的特性与 REQUIRED 非常相似,其特性是:当前方法存在事务时,子方法加入在嵌套事务执行。当父方法事务回滚时,子方法事务也跟着回滚。当子方法事务发送回滚时,父事务是否回滚取决于是否捕捉了异常。如果捕捉了异常,那么就不回滚,否则回滚。

可以看到 NESTED 与 REQUIRED 的区别在于:父方法与子方法对于共用事务的描述是不一样的,REQUIRED 说的是共用同一个事务,而 NESTED 说的是在嵌套事务执行。这一个区别的具体体现是:在子方法事务发生异常回滚时,父方法有着不同的反应动作。

对于 REQUIRED 来说,无论父子方法哪个发生异常,全都会回滚。而 REQUIRED 则是:父方法发生异常回滚时,子方法事务会回滚。而子方法事务发送回滚时,父事务是否回滚取决于是否捕捉了异常。

为了验证 NESTED 事务传播类型的特点,我们来做几个测试。

首先,我们来验证一下:当父方法事务发生异常时,子方法事务是否会回滚?

@Transactional
public void saveOrder() {
   // 保存订单信息
   save();
    // 保存用户信息
   userService.saveUser();
   throw new RuntimeException();
}


@Transactional(propagation = Propagation.NESTED)
public void saveUser() {
    userMapper.saveUser(User.builder().username("封于修").address("长沙天心区").build());
   
}

结果是:tb_order和 tb_user都没有插入数据,即:父子方法事务都回滚了。这说明父方法发送异常时,子方法事务会回滚。

接着,我们继续验证一下:当子方法事务发生异常时,如果父方法没有捕捉异常,父方法事务是否会回滚?

@Transactional
public void saveOrder() {
   // 保存订单信息
   save();
    // 保存用户信息
   userService.saveUser();  
}


@Transactional(propagation = Propagation.NESTED)
public void saveUser() {
    userMapper.saveUser(User.builder().username("封于修").address("长沙天心区").build());
    throw new RuntimeException();
}

结果是:tb_order和 tb_user都没有插入数据,即:父子方法事务都回滚了。这说明子方法发送异常回滚时,如果父方法没有捕捉异常,那么父方法事务也会回滚。

最后,我们验证一下:当子方法事务发生异常时,如果父方法捕捉了异常,父方法事务是否会回滚?

@Transactional
public void saveOrder() {
   // 保存订单信息
   save();
  // 捕捉异常
   try {
	    // 保存用户信息
	   userService.saveUser();
    } catch (Exception e) {
        e.printStackTrace();
    }  
}


@Transactional(propagation = Propagation.NESTED)
public void saveUser() {
   userMapper.saveUser(User.builder().username("封于修").address("长沙天心区").build());
   throw new RuntimeException();
}

结果是:tb_order插入了数据,tb_user没有插入数据,即:父方法事务没有回滚,子方法事务回滚了。这说明子方法发送异常回滚时,如果父方法捕捉了异常,那么父方法事务就不会回滚。

看到这里,相信大家已经对 REQUIRED、REQUIRES_NEW 和 NESTED 这三个传播类型有了深入的理解了。最后,让我们来总结一下:

事务传播类型特性
REQUIRED当前方法存在事务时,子方法加入该事务。此时父子方法共用一个事务,无论父子方法哪个发生异常回滚,整个事务都回滚。即使父方法捕捉了异常,也是会回滚。而当前方法不存在事务时,子方法新建一个事务。
REQUIRES_NEW无论当前方法是否存在事务,子方法都新建一个事务。此时父子方法的事务时独立的,它们都不会相互影响。但父方法需要注意子方法抛出的异常,避免因子方法抛出异常,而导致父方法回滚。
NESTED当前方法存在事务时,子方法加入在嵌套事务执行。当父方法事务回滚时,子方法事务也跟着回滚。当子方法事务发送回滚时,父事务是否回滚取决于是否捕捉了异常。如果捕捉了异常,那么就不回滚,否则回滚。

使用方法论
看到这里,你应该也明白:使用事务,不再是简单地使用 @Transaction 注解就可以,还需要根据业务场景,选择合适的传播类型。那么我们再升华一下使用 Spring 事务的方法论。一般来说,使用 Spring 事务的步骤为:

1、根据业务场景,分析要达成的事务效果,确定使用的事务传播类型。
2、在 Service 层使用 @Transaction 注解,配置对应的 propogation 属性。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李熠漾

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值