第20章 使用Spring进行事务管理
本章内容:
- 编程式事务管理
- 声明式事务管理
事务管理的实施通常有两种方式,即编程式事务管理和声明式事务管理。对于这两种事务管理方式的支持,Spring事务框架可以说是青出于蓝而胜于蓝。
20.1 编程式事务管理
通过Spring进行编程式事务管理有两种方式,要么直接使用PlatformTransactionManager,要么使用更方便的TransactionTemplate。二者各有优缺点,但总体上来说,推荐使用TransactionTemplate进行编程式事务管理。
20.1.1 直接使用PlatformTransactionManager进行编程式事务管理
PlatformTransactionManager接口定义了事务界定的基本操作,我们可以直接使用PlatformTransactionManager进行编程式事务管理,如下方代码清单所示。
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setTimeout(20);
...
TransactionStatus txStatus = transactionManager.getTransaction(definition);
try {
// 业务逻辑实现
}
catch(App1icationException e) {
transactionManager.rollback(txStatus);
throw e;
)
catch(RuntimeException e) {
transactionManager.rollback(txStatus);
throw e;
}
catch(Error e) {
transactionManager.rollback(txStatus);
throw e;
}
transactionManager.commit(txStatus);
只要为transactionManager提供合适的platformTransactionManager实现,然后结合TransactionDefinition开启事务,并结合TransactionStatus来回滚或者提交事务,就可以完成针对当前对象的整个事务管理。
直接使用PlatformTransactionManager,我们可以完全控制整个事务处理过程,但是,缺点也是很明显的。从抽象事务操作以屏蔽不同事务管理API差异的角度看,PlatformTransactionManager可能已经足够了。但是,从应用程序开发的角度来看,却依然过于底层,只是期间的这些异常处理就够我们忙活的了。如果在每个需要事务管理的地方,全都使用PlatformTransactionManager进行事务管理,那么重复代码的数量将是惊人的。
鉴于使用PlatformTransactionManager进行事务管理的流程比较固定,各个事务管理期间只有部分逻辑存在差异,我们可以考虑像Spring的数据访问层那样,**使用模板方法模式与Callback相结合的方式,对直接使用PlatformTransactionManager进行事务管理的代码进行封装。**这就有了更方便的编程式事务管理方式,即使用TransactionTemplate的编程式事务管理。
20.1.2 使用TransactionTemplate进行编程式事务管理
org.springframework.transaction.support.TransactionTemplate
对与PlatformTransactionManager相关的事务界定操作以及相关的异常处理进行了模板化封装,开发人员更多地关注于通过相应的Callback接口提供具体的事务界定内容即可。
Spring针对TransactionTemplate提供了两个Callback接口,TransactionCallback
和TransactionCallbackWithoutResult
,二者的唯一区别就是是否需要返回执行结果。
使用TransactionTemplate进行事务管理的代码,看起来要比直接使用PlatformTransactionManager要简洁并且容易管理得多,如下方代码清单所示。
TransactionTemplate txTemplate = ...;
Object result = txTemplate.execute(new TransactionCallback() {
public Object doInTransaction(TransactionStatus txStatus) {
Object result = null;
// 各种事务操作.....
return result;
}
});
或者
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus txStatus) {
// 事务操作1
// 事务操作2
// ...
}
});
TransactionTemplate会捕捉TransactionCallback或者TransactionCallbackWithoutResult事务操作中抛出的unchecked exception并回滚事务,然后将unchecked exception抛给上层处理。所以,现在我们只需要处理特定于应用程序的异常即可,而不用像直接使用PlatformTransactionManager那样,对所有可能的异常都进行处理。
如果事务处理期间没有任何问题,TransactionTemplate最终会为我们提交事务,唯一需要我们干预的就只剩下某些情况下的事务回滚了。如果在TransactionCallback或者TransactionCallbackWithoutResult的事务操作过程中需要让当前事务回滚而不是最终提交,一般来说,我们有如下两种方式。
- 抛出unchecked exception,TransactionTemplate会为我们处理事务的回滚。如果事务操作中可能抛出checked exception,那么就得在Callback内部捕获,然后转译为unchecked exception后抛出。 下方代码清单给出了针对这种情况的处理代码示例。
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus txStatus) {
try {
...
}
catch(CheckedException e) {
// 抛出特定的异常类型以避免一般情况下使用的RuntimeException
throw new RuntimeException(e);
}
}
});
使用Callback接口公开的TransactionStatus将事务标记为rollBackonly。**TransactionTemplate在最终提交事务的时候,如果检测到rollBackOnly标志状态被设置,将把提交事务改为回滚事务。**下方代码清单演示了这种情况。
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus txStatus) {
boolean needRollback = false;
...
if(needRollback)
txStatus.setRollbackOnly();
}});
对于事务操作中可能抛出的checked exception,如果既想回滚事务,又不想让它以unchecked exception的形式向上层传播的话,我们当然不能通过将其转译成unchecked exception的方式来处理。我们现在却可以通过TransactionStatus设置rollBackOnly来达到以上“一箭双雕”的目的(见下方代码清单)。
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus txStatus) {
try {
...
}
catch(CheckedException e) {
logger.warn("Transaction is Rolled back!", e);
txStatus.setRollbackOnly();
}
}
});
这种情况下,需要注意一个问题,千万不要只txStatus.setRollbackOnly()
而忘记记录日志。虽然你可能知道“swallow exception”是不对的,但这个地方确实容易忽略日志的记录,从而造成事务回滚了,而我们却不知道的情况。当发现数据库中本来应该删除的数据却依然存在,并且日志中也没有任何异常信息的时候,你就得花很长时间来想到底哪里出了问题了。
虽然使用TransactionTemplate要比直接使用PlatformTransactionManager更加便捷,但TransactionTemplate无法处理当事务操作中需要向上层抛出原来的checked exception的情况。 你应该也发现了,实际上TransactionCallback或者TransactionCallbackwithoutResult的方法定义中没有声明抛出任何checked exception,直接使用PlatformTransactionManager则没有这样的限制。不过,这应该并不会过多地限制TransactionTemplate展现其“个人魅力”吧?
20.1.3 编程创建基于Savepoint的嵌套事务
TransactionStatus不但可以在事务处理期间通过setRollbackOnly()
方法来干预事务的状态,如果需要,作为SavepointManager,它也可以帮助我们使用Savepoint机制来创建嵌套事务。
以银行账户间转账为例,来说明如何使用TransactionStatus创建基于Savepoint的嵌套事务。现在不是从一个账户转到另一个账户,而是从一个账户转到两个账户,一个是主账户,一个备用账户。如果向主账户转账失败,则将金额转入备用账户。总之,金额从第一个账户取出之后,必须存入两个账户的其中一个,以保证整个事务的完整性。在这样的前提下,我们的事务管理代码基本上如下方代码清单所示。
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus txStatus) {
BigDecimal transferAmount = new BigDecimal("20000");
try {
withdraw("WITHDRAW_ACOUNT_ID", transferAmount);
Object savePointBeforeDeposit = txStatus.createSavepoint();
try {
deposit("MAIN_ACOUNT_ID", transferAmount);
}
catch(DepositException ex) {
logger.warn("rollback to savepoint for main acount transfer failure", ex);
txStatus.rollbackToSavepoint(savePointBeforeDeposit);
deposit("SECONDARY_ACOUNT_ID", transferAmount);
}
finally {
txStatus.releaseSavepoint(savePointBeforeDeposit);
}
}
catch(TransferException e) {
logger.warn("failed to complete transfer operation!", e);
txStatus.setRo1lbackOnly();
}
}
});
当然,如果在转账期间的异常是unchecked exception,最外层的捕捉TransferException是没有太多必要的(这种情况下,TransactionTemplate将自动回滚事务)。
在这里,使用Savepoint创建嵌套事务的好处是,即使deposit过程中涉及多笔数据的更新,通过txStatus.rollbackToSavepoint(savePointBeforeDeposit)
也可以将这些数据恢复到没有存入金额之前的状态,而不会破坏当前事务的完整性。
如果在这里通过传播行为是PROPAGATION_REQUIRES_NEW
的TransactionDefinition创建一个新的事务的话,虽然deposit过程中出现问题也可以回滚数据,但取款与存款的操作就不在同一个事务中了(取款在当前事务,存款在另一个新的事务),这无疑违反了事务的ACID属性。
注意:使用TransactionStatus创建基于Savepoint的嵌套事务需要底层的PlatformTransactionManager实现类的支持,当前只有在JDBC3.0驱动下的DataSourceTransactionManager可用。
通过使用TransactionStatus创建基于Savepoint的嵌套事务并非创建嵌套事务的唯一方式,也并非最方便的方式。实际上,我们更倾向于使用结合PROPAGATION_NESTED
传播行为的声明式事务管理方式。