Spring 事务抽象带来的思考


一般来说,在一个复杂系统中,我们会集成很多的 data access frameworks,也就是所谓的数据持久层框架,例如通过 HibernateMyBatis 操作 Mysql 这样的关系型数据库,亦或是通过 JedisRedission 这样的客户端组件操作 Rediskey-value 数据库。总而言之,一旦涉及到数据层面的操作,就会引出事务问题。因此我们需要一个统一的事务标准帮我去处理这类问题,事务是一个抽象概念,无论是本地事务,或是分布式事务,从抽象概念上他们是同一类问题,我们需要从更高的角度去看待他们,建立抽象概念将事务问题抽象化,简单化,再针对具体的问题去实现这些抽象概念,这是一种合理的思维方式。而 Spring Framework 为我们建立了这样的模型,或许可以给我们一些启发。

Spring Framework 中的抽象模型


当我们使用不同的数据持久层框架时,由于他们 API 设计各异,此时如果没有一个统一的事务模型,而是针对各个框架 API 去做特殊处理,带来的代码复杂度是不可想象的。更大的问题是这些事务代码会严重地侵入我们的业务代码,与业务代码强耦合在一起,如果未来我们需要升级或更换持久层框架,那么升级成本将是巨大的。因此一个事务管理的抽象模型是必要,理解 Spring Framework 中的 Transaction Abstraction 可以帮助我们更好地理解事务问题。

TransactionManager 事务管理器

理解 Spring 事务管理的关键是理解 TransactionManager 事务管理器,这是事务抽象的核心 API,而 PlatformTransactionManager 是它的默认实现,Spring Framework 5.2 版本新增了适配响应式编程的实现 ReactiveTransactionManager,此处暂且不进行论述。

public interface PlatformTransactionManager extends TransactionManager {

    TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;

    void commit(TransactionStatus status) throws TransactionException;

    void rollback(TransactionStatus status) throws TransactionException;
    
}
  • getTransaction 方法用以获取一个 TransactionStatus 事务状态,这可能意味着开启一个新的事务,也可能代表一个已存在事务的延续。这需要取决于 TransactionDefinition 对象中对事务的定义,例如的事务的隔离级别,传播属性等。

  • commit 方法代表事务执行成功后提交,rollback 方法事务执行失败后回滚,这两个方法无疑具有很高的抽象性,因为不同的持久化框架它们提交回滚事务的 API 各不相同,因此这两个方法的实现必然具有很大差异,但是针对提交回滚的操作 Spring 依赖抽取出了一定的生命周期,例如在 AbstractPlatformTransactionManagerSpring 利用模板方法模式规范了事务提交回滚的基本模板,而子类实现只需填充其中的生命周期即可。

  • TransactionManager 是一个典型的 service provider interface,也就是所谓的 SPI 接口,它的实现是多样的,需要注意的是这些方法都可能会抛出 TransactionException,这是一个 RuntimeException,一般来讲事务性的异常对业务是致命性的,但也不排除少数业务场景中能从事务异常去恢复过来,因此 Spring 将事务异常声明为 unchecked exception,并不强迫开发者去捕获处理。

TransactionDefinition 事务定义

我们知道事务与事务是不同的,它需要自身的属性,例如事务的隔离级别和传播属性,这些事务的特性我们称之为事务的定义,不同的事务定义决定了事务在执行过程中的策略,例如 A 事务修改的数据能否被 B 事务访问到,在事务中执行额外操作时是否应该开启新的事务,这些因素都是我们在做事务管理时必须考虑的。

public interface TransactionDefinition {

	int PROPAGATION_REQUIRED = 0;

	int PROPAGATION_SUPPORTS = 1;

	int PROPAGATION_MANDATORY = 2;

	int PROPAGATION_REQUIRES_NEW = 3;

	int PROPAGATION_NOT_SUPPORTED = 4;

	int PROPAGATION_NEVER = 5;

	int PROPAGATION_NESTED = 6;

	int ISOLATION_DEFAULT = -1;

	int ISOLATION_READ_UNCOMMITTED = 1;

