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

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

为何须要嵌套事务? sql

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

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

    可是一般银行转账业务不管是否操做成功都会忘数据库里加入系统日志。若是日志输出与帐户金额调整在一个事务里,一旦事务回滚日志也会跟着一块儿消失。这时候就须要嵌套事务。 测试

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

为何有了嵌套事务还须要独立事务? spa

    假设如今银行须要知道当前正在进行转帐的实时交易数。 .net

    咱们知道一个完整的转帐业务会记录两第二天志,第一次用以记录是什么业务,第二次会记录这个业务总共耗时。所以完成这个功能时咱们只须要查询还未进行第二次记录的那些交易日志便可得出结果。 线程

时间 事务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 interface TransactionManager {
    //开启事务,使用不一样的传播属性来建立事务。
    public TransactionStatus getTransaction(TransactionBehavior behavior);
    //递交事务
    public void commit(TransactionStatus status) throws SQLException;
    //回滚事务
    public void rollBack(TransactionStatus status) throws SQLException;
}

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

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

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

/**
 * 事务传播属性
 * @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 那样的事务控制以前须要作几个约定:

  • 一、每条线程只能够拥有一个活动的数据库链接,称之为“当前链接”。
  • 二、程序在执行期间如持有数据库链接,须要使用“引用计数”标记。
  • 三、一个事务状态中最多只能存在一个子事务(Savepoint)。
  • 四、当前的数据库链接是能够被随时更换的,即便它的“引用计数不为0”。
  • 五、数据库链接具有“事务状态”。

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

1、为何要有当前链接?

    通常数据库事务操做遵循(开启事务 -> 操做 -> 关闭事务)三个步骤,这三个步骤能够看做是固定的。你不能随意调换它们的顺序。在多线程下若是数据库链接共享,将会打破这个顺序。由于极有可能线程 A 将线程 B 的事务一块儿递交了。

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

2、为何须要引用计数?

    引用计数是被用来肯定当前数据库链接是否能够被 close。当引用计数器收到“减法”操做时候若是计数器为零或者小于零,则认为应用程序已经不在使用这个链接,能够放心 close。

3、为何一个事务状态中只能存在一个子事务?

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

4、当前的数据库链接是能够被随时更换的,即便它的“引用计数不为0”

    咱们知道,随意更换当前链接有可能会引起数据库链接释放错误。可是依然须要这个风险的操做是因为“独立事务”的要求。

    在独立事务中若是当前链接已经存在事务,则会新建一个数据库链接做为当前链接并开启它的事务。

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

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

5、数据库链接具有“事务状态”

    事务管理器在建立事务对象时,须要知道当前数据链接是否已经具备事务状态。

    若是还没有开启事务,事务管理器能够认为这个链接是一个新的(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 状态是用来辅助事务递交与回滚操做。咱们先假设下面这个场景:

public static void main(){
  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
}
public static void insertData(){
  jdbc.execute("insert into ...");//执行插入语句,在执行过程当中引用计数会 +1,而后在-1
}

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

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

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

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

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

    如今咱们修改上面的代码以下:

public static void main(){
  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
}
public static void insertData(){
  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 他在文章中详细说述说了,事务隔离级别。这篇文章正好是本文做为基础部分的一个重要补充。在这里很是感谢 勇哥的贡献。


相关博文:

相关文章
相关标签/搜索