【中】Spring使用@Transactional 管理事物,Java事物详解
B站视频:https://www.bilibili.com/video/BV1eV411u7cg
技术文档:https://d9bp4nr5ye.feishu.cn/wiki/HX50wdHFyiFoLrkfEAAcTBdinvh
一、什么是事物
简单来说事物就是一组对数据库的操作事物要保证可靠性,必须具备四个特性:ACID。
- A:原子性:事物是一个原子操作单元,要么完全执行,要么完全不执行。事物中的所有操作要么全部成功,要么全部失败,没有中间状态。
- C:一致性:事物在执行前和执行后都必须保持数据库的一致性状态。
- I:隔离性:事物的隔离性确保并发执行的事物彼此不会相互干扰。
- D:一致性:一旦事物提交,其结果应该是永久性的,即使在系统故障的情况下也是如此。
二、什么是声明事物,什么是编程事物
声明事物
声明式事物是通过配置的方式来管理事物的行为,声明式事物的好处是可以将事物管理与业务逻辑相分离,提高了代码的可读性和维护性。
编程事物
编程式事物是通过编写代码显式地管理事物的开始、提交和回滚。使用编程式事物可以更加灵活地控制事物的细节,但需要更多的代码来处理事物管理,可能导致代码的冗余和增加了复杂性。
三、Spring 如何实现声明事物和编程事物的
声明事物
声明事物的代码很简单,我们也是经常使用的。
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
@Transactional
public void performTransaction() {
// 事物逻辑
}
}
编程事物
编程事物,需要自己来控制事物的流程,更加灵活但也更加复杂,一般不建议使用。(实际上我也没在生产环境中用过)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
@Service
public class TransactionService {
private final TransactionTemplate transactionTemplate;
@Autowired
public TransactionService(TransactionTemplate transactionTemplate) {
this.transactionTemplate = transactionTemplate;
}
public void performTransaction() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
// 事物逻辑
} catch (Exception e) {
status.setRollbackOnly();
throw e;
}
}
});
}
}
四、声明事物是怎么实现的
虽然我们实现事物的方式有声明式和编程式,但在实际的使用中,我们只会用声明式,所以我们有必要来深入理解一下声明事物。
其实简单来说在Spring中,我们开启声明事物用的是@Transactional
,本质上是使用的 代理和AOP来实现。
- 事物拦截器链(Interceptor Chain):Spring的声明式事物依赖于AOP技术,在运行时动态生成代理对象并创建事物拦截器链。在方法调用链中,每个事物拦截器都会被依次调用,并根据事物属性的定义决定是否开启、提交或回滚事物。
- 事物切点(Transaction Pointcut):事物切点定义了哪些方法需要被事物拦截器拦截并应用事物逻辑。切点通过表达式语言(如Spring表达式语言)或基于注解的方式来指定匹配的方法。Spring提供了灵活的切点表达式来满足各种粒度的事物控制需求。
- 事物属性解析:在声明式事物中,事物属性可以通过注解(如@Transactional)或配置文件来指定。事物属性包括隔离级别、传播行为、超时设置等。Spring会解析事物属性,并将其应用于方法上,以确定事物的行为。事物属性解析器根据事物定义的优先级,从全局配置或方法级别的注解中获取事物属性。
- 事物管理器(Transaction Manager):事物管理器是Spring框架的核心组件之一。它负责处理实际的事物管理操作,与底层的数据访问技术(如JDBC、Hibernate等)进行交互。事物管理器负责事物的创建、提交和回滚,并与当前线程进行绑定。Spring提供了多种事物管理器的实现,如
DataSourceTransactionManager
、JpaTransactionManager
等,可以根据具体的数据访问技术进行配置。 - 事物同步器(Transaction Synchronization):事物同步器用于在事物的不同阶段注册回调方法。在事物提交或回滚时,事物同步器会触发注册的回调方法,以执行一些额外的操作。例如,清理数据库连接、提交缓存数据等。Spring利用事物同步器来确保与事物相关的资源的正确管理和释放。
- 事物切面(Transaction Aspect):事物切面是由事物拦截器和事物切点组成的,它定义了在目标方法执行前后应用事物逻辑的规则。事物切面通过AOP技术将事物管理逻辑与业务逻辑进行解耦。当目标方法被调用时,事物切面会根据事物属性的定义,决定是否开启、提交。
五、@Transactional 注解的参数
在声明事物中,我们只需要和注解 @Transactional 打交道,所以我们有必要来深入理解一下这个注解中的参数配置。
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
// 事物管理器、暂时先忽略它,我们也不会去修改这个参数的值
@AliasFor("value")
String transactionManager() default "";
String[] label() default {};
// 事物传播行为
Propagation propagation() default Propagation.REQUIRED;
// 事物隔离级别
Isolation isolation() default Isolation.DEFAULT;
// 事物超时时间 -1,为永久不超时, 单位是秒
int timeout() default -1;
// 事物超时时间,可以设置单位,比如 timeoutString = "30s"
String timeoutString() default "";
// 是否只读事物
boolean readOnly() default false;
// 对哪些异常进行回滚
Class<? extends Throwable>[] rollbackFor() default {};
// 对哪些异常进行回滚【异常全限定名】
String[] rollbackForClassName() default {};
// 对哪些异常不回滚
Class<? extends Throwable>[] noRollbackFor() default {};
// 对哪些异常不回滚【异常全限定名】
String[] noRollbackForClassName() default {};
}
rollbackFor和rollbackForClassName的区别,直接来看使用方式。 最好使用rollbackFor 可以在编译的时候就帮我买检查是不是对的。
@Transactional(rollbackFor = Exception.class, rollbackForClassName = {"java.lang.Exception"})
@Transactional 注解的参数虽然多,但绝大部分都很好理解。这里主要是来说两个重要且不好理解的参数propagation
和 isolation
propagation (事物传播行为)
事物的传播行为是指:。它的配置如下:
值(小写方便阅读) | 描述 |
---|---|
REQUIRED(required) 默认值 | 1.如果当前没有事物,则创建一个新的事物,并将当前方法作为事物的起点。 2.如果当前已经存在事物,则加入到当前事物中,成为当前事物的一部分。 3.当前事物的提交和回滚都将影响到该方法。 |
REQUIRES_NEW (requires_new) | 1.无论当前是否存在事物,都创建一个新的事物。 2.如果当前存在事物,则将当前事物挂起,并启动一个新的事物。 3. 当前方法独立于外部事物运行,它有自己的事物边界。 |
SUPPORTS(supports) | 1. 如果当前存在事物,则加入到当前事物中,成为当前事物的一部分。 2.如果当前没有事物,则以非事物方式执行。 3.支持当前事物的执行,但不强制要求存在事物。 |
NOT_SUPPORTED (not_supported) | 1.以非事物方式执行操作。 2.如果当前存在事物,则将其挂起。 3.该方法在一个没有事物的环境中执行。 |
NEVER(never) | 1.以非事物方式执行操作。 2.如果当前存在事物,则抛出异常,表示不允许在事物中执行该方法。 |
MANDATORY(mandatory) | 1.要求当前存在事物,否则抛出异常。 2.该方法必须在一个已经存在的事物中被调用。 |
NESTED (nested) | 1.如果当前存在事物,则在嵌套事物中执行。 2.如果当前没有事物,则行为类似于 REQUIRED,创建一个新的事物。 |
存在事物的时候REQUIRED和NESTED的区别:REQUIRED 是加入当前事物,成为当前事物的一部分,NESTED 是生成嵌套事物,本质上是两个事物。(具体区别下面实践演示)
isolation(事物隔离级别)
其实就是我们之前学习数据库时候的数据库隔离级别了。
值(小写方便阅读) | 描述 |
---|---|
DEFAULT (default) | 默认的,看当前数据库默认的隔离级别是什么。 |
READ_UNCOMMITTED (read_uncommitted) | 读未提交 |
READ_COMMITTED (read_committed) | 读已提交 |
REPEATABLE_READ (repeatable_read) | 可重复读 |
SERIALIZABLE (serializable) | 序列化 |
六、@Transactional 实践
在使用 @Transactiona 注解的时候,一定要设置rollbackFor的值,默认情况下是,比如 IOException、SQLException 等。
理论
在深入理解事物的传播行为之前,我们需要理解三个基本的概念,理解了它们我们就理解了事物传播行为。它们分别是:嵌套事物、新事物、当前事物。
为了理解方便,这里我们先约定一下,有两个事物方法A和B,在A里面调用B。且A事物的定义如下不会改变,B事物的传播行为可能会变。
@Transactional(rollbackFor = Exception.class)
public void A() {
userMapper.insertUser("A",1);
sqlTestService.B();
}
public void B() {
userMapper.insertUser("B",2);
}
嵌套事物
修改B事物的传播行为,让它生成嵌套事物
@Transactional(rollbackFor = Exception.class, propagation = Propagation.NESTED)
嵌套事物和父事物是有关联的,当A事物回滚的时候,B事物一定回滚。
当B事物异常回滚的时候,要判断在A里面是否try了B事物,如果try就A不会回滚,只是B回滚。
新事物
修改B事物的传播行为,让它生成新事物
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
既然都说了是新事物,那A、B事物没有什么必然的关系了。
当前事物
修改B事物的传播行为,让它加入当前事物
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
@Transactional(rollbackFor = Exception.class, propagation = Propagation.SUPPORTS)
既然说是加入当前事物,那其实本质上还是一个事物,不管怎么样的异常,也不管如何处理异常,
实践
insertUser 方法就是一个简单的插入语句,为了避免误会,这里直接给出来。
@Insert("INSERT INTO t_users (`name`, `age`) VALUES (#{name}, #{age})")
void insertUser(@Param("name") String name,@Param("age") Integer age);
异常的话,是直接手动抛出一个异常
throw new RuntimeException("xxxxx");
B方法是否try:是指在A方法调用B方法的时候,是否使用了try catch 如下:
public void A() {
userMapper.insertUser("A",1);
try {
sqlTestService.B();
}catch (Exception e) {
e.printStackTrace();
}
}
代码很简单,来回变换很多,就不展示了,直接给执行结果:
B事物类型 | 异常方法 | B方法是否try | 插入数据结果 |
---|---|---|---|
新事物 | A方法 | 否 | B |
新事物 | A方法 | 是 | B |
新事物 | B方法 | 否 | 空 |
新事物 | B方法 | 是 | A |
嵌套事物 | A方法 | 否 | 空 |
嵌套事物 | A方法 | 是 | 空 |
嵌套事物 | B方法 | 否 | 空 |
嵌套事物 | B方法 | 是 | A |
加入当前事物 | A方法 | 否 | 空 |
加入当前事物 | A方法 | 是 | 空 |
加入当前事物 | B方法 | 否 | 空 |
加入当前事物 | B方法 | 是 | 空 |