	int ISOLATION_READ_COMMITTED = 2;

	int ISOLATION_REPEATABLE_READ = 4;

	int ISOLATION_SERIALIZABLE = 8;

	int TIMEOUT_DEFAULT = -1;

	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;
	}

}
  • Propagation:事务传播属性,对于在一个事务作用域范围内的所有代码而言,如果当前执行的代码已经处于一个事务上下文中,此时再次执行一个事务方法时,事务的传播属性将决定是否应该开启一个新的事务或是延续已经存在的事务,Spring 几乎提供了所有已知的传播属性,因此根据业务场景去设定恰当的传播属性是必要的。

  • Isolation:事务隔离级别,一般来讲分为四种,READ_UNCOMMITTED 读未提交,READ_COMMITTED 读已提交,REPEATABLE_READ 可重复读,SERIALIZABLE 串行化,隔离级别决定了一个事务与其他事务的隔离程度,隔离程度越高,并发时数据的安全性就更高,同时也意味着系统的并发吞吐量更低。

  • Timeout 事务超时时间,决定一个事务允许执行的最大时间,一旦超过这个阈值,事务将会被自动回滚。

  • Read-only status 事务只读状态,用于标示某个只做读操作而不对数据进行修改,在某些持久层框架中,这个状态是有意义的,可以帮助框架针对只读操作进行性能优化。

TransactionStatus 事务状态

在一个事务的执行过程中,必然存在一些关键的执行节点,这些节点意味着事务状态的改变,整个过程我们可以称之为事务的生命周期,而在这个过程中我们需要一个对象去记录事务的状态,Spring 抽象出了 TransactionStatus 接口用以提供事务状态的统一 API

public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {

    @Override
    boolean isNewTransaction();

    boolean hasSavepoint();

    @Override
    void setRollbackOnly();

    @Override
    boolean isRollbackOnly();

    void flush();

    @Override
    boolean isCompleted();
    
}

通过 TransactionStatus 可以获取到事务的一些基本状态,例如是否是一个新开启的事务,事务是否已被提交,事务是否已被回滚。值得注意的是其中还包含了创建 save point 以及回滚到某一 save point 的操作,在部分业务场景中我们需要 save point 这样的支持,有时我们不希望回滚整个事务,而是希望回滚到事务中的某个阶段,因此我们需要在事务执行过程中记录一些保存点,方便我们将事务回退至这些节点。

Declarative Transaction Management 声明式事务


声明式事务是 Spring 最为推崇的一种运用事务管理的方式,因为这种方式拥有最低的代码侵入性。Spring 声明式事务的本质是 AOP 面向切面的编程思想,通过 Java 注解驱动提供方法级别的事务支持。在启用 Spring 声明式事务的前提下,我们只需要在方法上添加 @Transactional 注解,Spring Framework 中的切面会自动切入该方法,织入事务管理的相关逻辑。需要注意的是这种方式仅能保证当前线程中的数据操作处于事务管理中,如果在当前线程中 fork 一个子线程,那么子线程中的数据操作并不处于当前事务的控制中。

基于 AOP 的事务管理带来的好处

面向切面的程序设计(Aspect-oriented programmingAOP,又译作面向方面的程序设计、剖面导向程序设计)是计算机科学中的一种程序设计思想,旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。通过在现有代码基础上增加额外的通知(Advice)机制,能够对被声明为“切点(Pointcut)”的代码块进行统一管理与装饰,如“对所有方法名以‘set*’开头的方法添加后台日志”。该思想使得开发人员能够将与代码核心业务逻辑关系不那么密切的功能(如日志功能)添加至程序中,同时又不降低业务代码的可读性。面向切面的程序设计思想也是面向切面软件开发的基础。

以上是维基百科对于 AOP 面向切面编程的解释。曾经看到这样一句话,软件架构的本质是分离业务逻辑和技术细节,我想这也正是 AOP 思想致力于达到的目的。一个优秀的软件架构应该做到关注点分离,我的理解是正确的代码做正确的事。对于任何一个系统,随着时间的推移,熵增是不可避免的,万物都是从有序演化为无序,很多程序员戏称的“屎山”就是这样形成的,但是良好的架构设计以及模块划分应该做到把系统的复杂度始终控制在可接受范围内。

