深入浅出事务之传播属性
本文参考《java Transaction design strategies》
大部分时候,我们都习惯了spring容器默认的配置,但有时候,我们需要知道更多……
当使用声明式事务模型时,您必须告诉容器如何去管理事务,例如,何时开启一个事务?哪些方法需要事务?当前不存在事务的情况下,容器是否需要为其添加事务控制?事实上,Spring提供了一个bean —— TransactionAttributSource,通过配置其事务(传播)属性(transaction attribute)来达到精确控制事务行为的目的。事务的属性总共有六种:
RequiredMandatoRequiresNewSupportsNotSupportedNever
Spring又添加了一种新的事务属性,PROPAGATION_NESTED,用于实现真正的嵌套事务,前提条件是外部环境必须提供相应的实现支持。尽管可以通过配置的方式在bean(类)级别指定事务属性,一般来说,还是应该将事务属性应用于方法级别。为整个bean配置某个事务属性意味着其内部所有的方法都采用它,而方法级别的事务属性可以将其覆盖。
REQUIRED
Required属性告诉容器某个特定的方法需要一个事务,如果上下文中已经存在事务,则加入;否则,开启一个事务。这是一种使用最频繁的事务属性,适用于大多数情况;但是,并不绝对,下面我们将会看到对于某些场景,总会有更好的理由去使用Mandatory属性。
MANDATORY
Mandatory属性告诉容器某个特定的方法需要一个事务。但是,不同于Required属性,它无论如何都不会开启新的事务;相反的,它会“强制”要求该方法被调用时上下文中必须存在事务,否则会抛出TransactionRequiredException异常,提示需要一个事务但没有找到。何时选择Mandatory,后面将会专门抽出一节来分析。
REQUIRESNEW
RequiresNew属性告诉容器某个特定的方法需要一个新事务的支持。如果上下文中已经存在事务A,则该事务A挂起,并启动一个新的事务B。当事务B结束后,事务A被唤醒并继续执行。事实上,使用RequiresNew违反了事务的ACID原则,因为新事务会导致原有事务的挂起。
该属性在某个行为必须被完成(提交或回滚)而不受外部事务结果影响时十分有用,例如记录日志。大多数交易系统的每一个操作都必须写进日志,无论其执行结果如何(成功或失败)。假设某个股票交易的方法placement()启动了一个事务,并且调用了一个通用的方法audit()来记录日志。由于audit()和placement()处于同一个事务的管辖范围之内,因此一旦placement()回滚,audit()记录的日志也会相应的进行回滚;这就违背了“任何成功或失败的操作都必须记录日志”这一业务逻辑。这个时候,如果将audit()的事务属性设作RequiresNew,则确保了audit()在一个新的、单独的事务中记录日志,因此不受placement()中外部事务的影响。
SUPPORTS
Supports属性告诉容器,该方法不需要事务支持,但如果当前上下文中已经存在了一个事务,则加入其中。Supports这是一个相当强大、相当有用的事务属性。考虑这样一个场景,业务方法A用来查询某个交易者特定时期的交易总量。由于是查询操作,因此这个时候事务并不是必须的,因此我们将其事务属性设为Supports,来告诉容器在调用方法时不要开启新的事务。但是,如果方法A被某个已经存在事务控制的方法B所调用,那么它就会加入当前事务;那么,在该事务提交之前,方法B中对数据的任何修改对于方法A来说,都是可见的。
下面我们举个更具体的例子来说明Supports的作用。假设某位交易者一天的最大交易额是一百万,如果采用Supports作为事务属性,一次超额交易的具体处理流程如下:
目前为止,当天总共的交易量是900,000事务启动交易者又进行了一笔200,000的交易调用事务属性为Supports的查询方法,因为是同一个事务,因此得到结果为1,100,000业务逻辑判断,已经超过最大交易限额,抛出异常,事务回滚
如果使用NotSupported会发生什么呢?我们不会得到任何异常,因为查询方法不会加入当前事务,因此它看不见当前事务中对数据库的任何修改。
目前为止,当天总共的交易量是900,000事务启动交易者又进行了一笔200,000的交易调用事务属性为SNotupported的查询方法,因为不属于当前事务,因此得到结果为900,000业务逻辑判断,没有超过最大交易限额,事务提交(实际已超过当天限额)
NotSupported
NotSupported属性告诉容器,该方法不需要事务支持;如果当前上下文中已经存在事务,则该事务被挂起直到该方法执行完毕。如果当前上下文中不存在事务,该方法则在没有事务支持的环境下执行。NotSupported适用于“某些方法在事务控制下有较大可能性会产生异常”的场合。例如在XA架构的事务处理过程中,调用包含DDL语句的存储过程往往会抛出异常,因此比较好的做法是将其设为NotSupported,暂时挂起当前事务。
Never
Never属性告诉容器,该方法必须在无事务的上下文中运行。注意,这与NotSupported不同,后者意味着,如果上下文中存在事务,则将该事务暂时挂起并在无事务的上下文中运行。但Never却不同,如果当前上下文中已经存在事务,则在调用该方法时会抛出一个异常,提示该方法不能在事务环境下运行。因为使用Never会导致各种意料之外的运行时异常,因此除非必要,一般不推荐使用
Required vs. Mandatory
Required和Mandatory是两个往往容易被混淆的事务属性。两者都可以运行在事务上下文环境中,但当上下文中不存在事务时,Required会自己开启一个事务,而Mandatory则不会。大多数情况下,你可以采用如下的“最佳实践”来区分何时需要哪种策略:如果某一方法需要运行在具备事务的环境,但其本身又不负责事务的控制(回滚),则该方法应该标志为Mandatory。可以看出,上述最佳实践是基于事务的所有权而得来的。
Nested vs. RequiresNew vs. Required
另外两个比较容易的事务属性是Spring提供的Nested与RequiresNew。如果当前上下文中存在事务,两者都是启动一个新的事务;如果当前上下文中不存在事务,则类似于PROPAGATION_REQUIRED,两者也都新建一个事务。那么,它们有什么区别呢?
PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 "内部" 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等。当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。
另一方面, PROPAGATION_NESTED 开始一个 "嵌套的" 事务, 它是已经存在事务的一个真正的子事务。嵌套事务开始执行时, 它将取得一个 savepoint。如果这个嵌套事务失败, 我们将回滚到此 savepoint。嵌套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。
总结一下,使用嵌套事务的场景总共有两个,如下图所示:
需要事务BC与事务AD一起提交,即:作为事务AD的子事务,事务BC只有在事务AD成功提交时(阶段3成功)才提交。这个需求简单称之为“联合提交”。这一点PROPAGATION_REQUIRED可以做到。需要事务BC的回滚不会影响事务AD的提交。这个需求简单称之为“隔离回滚”。这一点,可以通过设置事务属性为PROPAGATION_REQUIRES_NEW做到。
使用PROPAGATION_REQUIRED满足需求1,但子事务BC的rollback会迫使父事务AD也回滚,不能满足需求2。使用PROPAGATION_REQUIRES_NEW满足需求2,但子事务(严格意义上说,这时不应该称之为子事务)BC是一个全新的事务,父事务(严格意义上说,这时也不应该称之为父事务)AD的成功与否完全不影响BC的提交,不能满足需求1。
同时满足上述两条需求就要用到PROPAGATION_NESTED了。PROPAGATION_NESTED在事务AD执行到B点时,设置了savePoint。当BC事务成功提交时,PROPAGATION_NESTED的行为与PROPAGATION_REQUIRED一样。只有当事务AD在D点成功提交时,事务BC才真正提交; 如果阶段3执行异常,导致事务AD 回滚,事务BC也将一起回滚,从而满足了“联合提交”。 当阶段2执行异常,导致BC事务rollback时,因为设置了savePoint的缘故,AD事务可以选择与BC一起rollback或继续阶段3的执行并保留阶段1的执行结果,从而满足了“隔离回滚”。例如,可以把执行BC事务的方法try-catch起来,在catch中选择是否继续向上抛出异常(是,则回滚到A处;否,则回滚到B处)。而是用PROPAGATION_REQUIRED时,即使try-catch住BC的异常,AD事务也一定会被无条件rollback。
文章不错,谢谢分享