事务策略api层策略

    技术2024-01-26  101

    无论您是使用带有EJB 2.1或3.0的容器环境,Spring Framework环境,还是带有Java Open Transaction Manager(JOTM)的Web容器环境(例如Tomcat或Jetty),您仍然需要一种事务策略来确保数据库的一致性和完整性。 。 Java Transaction API(JTA)指定了与事务处理关联的语法和接口(请参阅参考资料 ),但是没有描述如何将这些构建块放在一起。 正如施工人员需要用一堆木材建造一栋房子的蓝图一样,您需要一种策略来描述如何将事务性构建基块组合在一起。

    关于本系列

    事务可提高数据的质量,完整性和一致性,并使您的应用程序更强大。 在Java应用程序中成功事务处理的实现不是一件容易的事,它与设计以及编码有关。 在本系列中 ,Mark Richards是您为从简单应用程序到高性能事务处理的用例设计有效事务策略的指南。

    我在本文中介绍的策略是API层事务策略 。 这是最健壮,最简单和最容易实现的交易策略。 这种简单性带来了我也描述的局限性和注意事项。 我在代码示例中使用EJB 3.0规范。 Spring Framework和JOTM的概念相同。

    基本结构

    API层事务策略之所以得名,是因为所有事务逻辑都包含在逻辑应用程序体系结构的API层中。 该层(有时称为应用程序的域层或外观层 )是逻辑层,以公共方法或接口的形式向客户端(或表示层)公开功能。 我之所以说是合乎逻辑的 ,是因为您可以本地访问域层(通过直接实例化和调用),也可以通过HTTP,远程方法调用(RMI),基于Internet的Orb跨Internet协议的RMI(RMI-IIOP),甚至是EJB来访问域层。 Java消息服务(JMS)。

    图1展示了大多数Java应用程序的典型逻辑应用程序层堆栈:

    图1.体系结构层和事务逻辑

    图1中的体系结构实现了API层事务策略。 包含事务逻辑的类以红色阴影显示。 请注意,这些仅由应用程序体系结构的域类(API层)组成。 客户端层,业务层和持久层不包含事务逻辑,这意味着这些层不启动,提交或回滚事务,也不包含事务注释,例如EJB 3.0中的@TransactionAttribute注释。 在整个应用程序体系结构中,启动,提交和回滚事务的唯一方法是API层的域类中包含的公共方法。 这就是为什么API层是最强大,最容易实现的交易策略的原因。

    不要挂在图1显示了四层这一事实上。 您的应用程序体系结构可以具有更多或更少的层。 您可以将表示层和域层组合在一个WAR文件中,或者您的域类可能在单独的EAR文件中。 您可能将业务逻辑放置在域类中为一层而不是两层。 与交易策略的工作方式或实施方式无关。

    此事务处理策略非常适合具有粗粒度API层的应用程序。 并且由于表示层不包含任何事务逻辑(即使对于更新请求也是如此),因此该策略非常适合必须支持多个客户端通道的应用程序,包括Web服务客户端,桌面客户端和远程客户端。 但是,这种灵活性要付出一定的代价-即,将客户端层限制为对给定事务性工作单元的单个请求。 我将在本文后面解释为什么需要此限制。

    策略设置和特征

    以下规则和特征适用于API层交易策略:

    只有应用程序体系结构的API层中包含的公共方法才包含事务逻辑。 其他方法,类或组件均不应包含事务逻辑(包括事务注释,程序化事务逻辑和回滚逻辑)。 API层中的所有公共写入方法(插入,更新和删除)都应标记为REQUIRED的事务属性。 API层中的所有公共写入方法(插入,更新和删除)都应包含回滚逻辑,以将事务标记为在检查到的异常时进行回滚。 默认情况下,API层中的所有公共读取方法都应标记为SUPPORTS的事务属性。 (请参阅“ 事务策略:了解事务陷阱 。”中的“ 永不说永不”边栏。)这将确保如果在该作用域的上下文中调用read方法,则该方法将包含在该作用域中。 否则,它将在没有事务上下文的情况下运行,并假定它是逻辑工作单元(LUW)中唯一被调用的方法。 我在这里假设读取操作(作为API层的入口点)并不会在数据库上调用写入操作。 来自API层的事务将传播到事务所有者 (在下一节中定义) 下调用的所有方法。 声明式事务模型通常用于此模式,并假设API层类由Java EE容器环境或其他框架(如Spring)管理。 如果不是,那么您很可能需要使用程序化交易模型。 (有关这些交易模型的更多信息,请参见“ 交易策略:模型和策略概述 ”。)

    如果仔细观察,考虑到我刚刚列出的规则,您可能会注意到此策略有一个小问题。 由于服务间通信,一种公共API层更新方法可以调用另一种公共API层更新方法。 由于这两种更新方法都是公共的,因此从客户端层的角度作为API层入口点公开,因此它们包含回滚逻辑。 但是,如果一个公共更新方法调用了另一个方法,则事务所有者在某些情况下可能无法控制回滚逻辑。 因此,如果交易所有者重新提交交易或采取纠正措施,则必须格外小心。 在这些情况下,您可能需要重构业务流程或处理逻辑以避免这种情况。

    局限性

    交易策略的限制之一是,对于任何给定的交易工作单元,客户端层(或表示层)类只能对API层进行一次调用。 这使得该交易策略不适用于“聊天”应用程序。 不幸的是,这是一个全有或全无的主张,并且在某些情况下可能需要应用程序重构(在本节后面介绍)。 让我解释一下为什么这对于此交易策略如此重要(和必要)。

    我将在整个交易策略系列中描述的所有交易策略都适用两个黄金法则(秘诀):

    启动事务的方法被指定为事务所有者 只有交易所有者可以回滚交易

    如果您不遵守这些规则,则交易策略将不起作用。 您很可能最终会遇到导致数据不一致和数据完整性差的问题。 第二个规则很重要,原因有几个。 首先,如果某个方法没有启动事务,则它没有业务来管理该事务(例如,将其标记为回滚)。 其次,如果链中较低级别的方法调用使事务回滚,则事务所有者将无法采取纠正措施来修复并重新提交事务。 一旦将其标记为回滚,这是唯一可能的结果。 您不能“撤消”交易。

    回到原点:与API层事务策略,客户端必须不会在单个工作单位多次调用API层需要交易。 如果客户端为单个LUW多次调用API,则事务工作单元必须在客户端开始和结束。 在这种情况下,API层方法必须具有MANDATORY的事务属性,而没有任何回滚逻辑。 请记住黄金法则:调用API层方法的客户端方法是事务所有者,只有事务所有者才应负责回滚。

    考虑图2所示的示例,其中客户端(客户端A)向API层(域模型)发出请求以将股票交易插入数据库中:

    图2. MANDATORY属性的使用

    在这种情况下,客户端正在开始交易; 因此是REQUIRED交易属性。 请注意,客户也有回滚责任,遵守我的两个黄金法则。 域类正确地具有MANDATORY事务属性,因为客户MANDATORY在开始事务,并且域模型( insertTrade() )不负责回滚。

    此策略对于图2中所示的场景是正确的。但是,假设您有另一个客户端应用程序(客户端B)需要使用相同的域模型( insertTrade() ),如图3所示:

    图3.非交易客户端问题

    请注意,客户B并未开始交易。 (它可能是无法使用事务的远程HTTP客户端,消息传递客户端或其他Java应用程序。)因为域模型方法被标记为MANDATORY ,所以客户端B将遇到TransactionRequiredException指示需要事务来调用该方法。

    没有适当的交易策略,通常解决此问题的方法是将域模型( insertTrade() )上的交易属性更改为REQUIRED 。 现在,如果您返回到客户端A的调用,您会发现您没有破坏任何东西。 客户端A开始事务,然后将事务上下文传递给域模型方法。 因为现在将domain-model方法标记为REQUIRED ,所以它使用现有的事务上下文。 请注意,domain-model方法不包含回滚逻辑。 域模型完成后(是否发生异常),控制权被传递回客户端。 所有这一切都对客户端A正常工作。但是,如果您查看客户端B,就会遇到问题:因为不存在任何事务上下文,所以域模型方法( insertTrade() )将启动一个新事务,但是在发生受检查的异常,因为域模型方法不负责回滚,所以不执行任何回滚。 图4说明了这种错误情况:

    图4.使用REQUIRED属性而无需回滚

    在没有适当的交易策略的情况下通常解决此问题的方法是将回滚逻辑添加到域模型方法中,以满足来自客户端B的调用。但是,如图5所示,这现在在客户端A中引起问题:

    图5.客户端回滚问题

    如果客户端A尝试回滚事务,不仅会收到异常,而且由于事务已被标记为回滚,因此客户端A无法采取纠正措施。

    乒乓。 节奏继续...

    这就是为什么交易策略如此重要,也为什么它们必须绝对的原因。 如前所述,您会发现自己的应用程序对LUW请求有85%的单API层调用和15%的多API层调用。 如果是这种情况(或类似的情况),您有两种选择:不协调事务性工作单元中的多个调用(这是个坏主意),或者-最好将多个API调用重构为一个使用汇总API层方法的单个API调用。

    为了说明这种重构技术,假设您有两个API层方法, insertTrade()和updateAcct() ,如清单1所示:

    清单1.多个API层方法
    @Stateless @Remote(TradingService.class) public class TradingServiceImpl implements TradingService { @PersistenceContext(unitName="trading") EntityManager em; @Resource SessionContext ctx; @TransactionAttribute(TransactionAttributeType.REQUIRED) public long insertTrade(TradeData trade) throws Exception { try { em.persist(trade); return trade.getTradeId(); } catch (Exception up) { ctx.setRollbackOnly(); throw up; } } @TransactionAttribute(TransactionAttributeType.REQUIRED) public void updateAcct(TradeData trade) throws Exception { try { //update account balance based on buy or sell... } catch (Exception up) { ctx.setRollbackOnly(); throw up; } } }

    假定这两种方法都可以作为独立的操作来调用。 但是,有时如清单2所示,客户机可以在同一个LUW中调用这两个方法:

    清单2.在同一客户端方法中执行多个表更新
    public TradeData invokeClientRequest(TradeData trade) throws Exception { try { insertTrade(trade); updateAcct(trade); return trade; } catch (Exception up) { //log the error throw up; } }

    与其通过在客户端层放置事务来弄乱一切,不如通过在TradingServiceImpl类和相应的TradingService接口中创建新的聚合方法来删除多API层调用。 清单3显示了新的聚合方法(为简便起见,未显示接口代码):

    清单3.添加一个公共聚合方法
    @Stateless @Remote(TradingService.class) public class TradingServiceImpl implements TradingService { @PersistenceContext(unitName="trading") EntityManager em; @Resource SessionContext ctx; @TransactionAttribute(TransactionAttributeType.REQUIRED) public TradeData placeTrade(TradeData trade) throws Exception { try { insertTrade(trade); updateAcct(trade); return trade; } catch (Exception up) { ctx.setRollbackOnly(); throw up; } } @TransactionAttribute(TransactionAttributeType.REQUIRED) public long insertTrade(TradeData trade) throws Exception { ... } @TransactionAttribute(TransactionAttributeType.REQUIRED) public void updateAcct(TradeData trade) throws Exception { ... } }

    重构的客户端代码如清单4所示:

    清单4.重构的客户端方法仅进行一次API层调用
    public TradeData invokeClientRequest(TradeData trade) throws Exception { try { return placeTrade(trade); } catch (Exception up) { //log the error throw up; } }

    注意,客户端方法现在正在对API层中名为placeTrade()的新聚合方法进行单个调用。 insertTrade()和updateAcct()方法仍然是公共的,因为无论新的聚合方法如何,它们也可以彼此独立地调用。

    尽管我仅出于说明目的使用一个简单示例,但并不是要琐碎这种重构技术的可能复杂性。 在某些情况下,尤其是使用基于Web的对象(例如HTTPServletRequest或HTTPSession客户端代码时,重构可能很复杂,并且涉及重大的代码更改。 您需要在重构客户端和服务器代码的工作量与数据完整性和一致性的需求和重要性之间进行权衡。 就是说,让我戴上我实用的帽子一会儿。 为了逐步迈向可靠的交易策略(例如本文所述的API层交易策略),可以将交易逻辑临时添加到客户端代码中,从而使两个API层调用在同一交易单元内保持协调(确保API层仍具有REQUIRED属性设置)。 但是,您应该了解这样做的含义:

    您将需要在客户端方法中使用程序化事务(请参阅“ 事务策略:模型和策略概述 ”)。 如果API层方法将事务标记为回滚,则需要将事务回滚包装在try / catch块中。 您将无法对异常采取纠正措施。 您将受限于客户端和API层之间使用的通信协议(例如,没有HTTP,没有JMS,等等)。

    请注意,通过逐步实施此事务策略,直到您完成重构工作后,您才能拥有坚实而强大的事务策略。

    交易策略实施

    API层交易策略的实施非常简单。 因为唯一包含事务逻辑的层是API层,所以我将仅在该层的域模型类中显示事务逻辑。

    从策略设置和特性中回想起,对于写操作(更新,插入和删除),公共API层方法应具有REQUIRED的事务属性,并包含事务回滚逻辑。 默认情况下,公共读取操作应具有SUPPORTS的事务属性,并且没有回滚逻辑。 下面的清单5展示了这种事务策略的实现:

    清单5.实施API层策略
    @Stateless @Remote(TradingService.class) public class TradingServiceImpl implements TradingService { @PersistenceContext(unitName="trading") EntityManager em; @Resource SessionContext ctx; @TransactionAttribute(TransactionAttributeType.REQUIRED) public long insertTrade(TradeData trade) throws Exception { try { em.persist(trade); return trade.getTradeId(); } catch (Exception up) { ctx.setRollbackOnly(); throw up; } } @TransactionAttribute(TransactionAttributeType.SUPPORTS) public TradeData getTradeOrder(long tradeId) { return em.find(TradeData.class, tradeId); } }

    实现此策略的一种优化方法是利用EJB 3.0中@TransactionAttribute批注的TYPE范围,默认情况下使整个类中的所有方法都REQUIRED ,并且仅将对SUPPORTS的读取操作覆盖。 清单6中说明了这种技术:

    清单6.优化API层策略的实现
    @Stateless @Remote(TradingService.class) @TransactionAttribute(TransactionAttributeType.REQUIRED) public class TradingServiceImpl implements TradingService { @PersistenceContext(unitName="trading") EntityManager em; @Resource SessionContext ctx; public long insertTrade(TradeData trade) throws Exception { try { em.persist(trade); return trade.getTradeId(); } catch (Exception up) { ctx.setRollbackOnly(); throw up; } } @TransactionAttribute(TransactionAttributeType.SUPPORTS) public TradeData getTradeOrder(long tradeId) { return em.find(TradeData.class, tradeId); } }

    我之所以建议使用这种方法,而不是默认情况下使所有SUPPORTS成为默认,是因为如果您忘记编写@TransactionAttribute批注,那么最好是在进行事务而不是不进行事务方面犯错。

    结论

    API层交易策略将适用于大多数业务应用程序。 它简单,简单,易于实现且功能强大。 您可能会面临一些应用程序重构以实现此策略的需求,但是从长远来看,您会发现,具有高水平的数据一致性和完整性所节省的费用将远远超过所做的努力。 记住,我在本文中描述的是一种交易策略 。 它涉及的不仅仅是简单的实现任务。 团队中的每个开发人员都应该了解和理解所使用的交易策略,能够描述它,并且最重要的是能够遵守它。

    尽管API层交易策略是最常见的策略,但它可能不适用于您的应用程序。 例如,有时在单个LUW中单个API调用与多个API调用之间的分配百分比颠倒了(例如,单个API层调用占20%,而多个API层调用占80%)。 在这种情况下,您可能不想承担如此庞然大物的重构工作。 那是您使用Client Orchestration交易策略的时候,我将在本系列的下一部分中进行讨论。 否则API层事务策略中实施的事务持续时间可能太长,从而导致数据库并发问题,吞吐量降低,连接等待时间减少,在极端情况下还会导致数据库死锁。 这些症状通常出现在高并发环境中,在该环境中,应用程序必须处理大量用户或负载。 在这种情况下,您将使用“高并发”交易策略-本系列第五篇文章的主题。 最后,您可能会发现自己处于一种标准的应用程序体系结构中,但该体系结构是为满足每毫秒处理时间至关重要的高速需求而设计的。 对于这些情况,您将要使用“高速交易”策略,这将在第六部分中介绍。


    翻译自: https://www.ibm.com/developerworks/java/library/j-ts3/index.html

    Processed: 0.022, SQL: 9