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

原创
2014/02/13 13:04
阅读数 1.1W

    写这篇博文的目的首先是与大家分享一下如何用更轻量化的办法去实现 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 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 那样的事务控制之前需要做几个约定:

  • 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 状态是用来辅助事务递交与回滚操作。我们先假设下面这个场景:

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 他在文章中详细说述说了,事务隔离级别。这篇文章正好是本文作为基础部分的一个重要补充。在这里非常感谢 勇哥的贡献。


相关博文:

展开阅读全文
打赏
17
95 收藏
分享
加载中
厉害了!最近在学习Spring 嵌套事务这块,源码我拿走学习了,谢谢!
2017/11/14 10:33
回复
举报
哈库纳博主

引用来自“在coding”的评论

请问楼主,每条线程只可以拥有一个活动的数据库连接,称之为“当前连接” ,这句话咋么理解,看过后面的request_new,如果有事务会再去获取一个连接,这个连接和前面事务获取的连接是一样的吗
先回答你的问题:“如果有事务会再去获取一个连接,这个连接和前面事务获取的连接是一样的吗?”
------
答:不一样。此时当前线程有两条数据库连接。但是活动的只有一个。
------
原理:request_new 会在申请新的连接之前把当前连接进行压栈操作、然后在申请新的连接作为当前连接,从而完成 request_new 嵌套功能支持。
嵌套的 request_new 当完成数据库操作释放连接之后,会做弹栈操作让当时被压栈的连接恢复到当前连接。
2016/12/08 10:07
回复
举报
请问楼主,每条线程只可以拥有一个活动的数据库连接,称之为“当前连接” ,这句话咋么理解,看过后面的request_new,如果有事务会再去获取一个连接,这个连接和前面事务获取的连接是一样的吗
2016/12/06 15:30
回复
举报
太棒了。。。

特别是那些对接口使用比较迷惑的人,认真揣摩一下接口API,体会一下。。肯定会有收获。。
2014/02/16 11:54
回复
举报
哈库纳博主

引用来自“山哥”的评论

历害,好好看看。

13,欢迎山哥 亲临指导。
2014/02/15 11:37
回复
举报
哈库纳博主

引用来自“黄勇”的评论

春哥,太棒了!看到你的在研究这方面,让我不得不放弃 Smart 的嵌套事务,因为你才是这方面当之无愧的专家。等你这个事务框架发布以后,我们可以考虑与 Smart 做一个集成,相信那一定非常振奋人心!建议给这个框架名一个名吧。

我也很期待,这个东西可以在 Smart 里面工作起来。我相信集成应该是非常容易的,而且对 Smart 是无侵入的。

至于名字么,呃目前打算还是叫 Hasor-JDBC,虽然有 Hasor 这个词在里面。但其实基本上已经和 Hasor 没什么关系了。
2014/02/15 11:32
回复
举报
哈库纳博主

引用来自“悠悠然然”的评论

没有必要完全模仿spring,如果你模仿spring最后就以下结果:
你不如spring
你比spring好
你和spring差不多
第一种概率45
第二种概率10
第三种概率45
所以只做必须的几个可以了。
这样简单、高效、易用就是你的优势。
如果完全模仿spring会迷失自我,最后人家说,这不就是模仿spring吗?这个时候无论怎样都是一种失败。

哥乱弹的,不一定正确。

没错这个设计,就是完全照 Spring 扒下来的,十足的轮子。搞着有这么几个目的。

1.分析 Spring 的事务控制方式,顺便向大家分享 Spring 的设计。这样如果有人想移植几个 Spring 事务控制方案也可以有个技术参照。当然移植过程可以在任意的语言平台上进行,只要它满足基本需求。

2.Spring JDBC 其内部绑的比较死,很难将其完全独立出来。其次在市面也很少有同类型相对独立的事务框架。

3.想给 Hasor 配上一个像样的数据库操作层。也是最初的起因,现在看来这条已经成为价值最低的因素了。

这个东西搞出来之后是完完全全的独立出来的,顺便我会写一个 java 版的实现。
这样一来任意一个框架都可以通过它拥有像 Spring 那样的事务控制体系了。
这个也算是为大家写自己框架时,在事务控制这块可以减少了一些工作。让更多优秀的框架可以快速成长。
2014/02/15 11:28
回复
举报
历害,好好看看。
2014/02/15 09:39
回复
举报
没有必要完全模仿spring,如果你模仿spring最后就以下结果:
你不如spring
你比spring好
你和spring差不多
第一种概率45
第二种概率10
第三种概率45
所以只做必须的几个可以了。
这样简单、高效、易用就是你的优势。
如果完全模仿spring会迷失自我,最后人家说,这不就是模仿spring吗?这个时候无论怎样都是一种失败。

哥乱弹的,不一定正确。
2014/02/15 09:06
回复
举报
春哥,太棒了!看到你的在研究这方面,让我不得不放弃 Smart 的嵌套事务,因为你才是这方面当之无愧的专家。等你这个事务框架发布以后,我们可以考虑与 Smart 做一个集成,相信那一定非常振奋人心!建议给这个框架名一个名吧。
2014/02/14 22:31
回复
举报
更多评论
打赏
14 评论
95 收藏
17
分享
返回顶部
顶部