事务策略之了解事务陷阱.docx

上传人:b****4 文档编号:11532881 上传时间:2023-03-18 格式:DOCX 页数:11 大小:62.01KB
下载 相关 举报
事务策略之了解事务陷阱.docx_第1页
第1页 / 共11页
事务策略之了解事务陷阱.docx_第2页
第2页 / 共11页
事务策略之了解事务陷阱.docx_第3页
第3页 / 共11页
事务策略之了解事务陷阱.docx_第4页
第4页 / 共11页
事务策略之了解事务陷阱.docx_第5页
第5页 / 共11页
点击查看更多>>
下载资源
资源描述

事务策略之了解事务陷阱.docx

《事务策略之了解事务陷阱.docx》由会员分享,可在线阅读,更多相关《事务策略之了解事务陷阱.docx(11页珍藏版)》请在冰豆网上搜索。

事务策略之了解事务陷阱.docx

事务策略之了解事务陷阱

在应用程序中使用事务常常是为了维护高度的数据完整性和一致性。

如果不关心数据的质量,就不必使用事务。

毕竟,Java平台中的事务支持会降低性能,引发锁定问题和数据库并发性问题,而且会增加应用程序的复杂性。

关于本系列

事务提高了数据的质量、完整性和一致性,使应用程序更健壮。

在Java应用程序中实现成功的事务处理不是一件容易的事,设计和编码几乎一样重要。

在这份新的系列文章中,MarkRichards将带领您设计一个有效的事务策略,适合从简单应用程序到高性能事务处理等各种用例。

但是不关心事务的开发人员就会遇到麻烦。

几乎所有与业务相关的应用程序都需要高度的数据质量。

金融投资行业在失败的交易上浪费数百亿美元,不好的数据是导致这种结果的第二大因素(请参阅参考资料)。

尽然缺少事务支持只是导致坏数据的一个因素(但是是主要的因素),但是完全可以这样认为,在金融投资行业浪费掉数十亿美元是由于缺少事务支持或事务支持不充分。

忽略事务支持是导致问题的另一个原因。

我常常听到“我们的应用程序中不需要事务支持,因为这些应用程序从来不会失败”之类的说法。

是的,我知道有些应用程序极少或从来不会抛出异常。

这些应用程序基于编写良好的代码、编写良好的验证例程,并经过了充分的测试,有代码覆盖支持,可以避免性能损耗和与事务处理有关的复杂性。

这种类型的应用程序只需考虑事务支持的一个特性:

原子性。

原子性确保所有更新被当作一个单独的单元,要么全部提交,要么回滚。

但是回滚或同时更新不是事务支持的惟一方面。

另一方面,隔离性将确保某一工作单元独立于其他工作单元。

没有适当的事务隔离性,其他工作单元就可以访问某一活动工作单元所做的更新,即使该工作单元还未完成。

这样,就会基于部分数据作出业务决策,而这会导致失败的交易或产生其他负面(或代价昂贵的)结果。

迟做总比不做好

我是在2000年年初开始关注事务处理问题的,当时我正在研究一个客户端站点,我发现项目计划中有一项内容优先于系统测试任务。

它称为实现事务支持。

当然,在某个主要应用程序差不多准备好进行系统测试时,给它添加事务支持是非常简单的。

遗憾的是,这种方法实在太普通。

至少这个项目(与大多数项目不同)确实实现了事务支持,尽管是在开发周期快结束时。

因此,考虑到坏数据的高成本和负面影响,以及事务的重要性(和必须性)这些基本常识,您需要使用事务处理并学习如何处理可能出现的问题。

您在应用程序中添加事务支持后常常会出现很多问题。

事务在Java平台中并不总是如预想的那样工作。

本文会探讨其中的原因。

我将借助代码示例,介绍一些我在该领域中不断看到的和经历的常见事务陷阱,大部分是在生产环境中。

虽然本文中的大多数代码示例使用的是SpringFramework(version2.5),但事务概念与EJB3.0规范中的是相同的。

在大多数情况下,用EJB3.0规范中的_cnnew1@TransactionAttribute注释替换SpringFramework@Transactional注释即可。

