Spring事务专题

一,前言

对于一个应用而言,事务的使用基本是不可避免的。虽然Spring给我们提供了开箱即用的事务功能——@Transactional
但是,自带的事务功能却也存在控制粒度不够的缺点。更糟糕的是,@Transactional在某些情况下就失效了。可能一些读者baidu/google一下解决办法后,失效的问题确实解决了。但是由于不了解底层的原理,这样的问题可能在今后的工作中往复出现。
本文就为大家揭开@Transactional下的秘密。

二,Spring提供的事务API

Spring提供了很多关于事务的API。但是最为基本的就是PlatformTransactionManagerTransactionDefintionTransactionStatus

事务管理器 PlatformTransactionManager

PlatformTransactionManager是事务管理器的顶层接口。事务的管理是受限于具体的数据源的(例如,JDBC对应的事务管理器就是DatasourceTransactionManager),因此PlatformTransactionManager只规定了事务的基本操作:创建事务,提交事物和回滚事务。

public interface PlatformTransactionManager extends TransactionManager {

    /**
     * 打开事务
     */
	TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
			throws TransactionException;

	/**
	 * 提交事务
	 */
	void commit(TransactionStatus status) throws TransactionException;

	/**
	 * 回滚事务
	 */
	void rollback(TransactionStatus status) throws TransactionException;
}

同时为了简化事务管理器的实现,Spring提供了一个抽象类AbstractPlatformTransactionManager,规定了事务管理器的基本框架,仅将依赖于具体平台的特性作为抽象方法留给子类实现。

事务状态 TransactionStatus

事务状态是我对TransactionStatus这个类的直译。其实我觉得这个类可以直接当作事务的超集来看(包含了事务对象,并且存储了事务的状态)。PlatformTransactionManager.getTransaction()时创建的也正是这个对象。
这个对象的方法都和事务状态相关:

public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {

	/**
	 * 是否有Savepoint Savepoint是当事务回滚时需要恢复的状态
	 */
	boolean hasSavepoint();

	/**
	 * flush()操作和底层数据源有关,并非强制所有数据源都要支持
	 */
	@Override
	void flush();

}

此外,TransactionStatus还从父接口中继承了其他方法,都归总在下方:


	/**
	 * 是否是新事务(或是其他事务的一部分)
	 */
	boolean isNewTransaction();

	/**
	 * 设置rollback-only 表示之后需要回滚
	 */
	void setRollbackOnly();

	/**
	 * 是否rollback-only
	 */
	boolean isRollbackOnly();

	/**
	 * 判断该事务已经完成
	 */
	boolean isCompleted();
	
	
	/**
	 * 创建一个Savepoint
	 */
	Object createSavepoint() throws TransactionException;

	/**
	 * 回滚到指定Savepoint
	 */
	void rollbackToSavepoint(Object savepoint) throws TransactionException;

	/**
	 * 释放Savepoint 当事务完成后,事务管理器基本上自动释放该事务所有的savepoint
	 */
	void releaseSavepoint(Object savepoint) throws TransactionException;

事务属性的定义 TransactionDefinition

TransactionDefinition表示一个事务的定义,将根据它规定的特性去开启事务。
事务的传播等级和隔离级别的常量同样定义在这个接口中。

	/**
	 * 返回事务的传播级别
	 */
	default int getPropagationBehavior() {
		return PROPAGATION_REQUIRED;
	}

	/**
	 * 返回事务的隔离级别
	 */
	default int getIsolationLevel() {
		return ISOLATION_DEFAULT;
	}

	/**
	 * 事务超时时间
	 */
	default int getTimeout() {
		return TIMEOUT_DEFAULT;
	}

	/**
	 * 是否为只读事务(只读事务在处理上能有一些优化)
	 */
	default boolean isReadOnly() {
		return false;
	}

	/**
	 * 返回事务的名称
	 */
	@Nullable
	default String getName() {
		return null;
	}


	/**
	 * 默认的事务配置
	 */
	static TransactionDefinition withDefaults() {
		return StaticTransactionDefinition.INSTANCE;
	}