业务逻辑与技术细节

AOP 的角度去看,一段业务逻辑代码中总是分布着很多 Pointcut 切入点,我们需要向其中嵌入技术细节,所谓的技术细节,可能是日志记录,事务管理,异步操作,限流降级等无关乎于业务本身的逻辑。此时如果我们将这些技术细节直接穿插到业务代码中,这具有太强的侵入性,无疑是一种糟糕的设计。而 AOP 旨在将技术细节抽象为一个个 Advice 通知,PointcutAdvice 共同组成一个 Aspect 切面,完成对业务代码的逻辑织入,以完成软件架构设计中的理想状态,关注点分离。

@EnableTransactionManagement

@EnableTransactionManagement 注解用于启用 Spring 声明式事务。

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

	boolean proxyTargetClass() default false;

	AdviceMode mode() default AdviceMode.PROXY;

	int order() default Ordered.LOWEST_PRECEDENCE;

}
  • mode 属性代表 Spring 声明式事务所采取的切面模式,默认为 PROXY 代理模式,在代理模式下,Spring AOP 会为我们的目标对象建立代理对象,在实际运行过程中,当我们调用目标方法时,实际执行的是代理对象中的对应方法,而代理对象中已经被织入了 Advice 逻辑,这是 AOP 的基本实现方式。需要注意的是这种模式下类内自调用切面是不会生效的,因此在 PROXY 模式下声明式事务无法应用于类内的方法调用。如果我们实在想将切面应用于类内方法调用,Spring 为我们提供了两种方式,一种是通过 AopContext#currentProxy() 方法获取当前对象的代理对象,当然这种方式不太优雅,因为需要在业务代码中关注技术细节,不符合关注点分离的原则。另一种方式便是将 mode 切换为 ASPECTJ 模式,AspectJ 是一个面向切面的框架,它扩展了Java语言。AspectJ 定义了 AOP 语法,它有一个专门的编译器用来生成遵守 Java 字节编码规范的 Class 文件。因为 ASPECTJ 技术可以直接改变 Class 字节码,在该模式下切面逻辑的应用不会受到任何影响。

  • proxyTargetClass 属性用于决定 Spring 是否面向 Class 类生成代理对象,默认为 false,此时如果我们的目标类实现了某 Interface,那么 Spring 会基于 JDK 动态代理生成接口类型的代理对象,反之 Spring 会基于 CGLIB 技术生成目标 Class 类型的代理对象。当然我们可以将该属性置为 true 强制 Spring 使用 CGLIB 代理。

@Transactional

在我们使用 Spring 声明式事务时,由于我们不会直接依赖于 TransactionDefinition 这样的 API,因此 @Transactional 注解就需要承载事务的定义信息。

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {

	@AliasFor("transactionManager")
	String value() default "";

	@AliasFor("value")
	String transactionManager() default "";

	Propagation propagation() default Propagation.REQUIRED;

	Isolation isolation() default Isolation.DEFAULT;

	int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

	boolean readOnly() default false;

	Class<? extends Throwable>[] rollbackFor() default {};

	String[] rollbackForClassName() default {};

	Class<? extends Throwable>[] noRollbackFor() default {};

	String[] noRollbackForClassName() default {};

}

我们可以看到 @Transactional 中指定的默认事务传播属性为 PROPAGATION_REQUIRED,一般来说这种传播属性最符合我们常规的业务需求。

本图取自官方文档

在该模式下,当首个事务性方法没调用时,Spring 的事务切面会为我们开启事务,此时在方法内调用其他的事务性方法不会再开启新的事务,而是依然处于上下文事务的管控中,在事务范围内的特定异常都出触发事务回滚。

另一个比较常用的传播属性是 PROPAGATION_REQUIRES_NEW,当然 Spring 中一共定义了七种事务的传播属性,此处不逐个论述。

本图取自官方文档