如果这两种框架使用了不同的概念和技术,我将同时给出SpringFramework和EJB3.0源代码示例。

本地事务陷阱

最好先从最简单的场景开始,即使用本地事务,一般也称为数据库事务。

在数据库持久性的早期(例如JDBC),我们一般会将事务处理委派给数据库。

毕竟这是数据库应该做的。

本地事务很适合执行单一插入、更新或删除语句的逻辑工作单元(LUW)。

例如,考虑清单1中的简单JDBC代码,它向TRADE表插入一份股票交易订单:

清单1.使用JDBC的简单数据库插入

清单1中的JDBC代码没有包含任何事务逻辑,它只是在数据库中保存TRADE表中的交易订单。

在本例中,数据库处理事务逻辑。

在LUW中,这是一个不错的单个数据库维护操作。

但是如果需要在向数据库插入交易订单的同时更新帐户余款呢?

如清单2所示:

清单2.在同一方法中执行多次表更新

在本例中,insertTrade()和updateAcct()方法使用不带事务的标准JDBC代码。

insertTrade()方法结束后,数据库保存(并提交了)交易订单。

如果updateAcct()方法由于任意原因失败,交易订单仍然会在placeTrade()方法结束时保存在TRADE表内,这会导致数据库出现不一致的数据。

如果placeTrade()方法使用了事务,这两个活动都会包含在一个LUW中,如果帐户更新失败,交易订单就会回滚。

随着Java持久性框架的不断普及,如Hibernate、TopLink和Java持久性API(JavaPersistenceAPI,JPA),我们很少再会去编写简单的JDBC代码。

更常见的情况是,我们使用更新的对象关系映射(ORM)框架来减轻工作,即用几个简单的方法调用替换所有麻烦的JDBC代码。

例如,要插入清单1中JDBC代码示例的交易订单,使用带有JPA的SpringFramework,就可以将TradeData对象映射到TRADE表,并用清单3中的JPA代码替换所有JDBC代码:

清单3.使用JPA的简单插入

注意,清单3在EntityManager上调用了persist()方法来插入交易订单。

很简单,是吧?

其实不然。

这段代码不会像预期那样向TRADE表插入交易订单,也不会抛出异常。

它只是返回一个值0作为交易订单的键,而不会更改数据库。

这是事务处理的主要陷阱之一:

基于ORM的框架需要一个事务来触发对象缓存与数据库之间的同步。

这通过一个事务提交完成,其中会生成SQL代码,数据库会执行需要的操作(即插入、更新、删除)。

没有事务,就不会触发ORM去生成SQL代码和保存更改,因此只会终止方法—没有异常,没有更新。

如果使用基于ORM的框架,就必须利用事务。

您不再依赖数据库来管理连接和提交工作。

这些简单的示例应该清楚地说明,为了维护数据完整性和一致性,必须使用事务。

不过对于在Java平台中实现事务的复杂性和陷阱而言,这些示例只是涉及了冰山一角。

SpringFramework@Transactional注释陷阱

您将测试清单3中的代码,发现persist()方法在没有事务的情况下不能工作。

因此,您通过简单的网络搜索查看几个链接,发现如果使用SpringFramework,就需要使用@Transactional注释。

于是您在代码中添加该注释,如清单4所示:

清单4.使用@Transactional注释

现在重新测试代码,您发现上述方法仍然不能工作。

问题在于您必须告诉SpringFramework,您正在对事务管理应用注释。

除非您进行充分的单元测试,否则有时候很难发现这个陷阱。

这通常只会导致开发人员在Spring配置文件中简单地添加事务逻辑,而不会使用注释。

要在Spring中使用@Transactional注释,必须在Spring配置文件中添加以下代码行:

annotation-driventransaction-manager="transactionManager"/>

transaction-manager属性保存一个对在Spring配置文件中定义的事务管理器bean的引用。

这段代码告诉Spring在应用事务拦截器时使用@Transaction注释。

如果没有它,就会忽略@Transactional注释,导致代码不会使用任何事务。

让基本的@Transactional注释在清单4的代码中工作仅仅是开始。

注意,清单4使用@Transactional注释时没有指定任何额外的注释参数。

