
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.DataSourceTransactionManagerorg.springframework.jdbc.support.JdbcTransactionManagerorg.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 修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。
173万+

被折叠的 条评论
为什么被折叠?