编程式使用Spring事务

有了上述这些API,就已经可以通过编程的方式实现Spring的事务控制了。
但是Spring官方建议不要直接使用PlatformTransactionManager这一偏低层的API来编程,而是使TransactionTemplateTransactionCallback这两个偏向用户层的接口
示例代码如下:

        //设置事务的各种属性;可以猜测TransactionTemplate应该是实现了TransactionDefinition
        transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
        transactionTemplate.setTimeout(30000);
        
        //执行事务 将业务逻辑封装在TransactionCallback中
        transactionTemplate.execute(new TransactionCallback<Object>() {
            @Override
            public Object doInTransaction(TransactionStatus transactionStatus) {
                    //....   业务代码
            }
        });

三,Spring注解式事务

上文中,我们从编程式事务了解了Spring事务API的基本使用方式。现在,我们在回到Spring注解式事务中,验证下注解式事务是否就是通过以上这些方式隐藏了具体的事务控制逻辑。

@EnableTransactionManagement

@EnableTransactionManagement是开启注解式事务的事务。如果注解式事务真的有玄机,那么@EnableTransactionManagement就是我们揭开秘密的突破口。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {

	/**
	 * 用来表示默认使用JDK Dynamic Proxy还是CGLIB Proxy
	 */
	boolean proxyTargetClass() default false;

	/**
	 * 表示以Proxy-based方式实现AOP还是以Weaving-based方式实现AOP
	 */
	AdviceMode mode() default AdviceMode.PROXY;

	/**
	 * 顺序
	 */
	int order() default Ordered.LOWEST_PRECEDENCE;

}

@EnableTransactionManagement注解看起来并没有特别之处,都是一些属性的配置。但它却通过@Import引入了另一个配置TransactionManagentConfigurationSelector

TransactionManangementConfigurationSelector

在Spring中,Selector通常都是用来选择一些Bean,向容器注册BeanDefinition的(严格意义上Selector仅时选择过程,注册的具体过程是在ConfigurationClasspathPostProcessor解析时,调用ConfigurationClassParser触发)。
主要的逻辑就是根据代理模式,注册不同的BeanDefinition。
对Proxy的模式而言,注入的有两个:

  • AutoProxyRegistrar
  • ProxyTransactionManagementConfiguration

AutoProxyRegistrar

Registrar同样也是用来向容器注册Bean的,在Proxy的模式下,它会调用AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);向容器中注册InfrastructureAdvisorAutoProxyCreator。而这个类就是我们上文提到的AbstractAdvisorAutoProxyCreator的子类。
从而,我们完成了我们的第一个条件——AOP代理。

ProxyTransactionManagementConfiguration

ProxyTransactionManagementConfiguration是一个配置类,如果算上其继承的父类,一共是声明了四个类:

  1. TransactionalEventListenerFactory
  2. BeanFactoryTransactionAttributeSourceAdvisor
  3. TransactionAttributeSource
  4. TransactionInterceptor

后三个类相对比较重要,我们一一分析。

BeanFactoryTransactionAttributeSourceAdvisor

从名字看就知道这是一个Advisor,那么它身上应该有Pointcut和Advise。
其中的Pointcut是TransactionAttributeSourcePointcut,主要是一些filter和matches之类的方法,用来匹配被代理类。
而Adivise就是我们之后要介绍的TransactionInterceptor

TransactionAttributeSource

TransactionAttributeSource只是一个接口,扩展了TransactionDefinition,增加了isCandidateClass()的方法(可以用来帮助Pointcut匹配)。
这里使用的具体实现是AnnotationTransactionAttributeSource。因为注解式事务候选类(即要被代理的类)是通过@Transactional注解标识的,并且所有的事务属性也都来自@Transactional注解。

TransactionInterceptor