我发现许多开发人员在使用@Transactional注释时并没有花时间理解它的作用。

例如,像我一样在清单4中单独使用@Transactional注释时,事务传播模式被设置成什么呢?

只读标志被设置成什么呢?

事务隔离级别的设置是怎样的?

更重要的是,事务应何时回滚工作?

理解如何使用这个注释对于确保在应用程序中获得合适的事务支持级别非常重要。

回答我刚才提出的问题:

在单独使用不带任何参数的@Transactional注释时,传播模式要设置为REQUIRED,只读标志设置为false,事务隔离级别设置为READ_COMMITTED,而且事务不会针对受控异常(checkedexception)回滚。

@Transactional只读标志陷阱

我在工作中经常碰到的一个常见陷阱是Spring@Transactional注释中的只读标志没有得到恰当使用。

这里有一个快速测试方法:

在使用标准JDBC代码获得Java持久性时,如果只读标志设置为true,传播模式设置为SUPPORTS,清单5中的@Transactional注释的作用是什么呢?

清单5.将只读标志与SUPPORTS传播模式结合使用—JDBC

@Transactional(readOnly=true,propagation=Propagation.SUPPORTS)

publiclonginsertTrade(TradeDatatrade)throwsException{

//JDBCCode...

}

当执行清单5中的insertTrade()方法时,猜一猜会得到下面哪一种结果:

A.抛出一个只读连接异常

B.正确插入交易订单并提交数据

C.什么也不做,因为传播级别被设置为SUPPORTS

是哪一个呢?

正确答案是B。

交易订单会被正确地插入到数据库中,即使只读标志被设置为true,且事务传播模式被设置为SUPPORTS。

但这是如何做到的呢?

由于传播模式被设置为SUPPORTS,所以不会启动任何事物,因此该方法有效地利用了一个本地(数据库)事务。

只读标志只在事务启动时应用。

在本例中,因为没有启动任何事务,所以只读标志被忽略。

如果是这样的话,清单6中的@Transactional注释在设置了只读标志且传播模式被设置为REQUIRED时,它的作用是什么呢?

清单6.将只读标志与REQUIRED传播模式结合使用—JDBC

@Transactional(readOnly=true,propagation=Propagation.REQUIRED)

publiclonginsertTrade(TradeDatatrade)throwsException{

//JDBCcode...

}

执行清单6中的insertTrade()方法会得到下面哪一种结果呢:

A.抛出一个只读连接异常

B.正确插入交易订单并提交数据

C.什么也不做,因为只读标志被设置为true

根据前面的解释,这个问题应该很好回答。

正确的答案是A。

会抛出一个异常,表示您正在试图对一个只读连接执行更新。

因为启动了一个事务(REQUIRED),所以连接被设置为只读。

毫无疑问,在试图执行SQL语句时,您会得到一个异常,告诉您该连接是一个只读连接。

关于只读标志很奇怪的一点是:

要使用它,必须启动一个事务。

如果只是读取数据,需要事务吗?

答案是根本不需要。

启动一个事务来执行只读操作会增加处理线程的开销,并会导致数据库发生共享读取锁定(具体取决于使用的数据库类型和设置的隔离级别)。

总的来说,在获取基于JDBC的Java持久性时,使用只读标志有点毫无意义,并会启动不必要的事务而增加额外的开销。

使用基于ORM的框架会怎样呢?

按照上面的测试,如果在结合使用JPA和Hibernate时调用insertTrade()方法,清单7中的@Transactional注释会得到什么结果?

清单7.将只读标志与REQUIRED传播模式结合使用—JPA

@Transactional(readOnly=true,propagation=Propagation.REQUIRED)

publiclonginsertTrade(TradeDatatrade)throwsException{

em.persist(trade);

returntrade.getTradeId();

}

清单7中的insertTrade()方法会得到下面哪一种结果:

A.抛出一个只读连接异常

B.正确插入交易订单并提交数据

C.什么也不做,因为readOnly标志被设置为true

正确的答案是B。

交易订单会被准确无误地插入数据库中。

请注意,上一示例表明,在使用REQUIRED传播模式时,会抛出一个只读连接异常。

