目录
2.2 Spring声明式事务 @Transactional
1.事务回顾
1.1 什么是事务
事务是一组操作的集合, 是一个不可分割的操作:
事务会把所有的操作作为一个整体, 一起向数据库提交或者是撤销操作请求, 所以这组操作要么同时成功, 要么同时失败.
1.2 为什么需要事务
我们在进行程序开发时,也会有事务的需求
比如转账操作:
第一步: A账户 -100 元.
第二步: B 账户 +100 元
如果没有事务,第一步执行成功了, 第二步执行失败了, 那么A 账户的100 元就平白无故消失了. 如果使用事务就可以解决这个问题, 让这一组操作要么一起成功, 要么一起失败.
比如秒杀系统,
第一步: 下单成功
第二步: 扣减库存
下单成功后, 库存也需要同步减少, 如果下单成功, 库存扣减失败, 那么就会造成下单超出的情况. 所以就需要把这两步操作放在同一个事务中. 要么一起成功, 要么一起失败.
理解事务概念为主, 实际企业开发时, 并不是简单的通过事务来处理。
1.3 事务的操作
事务的操作主要有三步:
-- 开启事务
start transaction;
-- 提交事务
commit;
-- 回滚事务
rollback;
2. Spring 中事务的实现
Spring 中的事务操作分为两类:
1.编程式事务 (手动写代码操作事务)
2.声明式事务 (利用注解自动开启和提交事务)
在学习事务之前, 我们先准备数据和数据的访问代码
需求: 用户注册, 注册时在日志表中插入一条操作记录.
数据准备:
-- 创建数据库
DROP DATABASE IF EXISTS trans_test;
CREATE DATABASE trans_test DEFAULT CHARACTER SET utf8mb4;
use trans_test;
-- ⽤⼾表
DROP TABLE IF EXISTS user_info;
CREATE TABLE user_info (
`id` INT NOT NULL AUTO_INCREMENT,
`user_name` VARCHAR (128) NOT NULL,
`password` VARCHAR (128) NOT NULL,
`create_time` DATETIME DEFAULT now(),
`update_time` DATETIME DEFAULT now() ON UPDATE now(),
PRIMARY KEY (`id`)
) ENGINE = INNODB DEFAULT CHARACTER
SET = utf8mb4 COMMENT = '⽤⼾表';
-- 操作⽇志表
DROP TABLE IF EXISTS log_info;
CREATE TABLE log_info (
`id` INT PRIMARY KEY auto_increment,
`user_name` VARCHAR ( 128 ) NOT NULL,
`op` VARCHAR ( 256 ) NOT NULL,
`create_time` DATETIME DEFAULT now(),
`update_time` DATETIME DEFAULT now() ON UPDATE now()
) DEFAULT charset 'utf8mb4';
代码准备:
model层:
mapper层:
server层:
2.1 Spring 编程式事务(了解)
Spring 手动操作事务和上面 MySOL操作事务类似,有3个重要操作步骤:
SpringBoot 内置了两个对象:
我们还是根据代码的实现来学习:
package com.example.trans.controller;
import com.example.trans.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/user")
@RestController
public class UserController {
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
@Autowired
private UserService userService;
@RequestMapping("register")
public Boolean register(String username, String password) {
//开启事务
TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);
Integer result = userService.insert(username, password);
System.out.println("插入用户表, result: " + result);
//回滚事务
// dataSourceTransactionManager.rollback(transaction);
//提交事务
dataSourceTransactionManager.commit(transaction);
return true;
}
}
这部分代码了解即可:
以上代码虽然可以实现事务,但操作也很繁琐,有没有更简单的实现方法呢?
接下来我们学习声明式事务
2.2 Spring声明式事务 @Transactional
声明式事务的实现很简单
在需要事务的方法上添加 @Transactional
注解就可以实现了.无需手动开启事务和提交事务, 进入方法时自动开启事务, 方法执行完会自动提交事务, 如果中途发生了没有处理的异常会自动回滚事务.
我们来看代码实现:
@RequestMapping("/trans")
@RestController
public class TransController {
@Autowired
UserService userService;
@Autowired
LogService logService;
@Transactional
@RequestMapping("/register")
public Boolean register(String userName, String password) {
Integer result = userService.insert(userName, password);
System.out.println("插入用户表, result: " + result);
return true;
}
}
运行程序, 发现数据插入成功
修改程序,使之出现异常
运行程序:
发现虽然日志显示数据插入成功, 但数据库却没有新增数据, 事务进行了回滚
@Transactional作用
@Transactional 可以用来修饰方法或类:
方法/类 被 @Transactional 注解修饰时, 在目标方法执行开始之前, 会自动开启事务, 方法执行结束
之后,自动提交事务.
如果在方法执行过程中, 出现异常, 且异常未被捕获, 就进行事务回滚操作
如果异常被程序捕获, 方法就被认为是成功执行, 依然会提交事务,
运行程序, 事务成功提交,:
在上述程序中, 发现虽然程序出错了, 但是由于异常被捕获了, 所以事务依然得到了提交
如果需要事务进行回滚, 有以下两种方式:
1.重新抛出异常
2.手动回滚事务
对比事务提交和回滚的日志
事务提交时:
事务回滚时:
当事务提交时, 日志会含有: Transaction synchronization committing SqlSession
3. @Transactional详解
通过上面的代码, 我们学习了 @Transactional 的基本使用. 接下来我们学习 @Transactional
注解的使用细节.
我们主要学习 @Transactional 注解当中的三个常见属性:
1. rollbackFor: 异常回滚属性, 指定能够触发事务回滚的异常类型, 可以指定多个异常类型
2.lsolation: 事务的隔离级别. 默认值为 Isolation.DEFAULT
3. propagation: 事务的传播机制. 默认值为 Propagation.REQUIRED
3.1 rollbackFor
- 默认情况下,Spring 的事务管理器只会在抛出运行时异常(
RuntimeException及其子类
)和错误(Error
)时才会回滚事务。这是因为在 Java 的异常体系中,非运行时异常(例如IOException
、SQLException
等检查异常)通常被认为是可以预期和合理处理的情况。 - 例如,如果你有一个方法是从文件中读取数据,可能会抛出
IOException
。在这种情况下,开发人员可能希望在捕获这个异常后进行一些特定的处理,比如记录日志、提示用户重新输入等操作,而不是直接回滚事务。
验证抛出IOException异常, 事务提交:
运行程序, 事务成功提交:
如果我们需要所有异常都回滚, 需要来配置 @Transactional 注解当中的 rollbackFor 属性, 通
过 rollbackFor 这个属性指定出现何种异常类型时事务进行回滚.
结论:
1.在Spring的事务管理中,默认只在遇到运行时异常 RuntimeException 和 Error 时才会回滚
2.如果需要回滚指定类型的异常, 可以通过 rollbackFor 属性来指定
3.2 @Transactional 注解什么时候会失效
1. 方法的访问权限问题
情况说明:如果@Transactional
注解标记的方法是private
的,那么这个注解将会失效。这是因为 Spring 事务管理是基于代理的机制来实现的,代理对象无法访问目标对象的private
方法。
示例代码:
import org.springframework.transaction.annotation.Transactional;
public class TransactionalService {
@Transactional
private void privateTransactionalMethod() {
// 业务逻辑
}
}
解释:在这个示例中,privateTransactionalMethod
方法虽然被标注了@Transactional
,但是由于它是private
方法,Spring 无法为其创建有效的代理来管理事务,所以该注解实际上不会起作用。
2. 方法内部调用自身方法的情况
情况说明:当一个类中的方法 A 调用了同一个类中的另一个被@Transactional
注解标记的方法 B,并且这个调用是在方法 A 内部直接调用(而不是通过代理对象调用)时,方法 B 上的@Transactional
注解会失效。这是因为在这种情况下,没有经过 Spring 的代理拦截,也就无法开启事务管理。
示例代码:
import org.springframework.transaction.annotation.Transactional;
public class TransactionalService {
public void outerMethod() {
innerTransactionalMethod();
}
@Transactional
public void innerTransactionalMethod() {
// 业务逻辑
}
}
解释:在这个例子中,outerMethod
直接调用innerTransactionalMethod
,这种内部调用不会触发 Spring 的事务代理,所以innerTransactionalMethod
上的@Transactional
注解在这里不会产生事务管理的效果。
3. 异常被捕获但没有重新抛出的情况
情况说明:@Transactional
注解默认是在运行时异常(RuntimeException
)和错误(Error
)抛出时才会回滚事务。如果在被@Transactional
注解标记的方法中捕获了异常,并且没有重新抛出(对于需要回滚事务的异常类型),那么事务将不会回滚。
示例代码:
import org.springframework.transaction.annotation.Transactional;
public class TransactionalService {
@Transactional
public void methodWithTransaction() {
try {
// 业务逻辑,可能会抛出RuntimeException
throw new RuntimeException("模拟异常");
} catch (RuntimeException e) {
// 捕获异常但没有重新抛出
}
}
}
解释:在这个示例中,methodWithTransaction
方法中虽然抛出了RuntimeException
,但是在捕获这个异常后没有重新抛出,所以事务不会回滚。
4.数据库不支持事务或者事务配置错误的情况
情况说明:如果使用的数据库本身不支持事务(虽然这种情况比较少见,大多数主流数据库都支持事务),或者在 Spring 配置中事务相关的配置(如数据源、事务管理器等)出现错误,那么@Transactional
注解也会失效。
5. 使用了不兼容的事务传播行为组合
情况说明:在多个方法嵌套调用并且每个方法都有@Transactional
注解的情况下,如果事务传播行为组合不兼容,可能会导致事务失效或者不符合预期。例如,在一个方法中使用REQUIRES_NEW
传播行为,而在嵌套调用的方法中使用NEVER
传播行为,这可能会导致事务无法正常开启或者出现冲突。
示例代码:
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
public class TransactionalService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void outerMethod() {
innerMethod();
}
@Transactional(propagation = Propagation.NEVER)
public void innerMethod() {
// 业务逻辑
}
}
解释:在这个例子中,outerMethod
要求开启一个新事务(REQUIRES_NEW
),而innerMethod
却声明不允许在事务中执行(NEVER
),这种不兼容的传播行为组合可能会导致事务管理出现问题,使得@Transactional
注解无法按照预期工作。
3.3 事务的隔离级别
3.3.1 MySQL 事务隔离级别
SQL标准定义了四种隔离级别, MySQL全都支持, 这四种隔离级别分别是:
1.读未提交(READ UNCOMMITTED): 读未提交, 也叫未提交读, 该隔离级别的事务可以看到其他事务中未提交的数据.
2.读提交(READ COMMITTED): 读已提交, 也叫提交读, 该隔离级别的事务能读取到已经提交事务的数据.
3. 可重复读(REPEATABLE READ): 事务不会读到其他事务对已有数据的修改, 即使其他事务已提交. 也就可以确保同一事务多次查询的结果一致, 但是其他事务新插入的数据, 是可以感知到的. 这也就引发了幻读问题. 可重复读, 是 MySQL 的默认事务隔离级别.
4.串行化(SERIALIZABLE): 序列化, 事务最高隔离级别, 它会强制事务排序, 使之不会发生冲突, 从而解决了脏读, 不可重复读和幻读问题, 但因为执行效率低,所以真正使用的场景并不多,
3.3.2 Spring 事务隔离级别
Spring 中事务隔离级别有5 种:
package org.springframework.transaction.annotation;
public enum Isolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
private final int value;
private Isolation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
Spring 中事务隔离级别可以通过 @Transactional 中的 isolation 属性进行设置
@Transactional(isolation = Isolation.READ_COMMITTED)
3.4 Spring 事务传播机制
3.4.1 什么是事务传播机制
事务传播机制就是: 多个事务方法存在调用关系时, 事务是如何在这些方法间进行传播的。
比如有两个方法 A, B 都被 @Transactional 修饰,A方法调用B方法
A方法运行时, 会开启一个事务. 当A调用B时, B方法本身也有事务, 此时B方法运行时, 是加入A的事务, 还是创建一个新的事务呢?
这个就涉及到了事务的传播机制.
事务隔离级别解决的是多个事务同时调用一个数据库的问题
而事务传播机制解决的是一个事务在多个节点(方法)中传递的问题
3.4.2 事务的传播机制有哪些
@Transactional 注解支持事务传播机制的设置, 通过 propagation 属性来指定传播行为
Spring 事务传播机制有以下7种:
public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
private final int value;
private Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
比如一对新人要结婚了, 关于是否需要房子
总结