`

【转】脱离 Spring 实现复杂嵌套事务,之一(必要的概念)

 
阅读更多

   写这篇博文的目的首先是与大家分享一下如何用更轻量化的办法去实现 Spring 那种完善的事务控制。

为什么需要嵌套事务?

    我们知道,数据库事务是为了保证数据库操作原子性而设计的一种解决办法。例如执行两条 update 当第二条执行失败时候顺便将前面执行的那条一起回滚。

    这种应用场景比较常见,例如银行转帐。A账户减少的钱要加到B账户上。这两个SQL操作只要有一个失败,必须一起撤销。

    但是通常银行转帐业务无论是否操作成功都会忘数据库里加入系统日志。如果日志输出与账户金额调整在一个事务里,一旦事务回滚日志也会跟着一起消失。这时候就需要嵌套事务。

时间 事务
T1 开始事务
T2 记录日志...
T3 转账500元
T4 记录日志...
T5 递交事务

为什么有了嵌套事务还需要独立事务?

    假设现在银行需要知道当前正在进行转账的实时交易数。

    我们知道一个完整的转账业务会记录两次日志,第一次用以记录是什么业务,第二次会记录这个业务总共耗时。因此完成这个功能时我们只需要查询还未进行第二次记录的那些交易日志即可得出结果。

时间 事务1 事务2
T1 开始事务  
T2 记录日志...  
T3   开始子事务
T4   转账500元
T5   递交子事务
T6 记录日志...  
T7 递交事务  

    分析一下上面这种嵌套事务就知道不会得出正确的结果,首先第一条日志会被录入数据库的先决条件是转账操作成功之后的递交事务。

    如果事务递交了,交易也就完成了。这样得出的查询结果根本不是实时数据。因此嵌套事务解决方案不能满足需求。倘若日志输出操作使用的是一个全新的事务,就会保证可以查询到正确的数据。(如下)。

时间 事务1 事务2
T1 开始事务 开始事务
T2 记录日志...  
T3 递交事务  
T4   转账500元
T5 开始事务  
T6 记录日志...  
T7 递交事务 递交事务

Spring 提供的几种事务控制

1.PROPAGATION_REQUIRED(加入已有事务)
    尝试加入已经存在的事务中,如果没有则开启一个新的事务。

2.RROPAGATION_REQUIRES_NEW(独立事务)
    挂起当前存在的事务,并开启一个全新的事务,新事务与已存在的事务之间彼此没有关系。

3.PROPAGATION_NESTED(嵌套事务)
    在当前事务上开启一个子事务(Savepoint),如果递交主事务。那么连同子事务一同递交。如果递交子事务则保存点之前的所有事务都会被递交。

4.PROPAGATION_SUPPORTS(跟随环境)
    是指 Spring 容器中如果当前没有事务存在,就以非事务方式执行;如果有,就使用当前事务。

5.PROPAGATION_NOT_SUPPORTED(非事务方式)
    是指如果存在事务则将这个事务挂起,并使用新的数据库连接。新的数据库连接不使用事务。

6.PROPAGATION_NEVER(排除事务)
    当存在事务时抛出异常,否则就已非事务方式运行。

7.PROPAGATION_MANDATORY(需要事务)
    如果不存在事务就抛出异常,否则就已事务方式运行。

事务管理器API接口

    对于开发者而言,对事务管理器的操作只会涉及到“get”、“commit”、“rollback”三个基本操作。因此数据库事务管理器的接口相对简单。如下:

/**
 * 数据源的事务管理器。
 * @version : 2013-10-30
 * @author 赵永春(zyc@hasor.net)
 */
public interfaceTransactionManager{
    //开启事务,使用不同的传播属性来创建事务。
    public TransactionStatus getTransaction(TransactionBehavior behavior);
    //递交事务
    publicvoidcommit(TransactionStatus status)throws SQLException;
    //回滚事务
    publicvoidrollBack(TransactionStatus status)throws SQLException;
}

取得的事务状态使用下面这个接口进行封装:

/**
 * 表示一个事务状态
 * @version : 2013-10-30
 * @author 赵永春(zyc@hasor.net)
 */
public interfaceTransactionStatus{
    //获取事务使用的传播行为
    public TransactionBehavior getTransactionBehavior();
    //获取事务的隔离级别
    public TransactionLevel getIsolationLevel();
    //
    //事务是否已经完成,当事务已经递交或者被回滚就标志着已完成
    publicbooleanisCompleted();
    //是否已被标记为回滚,如果返回值为 true 则在commit 时会回滚该事务
    publicbooleanisRollbackOnly();
    //是否为只读模式。
    publicbooleanisReadOnly();
    //是否使用了一个全新的数据库连接开启事务
    publicbooleanisNewConnection();
    //测试该事务是否被挂起
    publicbooleanisSuspend();
    //表示事务是否携带了一个保存点,嵌套事务通常会创建一个保存点作为嵌套事务与上一层事务的分界点。
    //注意:如果事务中包含保存点,则在递交事务时只处理这个保存点。
    publicbooleanhasSavepoint();
    //
    //设置事务状态为回滚,作为替代抛出异常进而触发回滚操作。
    publicvoidsetRollbackOnly();
    //设置事务状态为只读。
    publicvoidsetReadOnly();
}

    除此之外还需要声明一个枚举用以确定事务传播属性:

/**
 * 事务传播属性
 * @version : 2013-10-30
 * @author 赵永春(zyc@hasor.net)
 */
public enum TransactionBehavior {
    //
    //加入已有事务,尝试加入已经存在的事务中,如果没有则开启一个新的事务。
    PROPAGATION_REQUIRED,
    //
    //独立事务,挂起当前存在的事务,并开启一个全新的事务,新事务与已存在的事务之间彼此没有关系。
    RROPAGATION_REQUIRES_NEW,
    //
    //嵌套事务,在当前事务中开启一个子事务。如果事务递交将连同上一级事务一同递交。
    PROPAGATION_NESTED,
    //
    //跟随环境,如果当前没有事务存在,就以非事务方式执行;如果有,就使用当前事务。
    PROPAGATION_SUPPORTS,
    //
    //非事务方式,如果当前没有事务存在,就以非事务方式执行;如果有,就将当前事务挂起。
    PROPAGATION_NOT_SUPPORTED,
    //
    //排除事务,如果当前没有事务存在,就以非事务方式执行;如果有,就抛出异常。
    PROPAGATION_NEVER,
    //
    //强制要求事务,如果当前没有事务存在,就抛出异常;如果有,就使用当前事务。
    PROPAGATION_MANDATORY,
}

约定条件

    在实现类似 Spring 那样的事务控制之前需要做几个约定:

  • 1、每条线程只可以拥有一个活动的数据库连接,称之为“当前连接”。
  • 2、程序在执行期间如持有数据库连接,需要使用“引用计数”标记。
  • 3、一个事务状态中最多只能存在一个子事务(Savepoint)。
  • 4、当前的数据库连接是可以被随时更换的,即使它的“引用计数不为0”。
  • 5、数据库连接具备“事务状态”。

下面就讲讲为什么要先有这些约定:

一、为什么要有当前连接?

    一般数据库事务操作遵循(开启事务 -> 操作 -> 关闭事务)三个步骤,这三个步骤可以看作是固定的。你不能随意调换它们的顺序。在多线程下如果数据库连接共享,将会打破这个顺序。因为极有可能线程 A 将线程 B 的事务一起递交了。

    所以为了减少不必要的麻烦我们使用“当前连接”来存放数据库连接,并且约定当前连接是与当前线程绑定的。也就是说您在线程A下启动的数据库事务,是不会影响到线程B下的数据库事务。它们之间使用的数据库连接彼此互不干预。

二、为什么需要引用计数?

    引用计数是被用来确定当前数据库连接是否可以被 close。当引用计数器收到“减法”操作时候如果计数器为零或者小于零,则认为应用程序已经不在使用这个连接,可以放心 close。

三、为什么一个事务状态中只能存在一个子事务?

    答:子事务与父事务会被封装到不同的两个事务状态中。因此事务管理器从设计上就不允许一个事务状态持有两个事务特征,这样会让系统设计变得复杂。

四、当前的数据库连接是可以被随时更换的,即使它的“引用计数不为0”

    我们知道,随意更换当前连接有可能会引发数据库连接释放错误。但是依然需要这个风险的操作是由于“独立事务”的要求。

    在独立事务中如果当前连接已经存在事务,则会新建一个数据库连接作为当前连接并开启它的事务。

    独立事务的设计是为了保证,处于事务控制中的应用程序对数据库操作是不会有其它代码影响到它。并且它也不会影响到别人,故此称之为“独立”。

    此外在前面提到的场景“为什么有了嵌套事务还需要独立事务?”也已经解释独立事务存在的必要性。

五、数据库连接具备“事务状态”

    事务管理器在创建事务对象时,需要知道当前数据连接是否已经具有事务状态。

    如果尚未开启事务,事务管理器可以认为这个连接是一个新的(new状态),此时在事务管理器收到 commit 请求时,具有new状态时可以放心大胆的去处理事务递交操作。

    倘若存在事务,则很有可能在事务管理器创建事务对象之前已经对数据库进行了操作。基于这种情况下事务管理器就不能冒昧的进行 commit 或者 rollback。

    因此事务状态是可以用来决定事务管理器是否真实的去执行 commit 和 rollback 方法。有时候这个状态也被称之为“new”状态。

数据库连接可能存在的情况

    无论是否存在事务管理器,当前数据库连接都会具有一些固定的状态。那么下面就先分析一下当前数据库连接可能存在的情况有哪些?

  • 当前连接已经有程序使用(引用计数 !=0)
  • 当前连接尚未有程序使用(引用计数 ==0)
  • 当前连接已经开启了事务(autoCommit 值为 false)
  • 当前连接尚未开启事务(autoCommit 值为 true)

    上面虽然列出了四种情况,但是实际上可以看作两个状态值。

  • 1. 引用计数是否为0,表示是否可以关闭连接
  • 2. autoCommit是否为false(表示当前连接是否具有事务状态)

    引用计数为0,表示的是没有任何程序在执行时需要或者正在使用这个连接。也就是说这个数据库连接的存在与否根本不重要。

    autoCommit这个状态是来自于 Connection 接口,它表示的含义是数据库连接是否支持自动递交。如果为 true 表示Connection 在每次执行一条 sql 语句时都会跟随一个 commit 递交操作。如果执行失败,自然就相当于 rollback。因此可以看出这个值的情况反映出当前数据库连接的事务状态。

  • 1.有事务,引用大于0
  • 2.有事务,引用等于0
  • 3.没事务,引用大于0
  • 4.没事务,引用等于0

理解“new”状态

    new状态是用来标记当事务管理器创建新的事务状态时,当前连接的事务状态是如何的。并且辅助事务管理器决定究竟如何处理事务递交&回滚操作。

    上面这条定义准确的定义了 new 状态的作用,以及如何获取。那么我们要看看它究竟会决定哪些事情?

    根据定义,new 状态是用来辅助事务递交与回滚操作。我们先假设下面这个场景:

publicstaticvoidmain(){
  DataSource ds= ......;
  Connection conn = DataSourceUtil.getConnection(ds);//取得数据库连接,会导致引用计数+1
  conn.setAutoCommit(false);//开启事务
  conn.execute("update ...");//预先执行的 update 语句

  TransactionStatus status = tm.getTransaction(PROPAGATION_REQUIRED);//加入到已有事务,引用计数+1
  insertData();//执行数据库插入
  tm.commit(status);//引用计数-1

  conn.commit();//递交事务
  DataSourceUtil.releaseConnection(conn,ds);//释放连接,引用计数-1
}
publicstaticvoidinsertData(){
  jdbc.execute("insert into ...");//执行插入语句,在执行过程中引用计数会 +1,然后在-1
}

    在上面这个场景中,在调用 insertData 方法之前使用 REQUIRED(加入已有事务) 行为创建了一个事务。

    从逻辑上来讲 insertData 方法虽然在完成之后会进行事务递交操作,但是由于它的事务已经加入到了更外层的事务中。因此这个事务递交应该是被忽略的,最终的递交应当是由 conn.commit() 代码进行。

    我们分析一下在这个场景下 new 状态是怎样的。

    我们不难发现在 getTransaction 方法之前,应用程序实际上已经持有了数据库连接(引用计数+1),而随后它又关闭了自动递交,开启了事务。这样一来,就不满足 new 状态的特征。

   最后在 tm.commit(status) 时候,事务管理器会参照 new 状态。如果为 false 则不触发递交事务的操作。这恰恰保护了上面这个代码逻辑的正常运行。

    现在我们修改上面的代码如下:

publicstaticvoidmain(){
  DataSource ds= ......;
  Connection conn = DataSourceUtil.getConnection(ds);//取得数据库连接,会导致引用计数+1
  conn.execute("update ...");//预先执行的 update 语句

  TransactionStatus status = tm.getTransaction(PROPAGATION_REQUIRED);//加入到已有事务,引用计数+1
  insertData();//执行数据库插入
  tm.commit(status);//引用计数-1

  DataSourceUtil.releaseConnection(conn,ds);//释放连接,引用计数-1
}
publicstaticvoidinsertData(){
  jdbc.execute("insert into ...");//执行插入语句,在执行过程中引用计数会 +1,然后在-1
}

   我们发现,原本在申请连接之后的开启事务代码和释放连接之前的事务递交代码被删除了。也就是说在 getTransaction 时候数据库连接是满足 new 状态的特征的。

   程序中虽然在第四行有一条 SQL 执行语句,但是由于 Connection 在执行这个 SQL语句的时候使用的是自动递交事务。因此在 insertData 之后即使出现 rollback 也不会影响到它。

   最后在 tm.commit(status) 时候,事务管理器参照 new 状态。为 true 触发了交事务的操作。这也恰恰满足了上面这个代码逻辑的正常运行。

 

@黄勇 这里也有一篇文章简介事务控制 http://my.oschina.net/huangyong/blog/160012 他在文章中详细说述说了,事务隔离级别。这篇文章正好是本文作为基础部分的一个重要补充。在这里非常感谢 勇哥的贡献。

推荐:https://my.oschina.net/u/1166271/blog/199546

分享到:
评论

相关推荐

    Spring分布式事务实现

    其中,Spring的分布式事务管理是其核心特性之一,它允许开发者在分布式系统环境中处理复杂的事务逻辑。本篇将深入探讨Spring如何实现分布式事务,以及涉及到的相关技术。 首先,分布式事务是指在多个数据库或者服务...

    Spring事务管理Demo

    在Spring框架中,事务管理是核心特性之一,它允许开发者以声明式或编程式的方式处理应用中的事务。Spring事务管理的目的是确保数据的一致性和完整性,尤其是在多操作、多资源的环境中。本Demo将深入探讨Spring如何...

    Spring2.5实现事务管理(本地事务、分布式事务).doc

    Spring 2.5 实现事务管理(本地事务、分布式事务) Spring 框架提供了对事务管理的支持,它可以使得事务的管理变得更加简洁和灵活。事务管理是指在多个操作中维持一致性的机制,它可以确保在多个操作中,如果某个...

    spring事务的底层实现流程图

    spring事务的底层实现流程图 spring事务的底层实现流程图 spring事务的底层实现流程图 spring事务的底层实现流程图 spring事务的底层实现流程图 spring事务的底层实现流程图 spring事务的底层实现流程图 spring事务...

    spring-security实现复杂的权限管理

    授权机制是Spring Security的核心功能之一。通过定义访问决策策略,我们可以控制哪些用户可以访问哪些资源。这可以通过使用`@Secured`注解、表达式式语言(Spring EL)或者访问决策管理器(Access Decision Manager...

    Spring Nested事务简单案例

    在Spring框架中,事务管理是核心功能之一,它允许开发者以声明式或编程式的方式处理事务边界。在本案例中,我们关注的是Spring中的Nested事务,这是一个相对复杂的特性,但非常有用,特别是在需要子事务处理的场景下...

    Spring事务传播机制.docx

    当我们在使用 Spring 所提供的事务功能时,如果是仅仅处理单个的事务,是比较容易把握事务的提交与回滚,不过一旦引入嵌套事务后,多个事务的回滚和提交就会变得复杂起来,各个事务之间是如何相互影响的,是一个值得...

    springboot mybatis多数据源加事务嵌套

    springboot mybatis多数据源加事务嵌套 事务之间的调用 回滚 亲测可用 定义2个库分别建立 CREATE TABLE `user` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户编号', `user_name` varchar(25) ...

    spring 事务传播 demo

    在Spring框架中,事务管理是核心特性之一,它允许开发者以声明式或编程式的方式处理事务。本示例“spring 事务传播 demo”将聚焦于Spring的事务传播行为,这是在多个方法调用中控制事务边界的关键概念。下面我们将...

    Spring4--3.jdbcTemplate事务

    2. **编程式事务管理**:虽然较为复杂,但在某些特定场景下,如需要灵活控制事务的开始、提交和回滚时机,编程式事务管理是必要的。可以使用PlatformTransactionManager接口及其实现,如...

    论坛系统项目(Struts 2+Hibernate+Spring实现)

    论坛系统项目(Struts 2+Hibernate+Spring实现)论坛系统项目(Struts 2+Hibernate+Spring实现)论坛系统项目(Struts 2+Hibernate+Spring实现)论坛系统项目(Struts 2+Hibernate+Spring实现)论坛系统项目(Struts...

    Spring事务原理、Spring事务配置的五种方式

    PlatformTransactionManager是Spring提供的用于管理事务的基础接口,其下有一个实现的抽象类AbstractPlatformTransactionManager。我们使用的事务管理类,例如DataSourceTransactionManager等都是这个类的子类。 ...

    Spring JDBC与事务管理

    (1)使用Spring JDBC实现书店的购书过程,即有如下一个BookShopDao接口,编写BookShopDaoImp类实现该接口中的所有方法,并通过JUnit测试这些方法。 (2)(2) 在BookShopDao中添加一个purchase购书方法,其操作流程是...

    Spring事务流程图

    Spring事务管理是Spring框架的核心特性之一,主要用于处理应用程序中的数据一致性问题。在Spring中,事务管理分为编程式和声明式两种方式。本篇文章将详细解释Spring事务管理的流程,以及如何通过时序图来理解这一...

    spring 事务管理的理解

    本文将深入探讨Spring中的事务管理,包括其基本概念、工作原理以及如何在实际项目中配置和使用。 首先,我们需要了解什么是事务。事务是一组数据库操作,这些操作要么全部成功,要么全部失败。在事务中,如果任何...

    Spring事务小demo

    这个名为"Spring事务小demo"的项目提供了一个实践示例,帮助开发者了解Spring事务处理的基本概念和用法。 首先,Spring事务管理是Spring框架的核心特性之一,它允许我们以声明式或编程式的方式管理事务。声明式事务...

    spring与mybatis整合实现事务配置

    Spring是一个全面的后端开发框架,提供了依赖注入、AOP(面向切面编程)、事务管理等功能;而MyBatis则是一个轻量级的持久层框架,它将SQL与Java代码分离,使数据库操作更加灵活。将两者整合,可以实现高效、灵活的...

    Spring中事务的传播属性详解

    在使用Spring框架进行应用程序开发时,事务管理是一项非常重要的特性。Spring提供了两种事务管理方式:编程式事务管理和声明式事务管理。其中,声明式事务管理因其简洁性和易用性而更受欢迎。本文将详细介绍Spring中...

    Spring声明式事务处理

    Spring框架的声明式事务处理是Java企业级应用中不可或缺的一部分,它为开发者提供了一种方便、高效的方式来管理事务。在Spring中,事务管理分为编程式和声明式两种方式,而声明式事务处理则是通过配置来控制事务的...

Global site tag (gtag.js) - Google Analytics