在该模式下每一个事务性方法的执行都意味着开启一个全新的独立事务,每一个事务的提交和回滚都是独立的,内层的事务的回滚不会触发外层事务的回滚。

JDBC 事务管理


JDBC 事务管理为例,当我们的 application 上下文中存在 DataSource 数据源时,Spring 会自动为我们装配 DataSourceTransactionManager 应用于 JDBC 的事务管理。

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ JdbcTemplate.class, PlatformTransactionManager.class })
@AutoConfigureOrder(Ordered.LOWEST_PRECEDENCE)
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceTransactionManagerAutoConfiguration {

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnSingleCandidate(DataSource.class)
	static class DataSourceTransactionManagerConfiguration {

		@Bean
		@ConditionalOnMissingBean(PlatformTransactionManager.class)
		DataSourceTransactionManager transactionManager(DataSource dataSource,
				ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
			DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
			transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(transactionManager));
			return transactionManager;
		}

	}

}

我们可以看到 Spring 会收集上下文中的所有 PlatformTransactionManagerCustomizer 实例,用于修饰自动装配的 PlatformTransactionManagerCustomizerSpring 框架中的一种普遍性设计,主要提供开发者扩展框架默认对象的入口,这种设计非常符合开闭原则。因为在非必要条件下,Spring 并不倾向于引导开发者覆盖框架中内建的默认 bean,而是建立开发者使用 Spring 提供的 Customizer 自定义扩展器,一般来说,使用 Customizer 完全可以满足我们的对象自定义需求,同时我们无需关注对象的创建过程,这是一种有效的关注点分离。

获取事务对象

public class DataSourceTransactionManager extends AbstractPlatformTransactionManager
		implements ResourceTransactionManager, InitializingBean {
	// ...
	@Override
	protected Object doGetTransaction() {
		DataSourceTransactionObject txObject = new DataSourceTransactionObject();
		txObject.setSavepointAllowed(isNestedTransactionAllowed());
		ConnectionHolder conHolder =
				(ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource());
		txObject.setConnectionHolder(conHolder, false);
		return txObject;
	}

	@Override
	protected boolean isExistingTransaction(Object transaction) {
		DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
		return (txObject.hasConnectionHolder() && txObject.getConnectionHolder().isTransactionActive());
	}
	// ...
}

Spring 通过 Template Method Pattern 模仿方法模式将事务抽象逻辑与具体数据层实现逻辑分离,因此 DataSourceTransactionManager 中只需关注细节实现,而无需关注诸如事务传播属性这样的抽象概念。在获取事务对象的实现中,DataSourceTransactionObject 对象被创建,此时如果当前线程已经持有了数据库连接 ConnectionHolder,则会被设值到事务对象中,因为在同一条调用链中,即使需要开启多个事务,也可以共用一个数据库连接。因此 isExistingTransaction 在判断当前事务对象是否已经处于一个事务中时,只需要判断该事务对象是否持有了一个数据库连接。

开启事务

public class DataSourceTransactionManager extends AbstractPlatformTransactionManager
		implements ResourceTransactionManager, InitializingBean {
	// ...
	@Override
	protected void doBegin(Object transaction, TransactionDefinition definition) {
		DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
		Connection con = null;

		try {
			if (!txObject.hasConnectionHolder() ||
					txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
				Connection newCon = obtainDataSource().getConnection();
				if (logger.isDebugEnabled()) {
					logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
				}
				txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
			}

			txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
			con = txObject.getConnectionHolder().getConnection();

			Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
			txObject.setPreviousIsolationLevel(previousIsolationLevel);
			txObject.setReadOnly(definition.isReadOnly());

			// Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
			// so we don't want to do it unnecessarily (for example if we've explicitly
			// configured the connection pool to set it already).
			if (con.getAutoCommit()) {
				txObject.setMustRestoreAutoCommit(true);
				if (logger.isDebugEnabled()) {
					logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
				}
				con.setAutoCommit(false);
			}

			prepareTransactionalConnection(con, definition);
			txObject.getConnectionHolder().setTransactionActive(true);

			int timeout = determineTimeout(definition);
			if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
				txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
			}

			// Bind the connection holder to the thread.
			if (txObject.isNewConnectionHolder()) {
				TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
			}
		}

		catch (Throwable ex) {
			if (txObject.isNewConnectionHolder()) {
				DataSourceUtils.releaseConnection(con, obtainDataSource());
				txObject.setConnectionHolder(null, false);
			}
			throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
		}
	}
	// ...
}
  • 在事务开启过程中,如果当前事务是该线程调用链中的首个事务,那么 txObject 还未持有数据库连接,此时需要从 DataSource 数据源中获取一个 connection 连接。

  • 确定事务的一些基本属性,例如事务隔离级别,是否只读,事务超时时间等。

  • 将当前事务持有的 connection 缓存起来,以供后续使用,主要基于 ThreadLocal 实现。

  • 如果数据库连接被设置为了自动提交,Spring 会将其切换为手动提交,因为在一些 JDBC 驱动中,自动提交会带来较大的资源开销。