使用JDBC时是这样。

使用基于ORM的框架时,只读标志只是对数据库的一个提示,并且一条基于ORM框架的指令(本例中是Hibernate)将对象缓存的flush模式设置为NEVER,表示在这个工作单元中,该对象缓存不应与数据库同步。

不过,REQUIRED传播模式会覆盖所有这些内容,允许事务启动并工作,就好像没有设置只读标志一样。

这令我想到了另一个我经常碰到的主要陷阱。

阅读了前面的所有内容后,您认为如果只对@Transactional注释设置只读标志,清单8中的代码会得到什么结果呢?

清单8.使用只读标志—JPA

@Transactional(readOnly=true)

publicTradeDatagetTrade(longtradeId)throwsException{

returnem.find(TradeData.class,tradeId);

}

清单8中的getTrade()方法会执行以下哪一种操作?

A.启动一个事务,获取交易订单,然后提交事务

B.获取交易订单,但不启动事务

正确的答案是A。

一个事务会被启动并提交。

不要忘了,@Transactional注释的默认传播模式是REQUIRED。

这意味着事务会在不必要的情况下启动。

根据使用的数据库,这会引起不必要的共享锁,可能会使数据库中出现死锁的情况。

此外,启动和停止事务将消耗不必要的处理时间和资源。

总的来说,在使用基于ORM的框架时,只读标志基本上毫无用处,在大多数情况下会被忽略。

但如果您坚持使用它,请记得将传播模式设置为SUPPORTS(如清单9所示),这样就不会启动事务:

清单9.使用只读标志和SUPPORTS传播模式进行选择操作

@Transactional(readOnly=true,propagation=Propagation.SUPPORTS)

publicTradeDatagetTrade(longtradeId)throwsException{

returnem.find(TradeData.class,tradeId);

}

另外,在执行读取操作时,避免使用@Transactional注释,如清单10所示:

清单10.删除@Transactional注释进行选择操作

publicTradeDatagetTrade(longtradeId)throwsException{

returnem.find(TradeData.class,tradeId);

}

REQUIRES_NEW事务属性陷阱

不管是使用SpringFramework,还是使用EJB,使用REQUIRES_NEW事务属性都会得到不好的结果并导致数据损坏和不一致。

REQUIRES_NEW事务属性总是会在启动方法时启动一个新的事务。

许多开发人员都错误地使用REQUIRES_NEW属性,认为它是确保事务启动的正确方法。

考虑清单11中的两个方法:

清单11.使用REQUIRES_NEW事务属性

@Transactional(propagation=Propagation.REQUIRES_NEW)

publiclonginsertTrade(TradeDatatrade)throwsException{...}

@Transactional(propagation=Propagation.REQUIRES_NEW)

publicvoidupdateAcct(TradeDatatrade)throwsException{...}

注意,清单11中的两个方法都是公共方法,这意味着它们可以单独调用。

当使用REQUIRES_NEW属性的几个方法通过服务间通信或编排在同一逻辑工作单元内调用时,该属性就会出现问题。

例如,假设在清单11中,您可以独立于一些用例中的任何其他方法来调用updateAcct()方法,但也有在insertTrade()方法中调用updateAcct()方法的情况。

现在如果调用updateAcct()方法后抛出异常,交易订单就会回滚,但帐户更新将会提交给数据库,如清单12所示:

清单12.使用REQUIRES_NEW事务属性的多次更新

@Transactional(propagation=Propagation.REQUIRES_NEW)

publiclonginsertTrade(TradeDatatrade)throwsException{

em.persist(trade);

updateAcct(trade);

//exceptionoccurshere!

Traderolledbackbutaccountupdateisnot!

...

}

之所以会发生这种情况是因为updateAcct()方法中启动了一个新事务,所以在updateAcct()方法结束后,事务将被提交。

使用REQUIRES_NEW事务属性时,如果存在现有事务上下文,当前的事务会被挂起并启动一个新事务。

方法结束后,新的事务被提交,原来的事务继续执行。

由于这种行为,只有在被调用方法中的数据库操作需要保存到数据库中,而不管覆盖事务的结果如何时,才应该使用REQUIRES_NEW事务属性。

