spring中开启事务
# Spring中开启事务
# 什么是事务
在编程中,事务(Transaction)是指一个包含了一组操作或步骤的程序单元,这些操作或步骤被封装在一起作为一个单独的执行单元来执行。更简单的说就是,一个有若干操作的集合。执行后,它们要么一起成功,要么一起失败。
# 事务的特性
事务有四大特性(ACID),原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)四个属性,这些属性被称为ACID属性。
- 原子性:事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。
- 隔离性:事务的执行不会影响其他事务的执行。
- 持久性:一旦事务提交,其修改的效果就是永久性的。
- 一致性:事务的执行不能破坏数据库数据的完整性和一致性(必须使数据库从一个一致性状态变换到另一个一致性状态)。
# Spring中的事务
在简单的了解事务后,接下来看看在Spring中如何实现事务。
Spring 支持两种类型的事务:声明式事务和编程式事务。
# 编程式事务
它一种通过编程方式控制事务边界的方法,它提供了更加灵活的事务控制方式,允许开发人员根据业务需求自定义事务的边界——由开发人员手动控制事务的开启、提交和回滚,以实现对事务的精细控制。
Spring提供了PlatformTransactionManager接口
和TransactionTemplate类
来实现编程式事务管理。
开发人员可以通过注入TransactionTemplate
实例,并使用该实例的execute方法来执行包含事务操作的业务逻辑。
- 以下是通过
TransactionTemplate类
来实现事务管理的方法
@Autowired
private TransactionTemplate transactionTemplate;
public void updateUser(User user) {
try {
transactionTemplate.execute(status -> {
// 在事务中执行业务逻辑
// ...
return null;
});
} catch (Exception e) {
// 发生异常时回滚事务
transactionTemplate.rollback(status -> {
// 回滚事务
return null;
});
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
而PlatformTransactionManager
接口定义了3个基本的事务操作方法,具体的实现都是由不同的子类来实现的。它有众多实现,但是我们并不需要掌握这些具体实现类的用法,我们只需要掌握好 PlatformTransactionManager
的用法即可。例如:如果你使用的是 JDBC 那么可以将 DataSourceTransactionManager
作为事务管理器。
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
// 源码
2
3
4
5
6
- 以下是使用
PlatformTransactionManager
的实现类DataSourceTransactionManager
(springboot内置的)来实现编程式事务
@Autowired
private DataSourceTransactionManager transactionManager;
public void updateName(){
// 定义事务属性
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// 获取事务状态
TransactionStatus status = transactionManager.getTransaction(def);
try {
// 在此处执行数据库操作或其他业务逻辑
String username = "ready to one+1";
Integer num = 1;
workersMapper.updateUserName(username, num);
int res = 1/0;
// 提交事务
transactionManager.commit(status);
} catch (Exception e) {
// 发生异常时回滚事务
transactionManager.rollback(status);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
上述例子中,会发生异常ArithmeticException,从而导致事务回滚,不会对数据库进行更新。
以上就是编程式事务,可以发现——它将事务的控制跟业务代码混合在了一起,这一定程度上增加了代码的复杂性。且在每个操作前后都需要手动调用事务管理器的方法,这可能会增加出错的可能性。如果在一些业务复杂的情况下,该情况尤为明显。
因此,对于复杂的业务逻辑,通常建议使用声明式事务管理,因为它更简洁、更易于管理。
# 声明式事务
使用声明式事务——首先,需要在Spring的配置文件中配置一个PlatformTransactionManager
的实现,例如DataSourceTransactionManager
。然后,可以在需要使用事务的方法或类上添加注解,例如@Transactional
注解,来开启事务。
- 配置事务管理器
@Configuration
public class AppConfig {
@Autowired
private DataSource dataSource;
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource);
}
}
2
3
4
5
6
7
8
9
10
11
- 通过注解来开启事务
@Transactional
public void test(){
String username = "part+1";
Integer num = 1;
workersMapper.updateUserName(username, num);
int res = 1/0;
}
2
3
4
5
6
7
@Transactional
注解开启了事务,test
方法在事务的保护下执行,如果在执行过程中出现了异常,事务会被自动回滚。
以上就是spring中使用事务的两种方式
# @Transactional实现原理
- 它是声明式事务,它是不侵入代码的,之所以有这个特性,是跟它实现的原理有关
@Transactional
注解的逻辑是通过动态代理
来实现的,而生成这个动态代理类分成了两步:
1、向spring容器注册事务相关的切面逻辑
2、根据切面逻辑生成动态代理
当一个方法被标记为 @Transactional
时,Spring 会创建一个代理对象(JDK 动态代理或 CGLIB 代理),这个代理对象会在方法调用前开启事务,在方法调用后提交或回滚事务。
- 以下为参考讲解链接
- 掘金文章讲解 (opens new window)
动态代理为标注了
@Transactional
注解的方法增加切面逻辑,而事务的上下文包括数据库链接都是通过ThreadLocal
来传递。具体的执行则是通过反射实现。
Java 反射(Reflection)
是一种强大的机制,允许程序在运行时检查和操作类、字段、方法和构造函数等类的内部结构。通过反射,你可以在运行时获取类的信息,并且可以动态地调用方法、访问字段、创建对象等。这种能力使得 Java 反射在很多场景下非常有用,比如框架开发、动态代理、插件系统等。
# 注意事项
使用注解@Transactional
会有失效的情况,主要可以分为以下三大类:
# 代理不生效
- 在同一类中调用该类的另一方法,另一方法的事务不会生效,因为同一类中方法相互调用不会被方法拦截器拦截到,因此没有经过Spring的代理类。默认只有在外部调用事务才会生效。即相同类里边,A 方法没有事务,B 方法有事务,A 方法调用 B 方法,则 B 方法的事务会失效。
- 被final或static修饰的方法,也无法生效
- 使用的方法为非public方法
- 当前类没被spring管理,换句话说就是,没有在类上标注
@Service
、@Component
等注解,这个类就不会被加载成一个Bean,这个类就不会被Spring管理了,事务就失效了。
关于final的原因
- 不可重写:
final
方法不能被子类重写。这意味着即使通过 JDK 动态代理或 CGLIB 代理生成了代理类,代理类也无法覆盖final
方法。关于static的原因:
- 不属于对象:
static
方法属于类而不是对象。代理对象是针对对象的,因此无法拦截static
方法的调用。Spring 使用代理模式来拦截带有
@Transactional
注解的方法调用,并在方法调用前后插入事务管理的逻辑。因此,只有被 Spring 管理的类(即由 Spring 容器创建和管理的 Bean)才能正确地应用@Transactional
注解。如果一个类没有被 Spring 管理,@Transactional
注解就会失效。
# 框架或底层不支持
- spring的事务注解
@Transactional
只能应用到 public 方法上才会有效。其他类型(如:private)的方法虽然不报错,但不会生效。 - 多线程调用
- 数据库本身不支持事务
@Transactional
注解只能应用到public
方法上才会有效,这是由 Spring AOP 的实现机制决定的。下面是简要解释:
Spring AOP 的设计决策之一是只支持
public
方法。主要原因如下:
- 代理对象的可见性:代理对象需要能够访问目标方法,
public
方法是最容易被访问的。- 一致性:确保所有被代理的方法都具有相同的可见性,避免混淆和潜在的问题。
- 性能:
public
方法的代理实现更为简单和高效。
# 使用不当
- 异常被方法内部try catch捕获,未抛出
- rollbackFor属性设置错误
- 设置不支持事务
建议事务注解 @Transactional 一般添加在实现类上,而不要定义在接口上,如果加在接口类或接口方法上时,只有配置基于接口的代理这个注解才会生效。
# 关于不建议使用@Transactional
在阿里的开发手册中可以看到,它是不推荐使用@transactional注解的。因为它会有很多注意事项,一不注意就会导致事务失效,且还容易导致接口效率变慢————比如:方法中涉及到远程方法调用,或者方法中涉及到多个读操作。甚至出错,不容易排查,比如:方法中不小心捕获了异常,没有抛出,或者是抛出了异常是非受检异常,导致事务无法回滚。尤其是在复杂系统中。
个人认为其根本原因在于@transactional锁的粒度太大导致的。以下是2个例子:
# 受检异常例子
下面是一个伪代码
@Transactional
public 某个结算任务(支付参数) throws ParseException{
// 前置检查
xxxx
for(){
.....
}
....
}
2
3
4
5
6
7
8
9
如果参数中,带有日期字段,如果在这执行途中抛出了解析异常,该方法是不会回滚的。如果它的偏差只有几分钱,你可能不会注意到,因为该方法不会报错。日期月累下来,突然某一天要结算时,你会发现差额会异常的大。
因为解析异常是受检异常,即使抛出也不会回滚。
当然,它的解决方法也有,比如,加上rollbackFor属性
@Transactional(rollbackFor=Exception.class)
public 某个结算任务(支付参数) throws ParseException{
// 前置检查
xxxx
for(){
.....
}
....
}
2
3
4
5
6
7
8
9
这样一来就变成了任何异常都会回滚,当然你也可以指定更细粒度的异常。
# 远程调用例子
以下的事务可以正常实现,但遇到流量高峰期,会导致系统异常缓慢:
@Transactional(rollbackFor=Biz.class)
public Boolean service(){
queryTable1();
// 下列三个是 外部服务调用
outerServiceA();
outerServiceB();
outerServiceC();
updateTable1();
updateTable2();
return true;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
如果外部服务异常,返回就会很慢,但因为之前就已经开启了事务,所以queryTable1()方法,会造成大量事务,大量的锁堆积在数据库,从而影响其他服务对数据库的访问,导致整个系统响应很低,甚至于说系统垮掉。
当然解决方案也是有的,比如使用编程式事务:
@Resource
private TransactionTemplate transactionTemplate;
@Transactional(rollbackFor=Biz.class)
public Boolean service(){
queryTable1();
// 下列三个是 外部服务调用
outerServiceA();
outerServiceB();
outerServiceC();
//5.事务统一处理
transactionTemplate.execute((transactionstatus)->-
try{
//事务尽可能尽可能的小
updateTable1();
updateTable2();
}catch (Exception e){
transactionStatus.setRollbackOnly();
log.error("[服务] 更新错误!",e);
}
return transactionStatus;
});
return true;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
以上的目的是为了提高事务的精确度,这也是不提倡用注解事务的原因,它的粒度太大了。
有人可能会说,把更新的方法抽离出来,再使用@transactional可以吗?当然可以的,但前提是你别在本类中调用,否则它无法生效。
当然,如果你可以保证事务的粒度,使用注解@transactional也没事,毕竟官网是推荐使用它的,因为它的的确确很方便。