刚才我们说了,TransactionInterceptor就是我们找的Advise。
这个类稍微复杂一点,首先根据事务处理相关的逻辑都放在了其父类TransactionAspectSupport中。此外,为了适配动态代理的反射调用(两种代理方式),实现了MethodInterceptor接口。
也就是说,反射发起的入口是MethodInterceptor.invoke(),而反射逻辑在TransactionAspectSupport.invokeWithinTransaction()中。
我们可以简单看invokeWithTransaction()方法中的部分代码:

	@Nullable
	protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
			final InvocationCallback invocation) throws Throwable {

		
		TransactionAttributeSource tas = getTransactionAttributeSource();
		final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
		final TransactionManager tm = determineTransactionManager(txAttr);

		//省略部分代码
        
        //获取事物管理器
		PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
		final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

		if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
			// 打开事务(内部就是getTransactionStatus的过程)
			TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

			Object retVal;
			try {
				// 执行业务逻辑 invocation.proceedWithInvocation();
			}
			catch (Throwable ex) {
				// 异常回滚
				completeTransactionAfterThrowing(txInfo, ex);
				throw ex;
			}
			finally {
				cleanupTransactionInfo(txInfo);
			}

			//省略部分代码
            
            //提交事物
			commitTransactionAfterReturning(txInfo);
			return retVal;
		}

 四,事务失效的情况

1.访问权限问题

众所周知,java 的访问权限主要有四种:private、default、protected、public,它们的权限从左到右,依次变大。

但如果我们在开发过程中,把某些事务方法,定义了错误的访问权限,就会导致事务功能出问题

spring 要求被代理方法必须是public的。

说白了,在AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute方法中有个判断,如果目标方法不是 public,则TransactionAttribute返回 null,即不支持事务

2. 方法用 final 修饰 

有时候,某个方法不想被子类重写,这时可以将该方法定义成 final 的。普通方法这样定义是没问题的,我们可以看到 add 方法被定义成了final的,这样会导致事务失效

如果你看过 spring 事务的源码,可能会知道 spring 事务底层使用了 aop,也就是通过 jdk 动态代理或者 cglib,帮我们生成了代理类,在代理类中实现的事务功能

但如果某个方法用 final 修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。

3.方法内部调用

有时候我们需要在某个 Service 类的某个方法中,调用另外一个事务方法

由此可见,在同一个类中的方法直接内部调用,会导致事务失效。

那么问题来了,如果有些场景,确实想在同一个类的某个方法中,调用它自己的另外一个方法,该怎么办呢?

新加一个 Service 方法

这个方法非常简单,只需要新加一个 Service 方法,把 @Transactional 注解加到新 Service 方法上,把需要事务执行的代码移到新方法中。

在该 Service 类中注入自己

如果不想再新加一个 Service 类,在该 Service 类中注入自己也是一种选择

通过 AopContent 类

在该 Service 类中使用 AopContext.currentProxy() 获取代理对象。

4.未被 spring 管理


在我们平时开发过程中,有个细节很容易被忽略,即使用 spring 事务的前提是:对象要被 spring 管理,需要创建 bean 实例。

通常情况下,我们通过 @Controller、@Service、@Component、@Repository 等注解,可以自动实现 bean 实例化和依赖注入的功能。

5.多线程调用

在实际项目开发中,多线程的使用场景还是挺多的。如果 spring 事务用在多线程场景中,会有问题吗?

如果看过 spring 事务源码的朋友,可能会知道 spring 的事务是通过数据库连接来实现的。当前线程中保存了一个 map,key 是数据源,value 是数据库连接。我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。

6.表不支持事务

众所周知,在 mysql5 之前,默认的数据库引擎是myisam

它的好处就不用多说了:索引文件和数据文件是分开存储的,对于查多写少的单表操作,性能比 innodb 更好。

有些老项目中,可能还在用它。

7.未开启事务

有时候,事务没有生效的根本原因是没有开启事务。

8,事务不回滚

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Nathaniel333

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

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

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

打赏作者

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

抵扣说明:

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

余额充值