解密Spring事务底层:你的@Transactional真的安全吗?

01 引言

上一期介绍了Spring数据的两种类型:声明式事务和编程式事务。编程式事务可以更加细粒度的控制事务,几乎没有什么苛刻的条件,因为其底层相当于捕捉到异常,手动回滚。

但是,声明式事务的使用虽然入门简单,一个简单的注解就可以开启事务。但是@Transactional的属性影响着事务的控制,事务的声明也是有条件的,稍不注意就可能导致事务失效。

我们一起来深入了解一下吧!

02 核心实现原理

Spring通过声明式事务管理简化了开发,其中@Transactional注解是实现事务控制的核心手段。相比传统的JDBC编程式事务,声明式事务通过AOP实现解耦,使业务代码更清晰。

2.1 AOP代理机制

通过动态代理(JDKCGLIB)在运行时对目标对象进行增强。当调用@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 数据库本身不支持事务

支不支持事务和数据库引擎有关系,如MysqlMyISAM引擎不支持事务,而InnoDB引擎则支持事务。

4.2 非public方法

源码里面指出,不允许非Public的方法

4.3 rollbackFor设置错误

默认是java.lang.RuntimeException异常,如果是其他异常则就是就会出现事务失效

4.4 事务传播机制的错误使用

事务的传播机制,不同的传播机制对事务的影响都不同。

4.5 异常的内部捕捉

自己手动处理了异常,也会让事务失效

4.6 自调用

因为@Transactional是基于AOP动态代理实现的。如果使用自调用,也就是this,则会让事务失效。正确的处理办法:

// 获取当前类的代理
((TAopContext.currentProxy().method();

// 案例
UserInfo userInfo = new UserInfo();
userInfo.setId(14);
userInfo.setName("哪吒&敖丙");
((UserInfoService)AopContext.currentProxy()).update(userInfo);

4.7 事务方法被final、static修饰

因为@Transactional是基于AOP动态代理实现的。也就是通过 JDK 动态代理或者 CGLIB,帮我们生成了代理类,在代理类中实现的事务功能。但如果某个方法用finalstatic 修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

智_永无止境

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值