您的位置:首页 > 编程语言 > Java开发

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

2015-08-14 19:13 253 查看
事务传播行为种类

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”三个基本操作。因此数据库事务管理器的接口相对简单。如下:

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

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

?

约定条件

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

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

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

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

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

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

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

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

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

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

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

相关博文:

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

脱离 Spring 实现复杂嵌套事务,之二(PROPAGATION_REQUIRED - 加入已有事务)

脱离 Spring 实现复杂嵌套事务,之三(RROPAGATION_REQUIRES_NEW - 独立事务)

脱离 Spring 实现复杂嵌套事务,之四(PROPAGATION_NESTED - 嵌套事务)

脱离 Spring 实现复杂嵌套事务,之五(PROPAGATION_SUPPORTS - 跟随环境)

脱离 Spring 实现复杂嵌套事务,之六(PROPAGATION_NOT_SUPPORTED - 非事务方式)

脱离 Spring 实现复杂嵌套事务,之七(PROPAGATION_NEVER - 排除事务)

脱离 Spring 实现复杂嵌套事务,之八(PROPAGATION_MANDATORY - 要求存在事务)

脱离 Spring 实现复杂嵌套事务,之九(整合七种传播行为)

脱离 Spring 实现复杂嵌套事务,之十(实现篇)

原文:/article/3464578.html
/article/1918813.html
http://blog.chinaunix.net/uid-10289334-id-2964925.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: