事务管理的瑞士军刀:@Transactional 最佳实战

本文详细解释了事务的基础概念、ACID属性和不同隔离级别,以及如何在SpringBoot中使用@Transactional注解进行声明式事务管理。同时,讨论了事务失效的各种情况,包括方法访问限制、异常处理和Spring管理等。
摘要由CSDN通过智能技术生成

0 概述

0.1 什么是事务

  • 在讲解 @Transactional 之前,我们可以先来了解一下,什么是事务
  • 在计算机术语中,事务是指访问并可能更新数据库中各种数据项的一个程序执行单元
  • 通常分为声明式事务和编程式事务
    • 声明式事务:通过 AOP 机制,和业务代码进行解耦,无需手动管理
    • 编程式事务:在代码中进行硬编码,手动管理事务,比较繁琐

0.2 事务的属性(ACID)

  • 原子性(Atomicity):事务是一个不可分割的工作单位,事务中包括的操作要么全部成功,要么全部失败
  • 一致性(Consistency):事务必须使数据库从一个一致性状态变到另一个一致性状态
  • 隔离性(Isolation):多个事务并发执行时,一个事务的操作不应影响其他事务。即同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰
  • 持久性(Durability):一旦事务提交,则其结果永久保存在数据库中。即使系统崩溃,重新启动后数据库还能恢复到事务成功结束时的状态

0.3 事务的隔离级别

  • 读未提交(Read uncommitted):允许读取尚未提交的数据。这可能会导致脏读、不可重复读和幻读
  • 读已提交(Read committed):只允许读取已提交的数据。解决了脏读问题,但可能出现不可重复读和幻读
  • 可重复读(Repeatable read):在同一个事务内,多次读取同一数据返回的结果是一致的。解决了脏读和不可重复读的问题,但可能出现幻读
  • 序列化(Serializable):完全串行化的执行事务,每次只有一个事务执行,解决了脏读、不可重复读和幻读的问题,是最高级别的隔离,但性能开销也最大

