嵌套事务、挂起事务,Spring 是怎样给事务又实现传播特性的?

开发 后端
Spring 做为风靡世界的Java 开源框架,发挥着举足轻重的作用。那你有没有想过, Spring 内部又是怎么样实现的事务呢?

[[334020]]

Spring 做为风靡世界的Java 开源框架,发挥着举足轻重的作用。那你有没有想过, Spring 内部又是怎么样实现的事务呢?

而且 在 Spring 之中除了设置事务的「隔离级别」之外,还可以额外配置事务的「传播特性」。你要知道,传播特性里,有两个家伙比较特别,一个PROPAGATION_REQUIRES_NEW ,还有一个是PROPAGATION_NESTED。你要知道,所谓的 REQUIRES_NEW,是会在方法级联调用的时候,会开启一个新的事务,同时挂起(suspend)当前事务;NESTED 代表的嵌套事务,则是在方法级联调用的时候,在嵌套事务内执行。

我们应该都知道,这些传播行为,是 Spring 独有的,和数据库没有一毛钱关系。那在底层 Spring 用了什么黑魔法吗?是怎么样做到挂起事务的,又是怎么样嵌套事务的呢?

咱们一起来揭秘。

毋庸置疑,数据的操作中,我们离不开事务。

银行的转帐操作中,我们相信如果一边扣款,那对方一定会收到,而不竹篮打水一场空

事务在很多场景中都发挥着关键作用。

咱们先以 MySQL 为例,来捋一捋数据库的事务,隔离级别。

然后再来看这些数据库的配置,特性,在 Spring 里是怎样做的事务对应的。

以及 Spring 所谓的传播特性,又是如何作用到数据库的事务的,怎样做到挂起事务,嵌套事务。

事务

事务是什么?

它是一级原子性的SQL操作,一个独立的工作单元。如果工作单元中的SQL语句执行成功,那会全部成功,一个失败就会全部回滚。

与数据库的事务同时为大众熟知的是ACID。即数据库的原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。就像应用内为了线程之间的互斥,线程安全等,需要做大量的工作,数据库为了 ACID,也做了许多的工作。

隔离级别

SQL 的标准中定义了四种隔离级别,规定了哪些是事务之间可见的,哪些是事务内可见的。

READ UNCOMMITTED (未提交读) --> 两个事务间,一个的事务还没提交,但被另一个看到了。

READ COMMITTED (提交读) --> 两个事务间,只有一个事务提交之后,另一个事务才能看到。

REPEATABLE READ (可重复读)--> 一个事务执行中,多次读到的结果是一样的,即使其他事务做了修改,也先「看不见」

SERIALIZABLE (可串行化)--> 最高隔离级别,强制事务串行执行。

由于这些隔离级别,会造成所谓的「脏读」、「幻读」、「不可重复读」等问题,所以需要根据具体的场景选择适用的。

如何设置隔离级别

对于 MySQL 的隔离级别,可以全局的,也可以设置 Session 级别的。

官方文档设置语法如下:

 

我们通过客户端连接到 MySQL Server 的时候,可以做一些配置,通过jdbc的 URL 你也能看的出来,这样设置的就是 Session 级别的。

参考上面的官方文档,设置 session 隔离级别的命令是它:

  1. set session transaction isolation level SERIALIZABLE

注意,MySQL 默认的隔离级别是 REPEATABLE READ,我的改过。同时,这里查询当前隔离级别的SQL,旧版本的是SELECT @@TX_ISOLATION;,新版本的是SELECT @@Transaction_ISOLATION; 注意区分。

 

Spring 事务又是怎么回事

看过了数据库的隔离级别之后,我们再来看 Spring 里的实现。

在 Spring 里,隔离级别的定义有五种,四个和数据库的一致,另外包含一个 DEFAULT,代表不具体设置,直接使用数据库定义的。

咱们看到数据库的隔离级别可以设置 GLOBAL 级别的,也可以设置 SESSION 级别的,每个 SESSION 则代表的是一个连接。而在Spring 内,我们又是通过一个个独立的「连接」来操作数据库,完成CRUD。到这儿咱们应该就能知道,在 Spring 层面的设置,是通过 session 级别的 隔离来设置的,从而和数据库里事务隔离级别做了对应来实现。

那传播特性又是怎么回事呢?既然数据库并不直接支持这样的特性,Spring 又根据不同的传播特性要求,来迂回实现了。

总结起来,对于级联操作中,如果是REQUIRED_NEW这种的情况,所谓的挂起当前事务,开启新的事务,是 Spring 又去申请了一个新的 Connection,这样就会对应到一个新的事务上,然后将这个连接绑定到当前线程,再继续执行;

对于嵌套事务,底层则是通过数据库的 SAVEPOINT 来实现所谓的「子事务」

 

如果你熟悉代码 DEBUG 过程中每个方法对应的 Frame,可以类比一下,如果执行失败回滚的时候,可以指定回滚到当前事务的某个 SAVEPOINT,不需要全部回滚。

