01 引言
上一期介绍了Spring
数据的两种类型:声明式事务和编程式事务。编程式事务可以更加细粒度的控制事务,几乎没有什么苛刻的条件,因为其底层相当于捕捉到异常,手动回滚。
但是,声明式事务的使用虽然入门简单,一个简单的注解就可以开启事务。但是@Transactional
的属性影响着事务的控制,事务的声明也是有条件的,稍不注意就可能导致事务失效。
我们一起来深入了解一下吧!
02 核心实现原理
Spring
通过声明式事务管理简化了开发,其中@Transactional
注解是实现事务控制的核心手段。相比传统的JDBC
编程式事务,声明式事务通过AOP
实现解耦,使业务代码更清晰。
2.1 AOP代理机制
通过动态代理(JDK
或CGLIB
)在运行时对目标对象进行增强。当调用@Transactional
注解的方法时,代理对象会通过TransactionInterceptor
拦截,触发事务管理逻辑。
2.2 事务管理器核心接口
PlatformTransactionManager
是事务管理的核心接口,里面定义了事务的提交和回滚。
事务的具体操作使用的是TransactionTemplate
,如下:
org.springframework.transaction.support.TransactionTemplate
此类也是编程式事务主要依赖的类。
具体的视线类包括:
org.springframework.jdbc.datasource.DataSourceTransactionManager
org.springframework.jdbc.support.JdbcTransactionManager
org.springframework.transaction.jta.JtaTransactionManager
其中JdbcTransactionManager
继承DataSourceTransactionManager
主要用于JDBC、Hibernate等框架,而JtaTransactionManager
则用于分布式事务。
2.3 事务的同步器
TransactionSynchronizationManager
用来绑定资源(如数据库连接)到当前线程,确保事务上下文的线程安全。
事务同步必须由事务管理器通过initSynchronization()
和clearSynchronization()
来激活和停用。这由AbstractReactiveTransactionManager
自动支持。
资源管理代码在此管理器处于活动状态时注册同步,可以通过isSynchronizationActive
进行检查。并立即执行资源清理。如果事务同步未处于活动状态,则没有当前事务,或者事务管理器不支持事务同步。
03 主要参数详解
@Transactional(
propagation = Propagation.REQUIRED, // 事务传播类型
isolation = Isolation.DEFAULT, // 隔离级别(默认取数据库配置)
timeout = 30, // 超时秒数(-1表示无限制)
readOnly = false, // 是否开启只读优化
rollbackFor = {SQLException.class}, // 指定回滚的异常类型
noRollbackFor = IOException.class // 指定不回滚的异常
)
3.1 事务的传播机制
propagation
,当嵌套事务产生时,就需要通过事务的传播类型控制。
例如A方法有事务,B方法也有事务,在A方法中调用B方法,应该以哪个为准呢?这时就需要用到事务的传播类型。
public enum Propagation {
/**
* 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。
* 这是最常见的,也是默认的。
*/
REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
/**
* 支持当前事务,效果和REQUIRED一直
* 如果当前没有事务,就以非事务方式执行。
*/
SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
/**
* 使用当前的事务,如果当前没有事务,就抛出异常。
*/
MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
/**
* 新建事务,如果当前存在事务,把当前事务挂起。
* 新事务的回滚会影响当前事务
* 当前事务的回滚不影响新事物
*/
REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
/**
* 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
* 发生异常影响挂起的事务
*/
NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),
/**
* 以非事务方式执行,如果当前存在事务,则抛出异常。
*/
NEVER(TransactionDefinition.PROPAGATION_NEVER),
/**
* 如果当前存在事务,则在嵌套事务内执行。
* 如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。
*/
NESTED(TransactionDefinition.PROPAGATION_NESTED);
}
REQUIRED:
简单理解,A方法调用B方法,A如果没有事务,B就新建一个事务,A有事务就是将两个合成一个事务。总之有它的地方就有事务。
SUPPORTS:
这个也很简单,就是有A调用B方法,A有事务,那就按照事务执行,A没有事务就不用事务执行。
MANDATORY:
表示没有事务就报错,终止运行。有事务则同REQUIRED
。
报错信息:
REQUIRES_NEW
新建事务,如果当前存在事务,把当前事务挂起。事务A调用事务B,事务B会新建事务,并将事务A挂起。B事务回滚会A事务也会回滚,A事务回滚,则不影响B事务。
结果会发现,两个事务都回滚了。
如果在A事务里面制造了异常,则B事务不会回滚。这里不再演示。
NOT_SUPPORTED
与SUPPORTS
相反,不管有没有事务,自己都会以非事务的方式运行。如果出现异常会影响调用方的事务。
NEVER
比NOT_SUPPORTED
更严格,自已以非事务方式运行,如果调用方有事务,就直接报错。
NESTED
和REQUIRES_NEW
有点相反,嵌套事务。如果之前存在一个事务,则创建一个嵌套事务。嵌套事务的回滚不会影响到父事务,但是父事务的回滚会影响到嵌套事务。
NESTED
只能针对子事务单独提交才会生效。如果由主事务提交,则子事务也会一同提交。这时候嵌套事务也会影响主事务。
如图,无论是异常在A事务还是B事务,都会让事务回滚,因为这都是主事务提交,所以会无效。
手动提交子事务,则会发现嵌套事务并不影响主事务。
结果:新增记录正常,而更新操作回滚。
3.2 事务的隔离级别
isolation
,在进行多个事务的并发执行时,如果不对它们进行隔离,则可能会产生一些问题。例如:脏读、不可重复读和幻读。而事务隔离级别就是用来解决这些问题的。
具体的隔离级别,后面我们会专门来讲。
public enum Isolation {
/**
* 默认的隔离级别,会获取数据库的隔离级别
*/
DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),
/**
* 读未提交
*/
READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),
/**
* 读已提交
*/
READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),
/**
* 可重复度
*/
REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),
/**
* 串行化
*/
SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);
}
3.3 事务的超时时间
timeout
,默认-1
永不超时,如果事务占用时间太长,就会长时间占用数据库连接,影响的数据库的操作。合理的事使用超时时间,可以有效的节省数据库连接的资源。
如果事务超时,则会抛出异常,事务回滚。
3.4 只读
readOnly
默认为false
。只能用于select
的查询,其他的更新、删除、插入会直接报错。
3.5 指定回滚的异常类型
rollbackFor
,默认都是不指定的。因为默认的异常类型是java.lang.RuntimeException
,其他类型的异常需要通过该参数指定。
3.6 指定不回滚的异常类型
noRollbackFor
,默认都是不指定的。相当于取反,指定哪类异常不会回滚。
04 事务失效的场景
4.1 数据库本身不支持事务
支不支持事务和数据库引擎有关系,如Mysql
的MyISAM
引擎不支持事务,而InnoDB
引擎则支持事务。
4.2 非public方法
源码里面指出,不允许非Public
的方法
4.3 rollbackFor
设置错误
默认是java.lang.RuntimeException
异常,如果是其他异常则就是就会出现事务失效
4.4 事务传播机制的错误使用
事务的传播机制,不同的传播机制对事务的影响都不同。
4.5 异常的内部捕捉
自己手动处理了异常,也会让事务失效
4.6 自调用
因为@Transactional
是基于AOP动态代理实现的。如果使用自调用,也就是this
,则会让事务失效。正确的处理办法:
// 获取当前类的代理
((T)AopContext.currentProxy()).method();
// 案例
UserInfo userInfo = new UserInfo();
userInfo.setId(14);
userInfo.setName("哪吒&敖丙");
((UserInfoService)AopContext.currentProxy()).update(userInfo);
4.7 事务方法被final、static修饰
因为@Transactional
是基于AOP动态代理实现的。也就是通过 JDK 动态代理或者 CGLIB,帮我们生成了代理类,在代理类中实现的事务功能。但如果某个方法用final
、static
修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。