spring-5-事务
参考:
一、事务基础
1.什么是事务
事务是指作为单个逻辑工作单元执行的一系列操作,要么全部成功执行,要么全部失败回滚到初始状态,保证数据的一致性和完整性。事务具有ACID特性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
Spring的事务是指在Spring框架中对数据库操作进行管理的机制。通过Spring的事务管理,可以确保一组数据库操作要么全部成功提交,要么全部失败回滚,保持数据的一致性和完整性。Spring的事务管理可以通过声明式事务和编程式事务两种方式来实现。
简而言之,Spring事务要学习的就是,在接入数据库后咱们如何结合Spring框架管理好事务。
2.ACID特性
3.Spring中进行事务管理的2种方式
- 声明式事务
- 编程式事务
特性 | 声明式事务 | 编程式事务 |
---|---|---|
定义 | 使用注解或XML配置声明事务边界 | 手动编写代码管理事务 |
使用简便性 | 高。通过注解或XML配置即可完成事务管理 | 低。需要显式编码来管理事务 |
代码可读性 | 高。事务边界清晰,代码简洁 | 低。混合了业务逻辑和事务管理代码 |
灵活性 | 低。基于配置的方式,灵活性较低 | 高。可以在代码中灵活控制事务 |
侵入性 | 低。对业务逻辑代码侵入性小 | 高。对业务逻辑代码侵入性大 |
配置复杂度 | 低。通过注解或XML配置,简单明了 | 高。需要显式编写事务管理代码 |
维护性 | 高。配置与业务逻辑分离,便于维护 | 低。事务管理代码与业务逻辑耦合,不易维护 |
性能控制 | 中。大多数情况下性能表现良好 | 高。可以更精细地控制事务的行为和性能 |
学习成本 | 低。Spring 提供了便捷的注解和配置方式 | 高。需要熟悉 Spring 的事务管理 API |
适用场景 | 适用于大多数常见的事务管理场景 | 适用于需要细粒度控制事务的特殊场景 |
声明式事务:适用于大多数常见的事务管理场景,通过简单的注解或XML配置即可完成事务管理,适合对事务管理要求不是很复杂的情况下使用。
编程式事务:适用于需要细粒度控制事务的特殊场景,通过手动编写代码管理事务,可以灵活地控制事务的行为和性能,但相对复杂且侵入性较大。
二、事务隔离级别
参考下我这篇文章:Mysql-事务的基本特性和隔离级别
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ UNCOMMITTED(未提交读) | √ | √ | √ |
READ COMMITTED(已提交读) | × | √ | √ |
REPEATABLE READ(可重复读) | × | × | √ |
SERIALIZABLE(串行化) | × | × | × |
- 在MySQL中,默认的隔离级别是 REPEATABLE READ,即RR可重复读。
- 在Oracle中,默认的隔离级别是 READ COMMITTED,即RC读已提交。
三、声明式事务和编程式事务
下方例子中,为了演示,我们做以下例子。
mysql库:za7za8
表名:u_user
CREATE TABLE `u_user` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '姓名',
`age` int NOT NULL COMMENT '年龄',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
现在是个空表,后方我们的目的是插入条user数据,并将年龄更新。
-
插入一条数据
INSERT INTO `za7za8`.`u_user`(`name`, `age`) VALUES ('yang', 10);
-
更新数据
UPDATE `za7za8`.`u_user` SET `age` = 12 WHERE `name` = 'yang';
先手动演示下,等会演示时我们清空库。
SpringBoot的项目呢,我们也不需要web啥的,关键是这两个依赖。
<spring-boot.version>2.6.13</spring-boot.version>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.7</version>
</dependency>
验证下能不能查出来数据,刚才不是演示插入了条。
然后呢,我们准备下方法,也验证下先。
public interface IUserService extends IService<User> {
User insertAndUpdate(User user,int age);
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public User insertAndUpdate(User user, int age) {
save(user);
user.setAge(age);
updateById(user);
return user;
}
}
用户su,年龄更新成199,验证下。
嗯,数据库中也正常,序号别关心,我刚才验证删除了下,这是第三条数据了。
现在,我们怎么让这个事务出问题呢,那就是插入后,我们把它id更新掉,更新查询id=xx的时候查不到更新不了。
理想的情况是更新失败后,开始插入的数据会消失,数据库中不会有脏数据,这才叫事务。
那就在更新语句加个时间等等呗,让我们有时间手动操作,稍微改造下代码,更新失败时抛出异常。
@Override
public User insertAndUpdate(User user, int age) {
// 保存
save(user);
// 尝试休眠
try {
Thread.sleep(30 * 1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 更新age
user.setAge(age);
boolean updateFlag = updateById(user);
if (!updateFlag) {
throw new RuntimeException("更新UserAge异常!");
}
// 返回db记录
return getById(user.getId());
}
我先清空表,然后用新用户li来试下,发现有数据后,我们就把id更新掉,让它更新不了。
表已经清空
数据库插入数据li
发现写入了id为1的用户li数据,我们把id更新成2。
接着程序里尝试用userId为1的来更新,失败了,抛出异常。
最后再来查询一下
经过这个过程,如果不考虑事务,我们会发现,哎,数据库里有脏数据了,不符合我们的预期。
1.声明式事务
1.1 使用注解@Transactional
一个基本的方式,就是在方法/类上加上@Transactional注解。
字段 | 类型 | 默认值 | 描述 |
---|---|---|---|
value | String | "" | transactionManager 的别名。定义要使用的事务管理器的名称。 |
transactionManager | String | "" | value 的别名。定义要使用的事务管理器的名称。 |
label | String[] | {} | 事务限定符的标签数组。 |
propagation | Propagation | Propagation.REQUIRED | 定义事务传播类型,确定事务之间的关系。 |
isolation | Isolation | Isolation.DEFAULT | 定义事务隔离级别,控制事务之间的隔离程度。 |
timeout | int | -1 | 定义事务的超时时间(以秒为单位),负值表示没有超时。 |
timeoutString | String | "" | 以字符串格式定义事务的超时时间,允许更灵活地指定持续时间。 |
readOnly | boolean | false | 指定事务是否为只读,只读事务在读取数据时进行了优化。 |
rollbackFor | Class<? extends Throwable>[] | {} | 指定应触发回滚的异常类数组。 |
rollbackForClassName | String[] | {} | 指定应触发回滚的异常类名称数组(以字符串形式)。 |
noRollbackFor | Class<? extends Throwable>[] | {} | 指定不应触发回滚的异常类数组。 |
noRollbackForClassName | String[] | {} | 指定不应触发回滚的异常类名称数组(以字符串形式)。 |
回顾之前更新userId的场景,我们会发现问题的原因在我们本意它是一个事务,但是。
- A操作:idea中跑的程序
- B操作:手动操作数据库
两者之间的隔离性出现问题了,我手动操作的时候,看到了本意是事务的idea程序中跑的数据,类似于脏读。
我们直接加上@Transactional注解试下?
清空表,重复下操作。
哎?我们会发现,这个时候手动操作查不到数据了。
最后等待方法完成,整个过程都是顺利的。
这是为啥?咋还改不了了?这就是事务的用处。
使用了事务后,默认开启了我们RR级别。
在RR级别下,根据MVCC机制,我们手动操作B是看不到刚刚插入的数据的。
1.2 使用xml文件
2.编程式事务
编程式事务,就是不利用注解等操作,我们自己手动写代码来完成。
编程式事务主要通过 TransactionTemplate
或者直接使用 PlatformTransactionManager
来实现。
使用编程式事务的场景:
- 动态控制事务边界:有些复杂的业务逻辑需要在运行时决定事务的边界,编程式事务可以提供这种灵活性。
- 在非 Spring 管理的对象中使用事务:在一些非 Spring 管理的对象中使用事务管理,此时可以通过编程式事务来实现。
- 对性能有特殊要求:编程式事务比声明式事务具有更低的开销,因为它不需要进行 AOP 代理的处理。
2.1 使用 TransactionTemplate
@Service
public class MyService {
@Resource
private TransactionTemplate transactionTemplate;
public void doSomething() {
transactionTemplate.execute(status -> {
// 在此处执行你的业务逻辑
// 如果抛出 RuntimeException 或 Error,事务将回滚
// 否则事务将提交
return null;
});
}
}
2.2 使用PlatformTransactionManager
@Service
public class MyService {
@Resource
private TransactionTemplate transactionTemplate;
public void doSomething() {
DefaultTransactionDefinition defaultTransactionDefinition = new DefaultTransactionDefinition();
// 设置事务的传播行为、隔离级别等属性
defaultTransactionDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = transactionManager.getTransaction(defaultTransactionDefinition);
try {
// 在此处执行你的业务逻辑
// 提交事务
transactionManager.commit(status);
} catch (Exception e) {
// 回滚事务
transactionManager.rollback(status);
}
}
}
四、事务管理器
事务管理器 | 简介 | 适用场景 | 特点 |
---|---|---|---|
DataSourceTransactionManager | 用于 JDBC 数据源的事务管理器 | 直接使用 JDBC 进行数据库操作的应用程序 | - 轻量级,性能好 - 简单易用,适用于纯 JDBC 场景 |
JpaTransactionManager | 用于 JPA 的事务管理器 | 使用 JPA 进行持久化操作的应用程序,例如 Spring Data JPA | - 支持 JPA 标准 - 可与 Spring Data JPA 无缝集成 |
HibernateTransactionManager | 用于 Hibernate 的事务管理器 | 直接使用 Hibernate API 进行持久化操作的应用程序 | - 深度集成 Hibernate 特性 - 支持 Hibernate 特有功能 |
五、事务超时与只读属性
1.事务超时(timeout)
事务超时属性定义了一个事务应该在多长时间内完成,如果事务在指定的时间内没有完成,它将被自动回滚。
设置事务超时的主要目的是避免长时间运行的事务占用资源,导致系统性能下降。
@Service
public class MyService {
// 设置超时时间为5秒
@Transactional(timeout = 5)
public void doSomething() {
// 执行业务逻辑
// 如果在5秒内没有完成事务,将自动回滚
}
}
2.只读事务(readOnly)
只读事务属性用于声明事务中的操作不会修改数据库内容。
设置只读事务的主要目的是让数据库能够优化事务处理,因为数据库知道它不需要为只读操作持有锁或维持更复杂的事务机制。
Spring 的 @Transactional
注解中的 readOnly
属性主要是一个提示,告诉 Spring 和底层数据库驱动这个事务应该是只读的。
Spring 会尝试将这个信息传递给底层的数据库驱动或 JPA 实现,以便数据库可以进行相应的优化。
@Service
public class MyService {
// 设置为只读事务
@Transactional(readOnly = true)
public void readOnlyOperation() {
// 执行只读操作,例如查询
}
}
那咱们的Mysql是支持只读事务的,用SET TRANSACTION
即可。
-- 设置只读事务
SET TRANSACTION READ ONLY;
-- 开始事务
START TRANSACTION;
-- 在事务中执行查询操作
SELECT * FROM my_table;
-- 提交事务
COMMIT;
六、事务回滚与异常处理
1.默认回滚
在 Spring事务中,默认的回滚行为如下:
- 发生
RuntimeException
或其子类异常时,事务回滚。 - 发生
Error
时,事务回滚。
这里需要注意的是:
- 未检查异常(
RuntimeException
或其子类):自动回滚 - 已检查异常(
Exception
或其子类):不会回滚
@Service
public class MyService {
@Transactional
public void performOperation() {
try {
// 执行业务逻辑
// 模拟未检查异常
if (true) {
throw new RuntimeException("模拟未检查异常");
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
在事务方法中抛出 RuntimeException
或其子类,事务自动回滚。
那我catch了留空不处理?会怎么样?
那你这个方法最后它没抛异常出来呗,事务将不会回滚。
2.使用 @Transactional 注解进行回滚控制
@Transactional
注解提供了一些属性来控制事务的回滚行为:
rollbackFor
:指定哪些异常会触发事务回滚。noRollbackFor
:指定哪些异常不会触发事务回滚。
哎,那我rollbackFor里面写个Exception呢?
@Transactional(rollbackFor = Exception.class)
在这个示例中,即使抛出的是 Exception
,事务也会回滚,因为 rollbackFor
属性指定了 Exception.class
。
3.手动回滚
如果想要手动回滚也是可以的,不过,你用好上面的注解就够了,不用这么麻烦。
// 手动回滚事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
4.注意区分e.printStackTrace()和throw new RuntimeException
-
e.printStackTrace()
是一个用于打印异常堆栈跟踪信息的方法。这种方法只是输出异常信息,并不会重新抛出异常,因此事务管理器不会感知到异常的存在。
如果在事务中使用
e.printStackTrace()
而不重新抛出异常,事务将会被视为成功,并且会被提交。 -
throw new RuntimeException("模拟未检查异常")
抛出一个新的
RuntimeException
,这种方法会将异常传递给调用者,Spring才会检测到这个异常并回滚事务。
七、嵌套事务与保存点
在 Spring 事务管理中,嵌套事务和保存点(Savepoints)是两个用于处理复杂事务场景的高级特性。
它们帮助在多个子事务中维护事务的一致性,并允许在事务的中间点进行部分回滚。
1.嵌套事务
嵌套事务是指在一个外部事务中包含一个或多个内部事务。
Spring 本身不直接支持嵌套事务,但是通过合适的传播行为(propagation behavior),可以实现类似嵌套事务的效果。
传播行为参考:八、事务传播行为
2.嵌套事务和新事物
嵌套事务是创建一个子事务,子事务内回滚,不会影响外层事务,外层事务回滚呢,子事务会跟着回滚。
新事务是一个完全独立的事务。
3.保存点
这个主要是针对部分回滚的场景。
保存点允许你在一个事务的中间点设置一个回滚点,以便在出现问题时回滚到该保存点,而不是完全回滚整个事务。
保存点(Savepoints)必须通过编程式事务管理来实现,声明式事务管理(基于注解的方式)不直接支持保存点的创建和回滚。
// 设置保存点
Object savepoint = status.createSavepoint();
// 发生异常时回滚到保存点,而不是回滚整个事务
status.rollbackToSavepoint(savepoint);
@Service
public class MyService {
@Resource
private TransactionTemplate transactionTemplate;
public void performOperation() {
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = transactionManager.getTransaction(def);
try {
// 执行业务逻辑
// 设置保存点
Object savepoint = status.createSavepoint();
try {
// 执行可能会抛出异常的业务逻辑
} catch (Exception e) {
// 发生异常时回滚到保存点,而不是回滚整个事务
status.rollbackToSavepoint(savepoint);
}
// 提交事务
transactionManager.commit(status);
} catch (Exception e) {
// 完全回滚事务
transactionManager.rollback(status);
}
}
八、事务传播行为
Spring 的事务传播机制定义了事务方法是如何相互影响的。
通过传播行为,我们可以指定一个事务方法是否应该运行在现有事务中,或者应该启动一个新的事务等。
简而言之,就是别人调用我这个方法的时候,我该怎么办?
是加入之前的事务呢、还是单独新建一个事务呢,又或者?
示例:
@Transactional(propagation = Propagation.NEVER)
spring事务的7种传播行为。
传播行为 | 描述 | 典型场景 |
---|---|---|
REQUIRED | 如果当前存在事务,则加入该事务。 如果当前没有事务,则创建一个新的事务。 |
默认传播行为,确保所有操作在同一事务中执行。 |
SUPPORTS | 如果当前存在事务,则加入该事务。 如果当前没有事务,则以非事务方式执行。 |
不强制要求事务的读操作。 |
MANDATORY | 如果当前存在事务,则加入该事务。 如果当前没有事务,则抛出异常。 |
必须在事务中运行的操作,由外部调用确保事务存在。 |
REQUIRES_NEW | 无论是否存在当前事务,都创建一个新的事务。 如果当前存在事务,则挂起当前事务。 |
需要独立事务的操作,例如独立的日志记录。 |
NOT_SUPPORTED | 如果当前存在事务,则挂起当前事务,并以非事务方式执行。 | 不希望在事务中运行的操作。 |
NEVER | 如果当前存在事务,则抛出异常。 如果当前没有事务,则以非事务方式执行。 |
确保操作不在事务中运行。 |
NESTED | 如果当前存在事务,则在当前事务中创建一个嵌套事务。 如果当前没有事务,则创建一个新的事务。 |
需要部分回滚的复杂事务。 |
例如:
@Service
public class ExampleService {
@Transactional(propagation = Propagation.REQUIRED)
public void method1() {
// 主事务逻辑开始
System.out.println("method1: 主事务开始");
// 执行method2,创建嵌套事务
try {
method2();
} catch (Exception e) {
System.out.println("method1: 捕获到异常 " + e.getMessage());
}
// 主事务逻辑继续
System.out.println("method1: 主事务继续");
// 主事务逻辑结束
System.out.println("method1: 主事务结束");
}
@Transactional(propagation = Propagation.NESTED)
public void method2() {
// 嵌套事务逻辑开始
System.out.println("method2: 嵌套事务开始");
// 模拟操作和异常
if (true) { // 可以根据实际条件进行调整
throw new RuntimeException("method2: 嵌套事务发生异常");
}
// 嵌套事务逻辑结束
System.out.println("method2: 嵌套事务结束");
}
}
在上面的代码中:
- method1:Propagation.REQUIRED,如果当前存在事务,则加入该事务。如果当前没有事务,则创建一个新的事务。
- method2:Propagation.NESTED,如果当前存在事务,则在当前事务中创建一个嵌套事务。如果当前没有事务,则创建一个新的事务。
执行流程
method1
被调用,并开启一个新的事务。method1
调用method2
。由于method2
使用Propagation.NESTED
,所以在method1
的事务中创建一个嵌套事务。- 在
method2
中,抛出一个运行时异常,导致method2
的嵌套事务回滚到保存点。 method1
捕获到method2
抛出的异常,继续执行剩下的事务逻辑。
预期输出
method1: 主事务开始
method2: 嵌套事务开始
method1: 捕获到异常 method2: 嵌套事务发生异常
method1: 主事务继续
method1: 主事务结束
九、事务实现原理
十、事务失效场景
1.方法权限为private
由于事务是基于AOP的,咱们的CGLIB又是靠继承来动态代理的。
所以呢,spring 要求被代理方法必须是public的。
private、default 或 protected 的话,spring 不会提供事务功能,源码也会检查是不是public的。
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// ...
例如下面这个,事务就不会生效。
@Service
public class MyService {
@Transactional
private void performOperation() {
// 事务将不会生效,因为方法不是公共的
}
}
2.方法为final或者static
原理同上。
3.同类内部方法调用
Spring 的事务管理是通过 AOP 实现的。
Spring 使用代理对象来拦截对目标方法的调用,并在方法执行前后插入事务管理逻辑。
当你从外部调用一个标注了 @Transactional
的方法时,实际上是调用了该方法的代理对象,代理对象会在调用实际方法之前开启事务,并在方法执行完成后提交或回滚事务。
当一个类的方法调用同一个类的另一个方法时,这种调用是直接的,不会经过代理对象。这意味着事务管理逻辑不会被触发,因为代理对象的拦截器根本没有机会插入事务管理逻辑。
@Service
public class MyService {
@Transactional
public void outerMethod() {
// 直接调用事务不会生效
innerMethod();
}
@Transactional
public void innerMethod() {
// 事务将不会生效
}
}
我从外部对象调用innerMethod(),事务会生效,因为实际上调用的是代理对象。
我从内部调用innerMethod(),事务会失效,直接拿着this.xx就执行了。
4.未被spring管理
使用 spring 事务的前提是,对象要被 spring 管理,像下方这个就漏了@Service。
public class UserService {
@Transactional
public void method() {
// 事务将不会生效
}
}
5.多线程
多线程环境下,不同线程拿到的数据库连接都不一样,跨线程则失效了。
6.非事务支持的DB
例如,咱们Mysql的myisam 存储引擎不支持事务。