0.4 什么是 @Transactional

  • @Transactional 是 Spring 框架提供的一个注解,用于声明式事务管理。开发者能以非侵入式的方式管理事务,无需手动编写事务管理的代码,简化了事务管理的复杂性。当标记了 @Transactional 注解的方法被调用时,Spring 会自动为其开启事务,并在方法执行完毕后根据执行结果(是否抛出异常)来决定是否提交或回滚事务
  • 传播级别:
    • REQUIRED(默认: 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务,适用于大多数业务场景
    • SUPPORTS: 如果当前存在事务,则支持当前事务;如果当前没有事务,则以非事务方式继续运行。通常用于非核心不需要事务的业务逻辑操作
    • MANDATORY: 当前方法必须在一个事务中运行。如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。通常用于那些必须包含事务的业务逻辑,确保方法总是在事务中执行
    • REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。这意味着每次调用此方法时都会启动一个新的事务,而不受外部事务的影响。通常用于需要确保子操作不会干扰父事务的场景中,如发送通知或记录日志等
    • NOT_SUPPORTED: 以非事务方式执行操作,如果当前存在事务,则把当前事务挂起。通常用于一个事务中需要执行一些非事务性的子操作时
    • NEVER: 以非事务方式执行,如果当前存在事务,则抛出异常
    • NESTED: 如果当前存在事务,则执行一个嵌套事务,如果当前没有事务,则执行 REQUIRED 行为。嵌套事务允许在一个事务内部执行另一个事务,内部事务可以独立回滚,而不影响外部事务

1 使用场景

  • 服务层方法:业务逻辑主要在服务层实现,并且业务逻辑中通常涉及多个数据库操作,所以 @Transactional 注解会加在服务层的方法上
  • 需要一致性的操作:任何需要确保数据一致性的操作,如金融交易、订单处理等

2 环境准备

  • 演示 @Transactional 注解的用法,我这里需要准备 MySQL、MyBatisPlus、Web 环境,所需依赖如下
    <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>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.3.1</version>
    </dependency>
    

    注意:我使用的是 SpringBoot3,因此版本必须使用适配 SpringBoot3 的依赖,特别是 MybatisPlus,如果使用了低版本的依赖,那么会报错:Property ‘sqlSessionFactory‘ or ‘sqlSessionTemplate‘ are required

  • application.yaml 文件配置如下:
    server:
      port: 8888
    spring:
      datasource:
        url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8
        username: root
        password: root
        driver-class-name: com.mysql.cj.jdbc.Driver
    mybatis-plus:
      # 指定 MapperXML 文件的位置
      mapper-locations: classpath:/mapper/**/*.xml
      # 指定实体类的包扫描路径
      type-aliases-package: top.ezjava.java17demo
      global-config:
        db-config:
          id-type: input
          # 驼峰下划线转换
          db-column-underline: true
          # 刷新 mapper
          refresh-mapper: true
      configuration:
        # 将 Java 实体类属性的驼峰命名规则转换为数据库字段的下划线命名规则
        map-underscore-to-camel-case: true
        # 查询结果中包含空值的列,在映射的时候,不会映射这个字段
        call-setters-on-nulls: true
        # 开启 sql 日志
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    logging:
      level:
        root: info
    
  • 主启动类
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    @MapperScan(basePackages = "top.ezjava.java17demo")
    public class Java17demoApplication {
        public static void main(String[] args) {
            SpringApplication.run(Java17demoApplication.class, args);
        }
    }
    
  • 初始化的表
    在这里插入图片描述

3 基本事务演示

  • 实体类对象:

    import com.baomidou.mybatisplus.annotation.TableField;
    import com.baomidou.mybatisplus.annotation.TableId;
    import com.baomidou.mybatisplus.annotation.TableName;
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    /**
     * User实体类
     * @Author Jasper
     * @Time 2024/2/12
     * @公众号:EzCoding 
     */
    @AllArgsConstructor
    @NoArgsConstructor
    @Data
    @TableName("t")
    public class TUser {
    
        @TableId
        private Long id;
    
        @TableField(value = "username")
        private String name;
    
        @TableField("password")
        private String password;
    
        public TUser(String name, String password) {
            this.name = name;
            this.password = password;
        }
    }
    
  • Mapper 接口

    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import org.apache.ibatis.annotations.Mapper;
    
    /**
     * Mapper
     * @Author Jasper
     * @Time 2024/2/12
     * @公众号:EzCoding 
     */
    @Mapper
    public interface TransactionMapper extends BaseMapper<TUser> {
    
    }
    
  • 业务类,由于只是简单验证,我就直接不再使用标准的三层结构,直接在 Controller 层写代码了

    import jakarta.annotation.Resource;
    import org.springframework.transaction.annotation.Transactional;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * 事务演示业务
     * @Author Jasper
     * @Time 2024/2/12
     * @公众号:EzCoding 
     */
    @RestController
    public class TransactionController {
    
        @Resource
        private TransactionMapper transactionMapper;
    
    //    @Transactional(rollbackFor = Exception.class)
        @GetMapping("/transactionDemo")
        public void transactionDemo() {
            TUser user1 = new TUser("Jasper", "abc");
            transactionMapper.insert(user1);
            int a = 1 / 0;
    
            TUser user2 = new TUser("EzCoding", "123");
            transactionMapper.insert(user2);
        }
    }
    
  • 执行访问,没加 @Transactional 注解时,遇到报错,会把 user1 插入到数据库,因此会留下脏数据
    在这里插入图片描述

  • 当加上 @Transactional 注解后,重启服务,再次访问,遇到报错的话,已插入的数据会被回滚
    在这里插入图片描述

4 事务失效演示

  • 情况1:当使用 this 调用方法时,事务会失效。将上述代码改为如下:
    @GetMapping("/transactionDemo")
    public void transactionDemo() {
        this.insert();
    }
    
    @Transactional(rollbackFor = Exception.class)
    public void insert() {
        TUser user1 = new TUser("Jasper", "abc");
        transactionMapper.insert(user1);
        int a = 1 / 0;
    
        TUser user2 = new TUser("EzCoding", "123");
        transactionMapper.insert(user2);
    }
    
    重启服务,执行访问,数据库又多了一条脏数据,如下图所示:
    this事务失效
  • 情况2:当异常未被抛出时,事务会失效,将代码改为如下:
    @Transactional(rollbackFor = Exception.class)
    @GetMapping("/transactionDemo")
    public void transactionDemo() {
        TUser user1 = new TUser("Jasper", "abc");
        transactionMapper.insert(user1);
        try {
            int a = 1 / 0;
        } catch (Exception e) {
            System.out.println("异常了");
        }
        TUser user2 = new TUser("EzCoding", "123");
        transactionMapper.insert(user2);
    }
    
    在这里插入图片描述在这里插入图片描述
  • 情况3:非 public 修饰的方法,事务也会失效,IDEA 都会给出提示:
    @Transactional(rollbackFor = Exception.class)
    @GetMapping("/transactionDemo")
    private void transactionDemo() {
        TUser user1 = new TUser("Jasper", "abc");
        transactionMapper.insert(user1);
        int a = 1 / 0;
        TUser user2 = new TUser("EzCoding", "123");
        transactionMapper.insert(user2);
    }
    
    在这里插入图片描述

5 总结

  • 事务就是要求业务代码操作数据库时,要么同时成功要么同时失败,不允许出现部分成功的情况
  • @Transactional 注解提供了声明式事务管理的便捷方式
  • 事务的失效情况总结:
    • 非公共方法@Transactional 注解的方法必须是 public 的
    • 方法内部调用:避免通过 this 进行内部调用
    • 异常处理不当:确保异常被正确抛出和捕获
    • 类未被 Spring 管理:确保类被 Spring 容器管理
    • 数据库不支持事务:MyISAM 不支持事务,MySQL 默认的存储引擎是 InnoDB(支持事务)

  • 创作不易,感谢阅读,若您喜欢这篇文章,不妨传承这份知识的力量,点个赞或关注我吧~
  • 微信gzh:EzCoding
  • 40
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值