比如,假设尝试的所有股票交易都必须被记录在一个审计数据库中。

出于验证错误、资金不足或其他原因,不管交易是否失败,这条信息都需要被持久化。

如果没有对审计方法使用REQUIRES_NEW属性,审计记录就会连同尝试执行的交易一起回滚。

使用REQUIRES_NEW属性可以确保不管初始事务的结果如何,审计数据都会被保存。

这里要注意的一点是,要始终使用MANDATORY或REQUIRED属性,而不是REQUIRES_NEW,除非您有足够的理由来使用它,类似审计示例中的那些理由。

回页首

事务回滚陷阱

我将最常见的事务陷阱留到最后来讲。

遗憾的是,我在生产代码中多次?

?

?

到这个错误。

我首先从SpringFramework开始,然后介绍EJB3。

到目前为止,您研究的代码类似清单13所示:

清单13.没有回滚支持

@Transactional(propagation=Propagation.REQUIRED)

publicTradeDataplaceTrade(TradeDatatrade)throwsException{

try{

insertTrade(trade);

updateAcct(trade);

returntrade;

}catch(Exceptionup){

//logtheerror

throwup;

}

}

假设帐户中没有足够的资金来购买需要的股票,或者还没有准备购买或出售股票,并抛出了一个受控异常(例如FundsNotAvailableException),那么交易订单会保存在数据库中吗?

还是整个逻辑工作单元将执行回滚?

答案出乎意料:

根据受控异常(不管是在SpringFramework中还是在EJB中),事务会提交它还未提交的所有工作。

使用清单13,这意味着,如果在执行updateAcct()方法期间抛出受控异常,就会保存交易订单,但不会更新帐户来反映交易情况。

这可能是在使用事务时出现的主要数据完整性和一致性问题了。

运行时异常(即非受控异常)自动强制执行整个逻辑工作单元的回滚,但受控异常不会。

因此,清单13中的代码从事务角度来说毫无用处;尽管看上去它使用事务来维护原子性和一致性,但事实上并没有。

尽管这种行为看起来很奇怪,但这样做自有它的道理。

首先,不是所有受控异常都是不好的;它们可用于事件通知或根据某些条件重定向处理。

但更重要的是,应用程序代码会对某些类型的受控异常采取纠正操作,从而使事务全部完成。

例如,考虑下面一种场景:

您正在为在线书籍零售商编写代码。

要完成图书的订单,您需要将电子邮件形式的确认函作为订单处理的一部分发送。

如果电子邮件服务器关闭,您将发送某种形式的SMTP受控异常,表示邮件无法发送。

如果受控异常引起自动回滚,整个图书订单就会由于电子邮件服务器的关闭全部回滚。

通过禁止自动回滚受控异常,您可以捕获该异常并执行某种纠正操作(如向挂起队列发送消息),然后提交剩余的订单。

使用Declarative事务模式(本系列的第2部分将进行更加详细的描述)时,必须指定容器或框架应该如何处理受控异常。

在SpringFramework中,通过@Transactional注释中的rollbackFor参数进行指定,如清单14所示:

清单14.添加事务回滚支持—Spring

@Transactional(propagation=Propagation.REQUIRED,rollbackFor=Exception.class)

publicTradeDataplaceTrade(TradeDatatrade)throwsException{

try{

insertTrade(trade);

updateAcct(trade);

returntrade;

}catch(Exceptionup){

//logtheerror

throwup;

}

}

注意,@Transactional注释中使用了rollbackFor参数。

这个参数接受一个单一异常类或一组异常类,您也可以使用rollbackForClassName参数将异常的名称指定为JavaString类型。

还可以使用此属性的相反形式(noRollbackFor)指定除某些异常以外的所有异常应该强制回滚。

通常大多数开发人员指定Exception.class作为值,表示该方法中的所有异常应该强制回滚。

在回滚事务这一点上,EJB的工作方式与SpringFramework稍微有点不同。

EJB3.0规范中的@TransactionAttribute注释不包含指定回滚行为的指令。

必须使用SessionContext.setRollbackOnly()方法将事务标记为

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 法律文书 > 调解书

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1