事务的中断与恢复

public class DataSourceTransactionManager extends AbstractPlatformTransactionManager
		implements ResourceTransactionManager, InitializingBean {
	// ...
	@Override
	protected Object doSuspend(Object transaction) {
		DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
		txObject.setConnectionHolder(null);
		return TransactionSynchronizationManager.unbindResource(obtainDataSource());
	}

	@Override
	protected void doResume(@Nullable Object transaction, Object suspendedResources) {
		TransactionSynchronizationManager.bindResource(obtainDataSource(), suspendedResources);
	}
	// ...
}

JDBC 事务管理对于事务的中断恢复的实现较为简单,通过暂时从事务对象和线程上下文中收回连接来中断事务,通过向线程上下文中归还连接来恢复事务。

事务的提交与回滚

public class DataSourceTransactionManager extends AbstractPlatformTransactionManager
		implements ResourceTransactionManager, InitializingBean {
	// ...
	@Override
	protected void doCommit(DefaultTransactionStatus status) {
		DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
		Connection con = txObject.getConnectionHolder().getConnection();
		if (status.isDebug()) {
			logger.debug("Committing JDBC transaction on Connection [" + con + "]");
		}
		try {
			con.commit();
		}
		catch (SQLException ex) {
			throw new TransactionSystemException("Could not commit JDBC transaction", ex);
		}
	}

	@Override
	protected void doRollback(DefaultTransactionStatus status) {
		DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
		Connection con = txObject.getConnectionHolder().getConnection();
		if (status.isDebug()) {
			logger.debug("Rolling back JDBC transaction on Connection [" + con + "]");
		}
		try {
			con.rollback();
		}
		catch (SQLException ex) {
			throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
		}
	}
	// ...
}

对于事务的提交和回滚,只需将调用委派给 Connection 对象即可。我们可以从 JDBC 事务管理的实现中看到 Spring Framework 只是针对事务管理流程进行抽象,而实际的事务实现既然委托给底层的 JDBC 驱动执行,实现了良好的关注点分离。这也是我们在系统设计时应该追求达到的理想状态,也就是分离业务逻辑和技术细节,当然这需要开发者拥有良好的抽象性思维,这也是一个代码工匠持久学习和追求的方向。


文章作者: Ethan Zhang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Ethan Zhang !
评论
 上一篇
浅谈微服务架构设计 浅谈微服务架构设计
“架构”一个抽象而看似高端的词汇,我们难以给它一个明确的定义。人们发明了很多描述架构的概念,单体架构、SOA架构、六边形架构、洋葱圈架构、微服务架构等。这些不同概念的定义和思想相互渗透影响,没有清晰的边界。在实践中一个系统往往是参考多种思
2021-03-02 Ethan Zhang
下一篇 
记一次 Redis 上线事故 记一次 Redis 上线事故
Redis 作为最被广泛使用的缓存中间件之一,虽然市面上大量开源的 Redis 开源客户端提供的 API 是非常简洁的,但如果我们需要将它用得游刃有余,使其真正高效地服务于我们的应用服务,那么我们就需要深入理解 Redis 的存储结构和运
2020-12-23 Ethan Zhang