在 Spring 层面,是通过 JDBC 3.0来实现的,看下面这段代码注释。

  1. * <p>This transaction manager supports nested transactions via the JDBC 3.0 
  2. * {@link java.sql.Savepoint} mechanism. The 
  3. * {@link #setNestedTransactionAllowed "nestedTransactionAllowed"} flag defaults 
  4. to "true", since nested transactions will work without restrictions on JDBC 
  5. * drivers that support savepoints (such as the Oracle JDBC driver). 

事务挂起的部分代码如下:

  1. /** 
  2.    * Suspend the given transaction. Suspends transaction synchronization first
  3.    * then delegates to the {@code doSuspend} template method. 
  4.    * @param transaction the current transaction object 
  5.    * (or {@code nullto just suspend active synchronizations, if any
  6.    * @return an object that holds suspended resources 
  7.    * (or {@code null} if neither transaction nor synchronization active) 
  8.    * @see #doSuspend 
  9.    * @see #resume 
  10.    */ 
  11.   @Nullable 
  12.   protected final SuspendedResourcesHolder suspend(@Nullable Object transaction) throws TransactionException { 
  13.     if (TransactionSynchronizationManager.isSynchronizationActive()) { 
  14.       List<TransactionSynchronization> suspendedSynchronizations = doSuspendSynchronization(); 
  15.       try { 
  16.         Object suspendedResources = null
  17.         if (transaction != null) { 
  18.           suspendedResources = doSuspend(transaction); 
  19.         } 
  20.         String name = TransactionSynchronizationManager.getCurrentTransactionName(); 
  21.         TransactionSynchronizationManager.setCurrentTransactionName(null); 
  22.         boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); 
  23.         TransactionSynchronizationManager.setCurrentTransactionReadOnly(false); 
  24.         Integer isolationLevel = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel(); 
  25.         TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(null); 
  26.         boolean wasActive = TransactionSynchronizationManager.isActualTransactionActive(); 
  27.         TransactionSynchronizationManager.setActualTransactionActive(false); 
  28.         return new SuspendedResourcesHolder( 
  29.             suspendedResources, suspendedSynchronizations, name, readOnly, isolationLevel, wasActive); 
  30.       } 
  31.   } 

逻辑在 doSuspend里,继续看

  1. @Override 
  2.   protected Object doSuspend(Object transaction) { 
  3.     DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction
  4.     txObject.setConnectionHolder(null); 
  5.     return TransactionSynchronizationManager.unbindResource(obtainDataSource()); 
  6.   } 
  7.  
  8.   @Override 
  9.   protected void doResume(@Nullable Object transaction, Object suspendedResources) { 
  10.     TransactionSynchronizationManager.bindResource(obtainDataSource(), suspendedResources); 
  11.   } 

然后对应的bind 和 unbind 操作,是在ThreadLocal 对象里,将资源对象绑定或移出当前线程对应的 resources 来实现的。

  1. private static final ThreadLocal<Map<Object, Object>> resources = 
  2.       new NamedThreadLocal<>("Transactional resources"); 
  3. /** 
  4.    * Bind the given resource for the given key to the current thread. 
  5.    * @param key the key to bind the value to (usually the resource factory) 
  6.    * @param value the value to bind (usually the active resource object) 
  7.    * @throws IllegalStateException if there is already a value bound to the thread 
  8.    * @see ResourceTransactionManager#getResourceFactory() 
  9.    */ 
  10.   public static void bindResource(Object key, Object value) throws IllegalStateException { 
  11.     Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); 
  12.     Assert.notNull(value, "Value must not be null"); 
  13.     Map<Object, Object> map = resources.get(); 
  14.     // set ThreadLocal Map if none found 
  15.     if (map == null) { 
  16.       map = new HashMap<>(); 
  17.       resources.set(map); 
  18.     } 
  19.     Object oldValue = map.put(actualKey, value); 
  20.     // Transparently suppress a ResourceHolder that was marked as void... 
  21.     if (oldValue instanceof ResourceHolder && ((ResourceHolder) oldValue).isVoid()) { 
  22.       oldValue = null
  23.     } 
  24.   } 

对比上面的说明和代码,这两个传播特性的区别也很明了了:

NESTED 的特性,本质上还是同一个事务的不同保存点,如果涉及到外层事务回滚,则内层的也将会被回滚;

 

REQUIRED_NEW 的实现对应的是一个新的事务,拿到的是新的资源,所以外层事务回滚时,不影响内层事务。

本文转载自微信公众号「Tomcat那些事儿」,可以通过以下二维码关注。转载本文请联系Tomcat那些事儿公众号。

 

 

责任编辑:武晓燕 来源: Tomcat那些事儿
相关推荐

2023-10-30 07:36:19

Spring事务传播机制

2024-04-17 08:11:01

数据库事务流程

2022-08-27 14:14:06

Spring事务开发

2024-01-04 12:48:00

Spring

2020-08-19 09:45:29

Spring数据库代码

2021-09-02 18:39:01

Spring隔离级别

2009-12-22 15:55:10

WCF事务

2009-07-20 18:11:52

iBATIS事务Spring

2023-11-02 07:52:30

Java工具

2023-05-06 07:29:49

Spring事务传播

2017-01-19 15:32:36

Java全局事务本地事务

2022-09-27 21:14:54

Spring事务传播机制

2022-10-08 00:24:40

嵌套事务加入事务事务

2009-06-22 09:01:57

Spring声明式事务

2022-04-26 21:49:55

Spring事务数据库

2018-11-16 15:35:10

Spring事务Java

2009-06-30 16:41:12

Hibernate的事

2018-07-17 10:58:45

数据库数据库事务隔离级别

2021-06-26 14:59:13

SpringTransaction执行

2021-09-07 10:33:42

MySQL事务隔离性
点赞
收藏

51CTO技术栈公众号