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

事务传播行为种类

Spring在TransactionDefinition接口中规定了7种类型的事务传播行为,

它们规定了事务方法和事务方法发生嵌套调用时事务如何进行传播:

表1事务传播行为类型

事务传播行为类型

说明

PROPAGATION_REQUIRED

如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。

PROPAGATION_SUPPORTS

支持当前事务,如果当前没有事务,就以非事务方式执行。

PROPAGATION_MANDATORY

使用当前的事务,如果当前没有事务,就抛出异常。

PROPAGATION_REQUIRES_NEW

新建事务,如果当前存在事务,把当前事务挂起。

PROPAGATION_NOT_SUPPORTED

以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

PROPAGATION_NEVER

以非事务方式执行,如果当前存在事务,则抛出异常。

PROPAGATION_NESTED

如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

为什么需要嵌套事务?

我们知道,数据库事务是为了保证数据库操作原子性而设计的一种解决办法。例如执行两条 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”三个基本操作。因此数据库事务管理器的接口相对简单。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
 * 数据源的事务管理器。
 * @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;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * 表示一个事务状态
 * @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();
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
 * 事务传播属性
 * @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 状态是用来辅助事务递交与回滚操作。我们先假设下面这个场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 则不触发递交事务的操作。这恰恰保护了上面这个代码逻辑的正常运行。

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

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

相关博文:

上一篇:Android 滑动效果基础篇(四)—— Gallery + GridView


下一篇:【JS复习笔记】07 复习感想