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); }
- 情况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