价格行为交易之陷阱交易_了解交易陷阱

在应用程序中使用事务的最常见原因是要保持高度的数据完整性和一致性。 如果您不担心数据质量,则不必担心交易。 毕竟,Java平台中的事务支持可能会降低性能,引入锁定问题和数据库并发问题,并增加应用程序的复杂性。

但是,不关心事务的开发人员则要承担后果。 几乎所有与业务相关的应用程序都需要高度的数据质量。 仅金融投资行业在失败的交易上就浪费了数百亿美元,而不良数据是第二大原因。 尽管缺乏交易支持只是导致不良数据(尽管是一个主要因素)的一个因素,但是可以推断,仅金融投资行业就浪费了数十亿美元,这是由于缺乏或不良的交易支持所致。

对事务支持的无知是问题的另一个来源。 我经常听到这样的说法:“我们在应用程序中不需要事务支持,因为它们永远不会失败。” 对。 我目睹了一些实际上很少或从未抛出异常的应用程序。 这些应用程序依靠编写良好的代码,编写良好的验证例程以及全面的测试和代码覆盖支持来避免性能成本和与事务处理相关的复杂性。 这种想法的问题在于,它仅考虑了交易支持的一个特征: 原子性 。 原子性确保将所有更新视为一个单元,并全部提交或全部回滚。 但是回滚或协调更新不是事务支持的唯一方面。 隔离是另一个方面,可确保一个工作单元与其他工作单元隔离。 没有适当的事务隔离,其他工作单元就可以访问正在进行的工作单元所做的更新,即使该工作单元不完整也是如此。 结果,可能会基于部分数据制定业务决策,这可能会导致交易失败或其他负面(或代价高昂)的结果。

因此,鉴于不良数据的高昂成本和负面影响,以及交易非常重要(和必要)的基本知识,您需要使用交易并学习如何处理可能出现的问题。 您按下并向您的应用程序添加事务支持。 这就是问题经常开始的地方。 事务似乎并不总是像Java平台所承诺的那样工作。 本文是探究其原因的。 在代码示例的帮助下,我将介绍一些我经常在现场看到和体验的常见事务陷阱,在大多数情况下是在生产环境中。

尽管本文的大多数代码示例都使用Spring Framework(版本2.5),但是事务概念与EJB 3.0规范相同。 在大多数情况下,只需要用EJB 3.0规范中的@TransactionAttribute注释替换Spring Framework @Transactional注释即可。 在两个框架的概念和技术不同的地方,我同时包括了Spring Framework和EJB 3.0源代码示例。

本地交易陷阱

一个简单的好起点是最简单的场景:使用本地事务 ,通常也称为数据库事务 。 在数据库持久性的早期(例如JDBC),我们通常将事务处理委托给数据库。 毕竟,这不是数据库应该做的吗? 对于执行单个插入,更新或删除语句的逻辑工作单元(LUW),本地事务工作正常。 例如,考虑清单1中的简单JDBC代码,该代码将股票交易订单插入到TRADE表中:

清单1.使用JDBC的简单数据库插入
@Stateless
public class TradingServiceImpl implements TradingService {
   @Resource SessionContext ctx;
   @Resource(mappedName="java:jdbc/tradingDS") DataSource ds;

   public long insertTrade(TradeData trade) throws Exception {
      Connection dbConnection = ds.getConnection();
      try {
         Statement sql = dbConnection.createStatement();
         String stmt =
            "INSERT INTO TRADE (ACCT_ID, SIDE, SYMBOL, SHARES, PRICE, STATE)"
          + "VALUES ("
          + trade.getAcct() + "','"
          + trade.getAction() + "','"
          + trade.getSymbol() + "',"
          + trade.getShares() + ","
          + trade.getPrice() + ",'"
          + trade.getState() + "')";
         sql.executeUpdate(stmt, Statement.RETURN_GENERATED_KEYS);
         ResultSet rs = sql.getGeneratedKeys();
         if (rs.next()) {
            return rs.getBigDecimal(1).longValue();
         } else {
            throw new Exception("Trade Order Insert Failed");
         }
      } finally {
         if (dbConnection != null) dbConnection.close();
      }
   }
}

清单1中的JDBC代码不包含事务逻辑,但是将贸易订单保留在数据库的TRADE表中。 在这种情况下,数据库将处理事务逻辑。

对于LUW中的单个数据库维护操作,这一切都很好。 但是假设您需要在将交易订单插入数据库的同时更新帐户余额,如清单2所示:

清单2.用相同的方法执行多个表更新
public TradeData placeTrade(TradeData trade) throws Exception {
   try {
      insertTrade(trade);
      updateAcct(trade);
      return trade;
   } catch (Exception up) {
      //log the error
      throw up;
   }
}

在这种情况下, insertTrade()updateAcct()方法使用标准JDBC代码而不进行事务处理。 一旦insertTrade()方法结束,数据库将持久保存(并提交)交易订单。 如果updateAcct()方法由于任何原因而失败,则贸易订单将保留在placeTrade()方法末尾的TRADE表中,从而导致数据库中的数据不一致。 如果placeTrade()方法使用了交易,则这两个活动都将包含在单个LUW中,并且如果帐户更新失败,则将回滚贸易订单。

随着诸如Hibernate,TopLink和Java Persistence API(JPA)之类的Java持久性框架的普及,我们很少再编写直接的JDBC代码。 更常见的是,我们使用更新的对象关系映射(ORM)框架,通过用几个简单的方法调用替换所有讨厌的JDBC代码,使我们的生活更轻松。 例如,要使用清单1中的JDBC代码,从清单1的JDBC代码示例中插入交易订单,您需要将TradeData对象映射到TRADE表,并用清单3中的JPA代码替换所有的JDBC代码:

清单3.使用JPA的简单插入
public class TradingServiceImpl {
    @PersistenceContext(unitName="trading") EntityManager em;

    public long insertTrade(TradeData trade) throws Exception {
       em.persist(trade);
       return trade.getTradeId();
    }
}

请注意,清单3调用EntityManager上的persist()方法来插入交易订单。 简单吧? 并不是的。 此代码不会按预期将贸易订单插入TRADE表中,也不会引发异常。 它将简单地返回值0作为交易订单的键,而无需更改数据库。 这是事务处理的第一个主要陷阱: 基于ORM的框架需要事务才能触发对象缓存和数据库之间的同步 。 通过事务提交生成SQL代码,并且数据库受到所需操作(即插入,更新,删除)的影响。 没有事务,ORM不会触发生成SQL代码并保留更改的触发器,因此该方法只是简单地结束-没有异常,没有更新。 如果使用的是基于ORM的框架,则必须使用事务。 您不再可以依靠数据库来管理连接和提交工作。

这些简单的示例应清楚表明,必须进行事务处理才能维护数据的完整性和一致性。 但是它们只是开始触及与在Java平台中实现事务相关的复杂性和陷阱的表面。

Spring Framework @Transactional注释陷阱

因此,您测试了清单3中的代码,并发现persist()方法在没有事务的情况下不起作用。 结果,您通过简单的Internet搜索查看了一些链接, @Transactional现在Spring Framework中,您需要使用@Transactional批注。 因此,将注释添加到代码中,如清单4所示:

清单4.使用@Transactional批注
public class TradingServiceImpl {
   @PersistenceContext(unitName="trading") EntityManager em;

   @Transactional
   public long insertTrade(TradeData trade) throws Exception {
      em.persist(trade);
      return trade.getTradeId();
   }
}

您重新测试您的代码,发现它仍然不起作用。 问题在于您必须告诉Spring Framework您正在使用注释进行事务管理。 除非您要进行完整的单元测试,否则有时很难发现这个陷阱。 通常,这会使开发人员仅在Spring配置文件中添加事务逻辑,而不是通过注释。

在Spring中使用@Transactional批注时,必须将以下行添加到Spring配置文件中:

<tx:annotation-driven transaction-manager="transactionManager"/>

transaction-manager属性保存对Spring配置文件中定义的事务管理器bean的引用。 此代码告诉Spring在应用事务拦截器时使用@Transaction批注。 没有它, @Transactional注释被忽略,导致无交易在你的代码中使用。

使基本的@Transactional批注在清单4中的代码中起作用仅是开始。 请注意,清单4使用@Transactional批注而不指定任何其他批注参数。 我发现许多开发人员使用@Transactional批注而没有花时间完全了解它的作用。 例如,当像清单4一样@Transactional使用@Transactional批注时,事务传播模式设置为什么? 只读标志设置为什么? 事务隔离级别设置为什么? 更重要的是,交易何时应撤回工作? 了解如何使用此批注对于确保您的应用程序中具有适当级别的事务支持很重要。 为了回答我刚刚问的问题:当@Transactional使用@Transactional批注而不使用任何参数时,传播模式设置为REQUIRED ,只读标志设置为false ,事务隔离级别设置为数据库默认值(通常为READ_COMMITTED ),并且事务不会在检查到的异常时回滚。

@Transactional只读标志陷阱

我在旅行中经常遇到的一个常见陷阱是对Spring @Transactional批注中的只读标志的不正确使用。 这是一个快速的测验:当使用标准的JDBC代码进行Java持久化时,清单5中的@Transactional注释在将只读标志设置为true并将传播模式设置为SUPPORTS什么?

清单5.在SUPPORTS传播模式下使用只读SUPPORTS
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)
public long insertTrade(TradeData trade) throws Exception {
   //JDBC Code...
}

当清单5中的insertTrade()方法执行时,执行以下操作:

  • 引发只读连接异常
  • 正确插入交易订单并提交数据
  • 由于传播级别设置为SUPPORTS因此不执行任何操作

放弃? 正确的答案是B。即使将只读标志设置为true并且将交易传播设置为SUPPORTS ,也可以将贸易订单正确地插入数据库中。 但是那怎么可能呢? 由于SUPPORTS传播模式,没有事务开始,因此该方法有效地使用了本地(数据库)事务。 只读标志仅在事务开始时才应用。 在这种情况下,没有事务开始,因此只读标志将被忽略。

好的,如果是这样,当设置了只读标志并且将传播模式设置为REQUIRED时,清单6中的@Transactional注释会做什么?

清单6.在REQUIRED传播模式下使用只读— JDBC
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception {
   //JDBC code...
}

执行后,执行清单6中的insertTrade()方法:

  • 引发只读连接异常
  • 正确插入交易订单并提交数据
  • 不执行任何操作,因为只读标志设置为true

鉴于先前的解释,这一答案应该很容易回答。 正确的答案是A。将引发异常,表明您正在尝试对只读连接执行更新操作。 由于事务已启动( REQUIRED ),因此连接设置为只读。 果然,当您尝试执行SQL语句时,会收到一个异常,告诉您该连接是只读连接。

关于只读标志的奇怪之处在于,您需要启动一个事务才能使用它。 如果仅读取数据,为什么需要进行交易? 答案是你不知道。 启动事务以执行只读操作会增加处理线程的开销,并可能导致数据库上的共享读取锁定(取决于您使用的数据库类型以及设置的隔离级别)。 最重要的是,当您将只读标志用于基于JDBC的Java持久性时,它有些无意义,并且在启动不必要的事务时会导致额外的开销。

当您使用基于ORM的框架时该怎么办? 与测验的格式保持一致,您是否可以猜出,如果使用JPA和Hibernate调用insertTrade()方法,清单7中的@Transactional注释的结果是什么?

清单7.在REQUIRED传播模式下使用只读-JPA
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception {
   em.persist(trade);
   return trade.getTradeId();
}

清单7中的insertTrade()方法是否可以:

  • 引发只读连接异常
  • 正确插入交易订单并提交数据
  • 不执行任何操作,因为readOnly标志设置为true

这个问题的答案有些棘手。 在某些情况下,答案是C,但是在大多数情况下(尤其是在使用JPA时),答案是B。交易订单已正确插入数据库中,没有错误。 等一下-前面的示例显示,当使用REQUIRED传播模式时,将引发只读连接异常。 当您使用JDBC时,这是正确的。 但是,当您使用基于ORM的框架时,只读标志的工作方式略有不同。 当您在插入上生成密钥时,ORM框架将转到数据库以获取密钥并随后执行插入。 对于某些供应商(例如Hibernate),刷新模式将设置为MANUAL ,并且对于未生成密钥的插入不会发生插入。 更新也是如此。 但是,当只读标记设置为true时,其他供应商(如TopLink)将始终执行插入和更新。 尽管这是特定于供应商和版本的,但此处的要点是,不能保证在设置了只读标志时(尤其是在与供应商无关的JPA中使用JPA时)不会发生插入或更新。

这使我陷入了我经常遇到的另一个重大陷阱。 到目前为止,您已经阅读了所有内容,如果仅在@Transactional批注上设置只读标志,那么您认为清单8中的代码会做什么?

清单8.使用只读— JPA
@Transactional(readOnly = true)
public TradeData getTrade(long tradeId) throws Exception {
   return em.find(TradeData.class, tradeId);
}

清单8中的getTrade()方法:

  • 开始交易,获取交易订单,然后提交交易
  • 无需开始交易即可获取贸易订单

正确答案是A。事务已启动并提交。 不要忘记: @Transactional批注的默认传播模式是REQUIRED 。 这意味着实际上不需要事务时将启动事务(请参阅永不说永不 )。 。 根据您使用的数据库,这可能会导致不必要的共享锁,从而导致数据库中可能出现死锁情况。 另外,开始和停止事务正在消耗不必要的处理时间和资源。 底线是,当您使用基于ORM的框架时,只读标志是非常无用的,并且在大多数情况下会被忽略。 但是,如果您仍然坚持使用它,请始终将传播模式设置为SUPPORTS ,如清单9所示,这样就不会启动任何事务:

清单9.使用只读和SUPPORTS传播模式进行选择操作
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)
public TradeData getTrade(long tradeId) throws Exception {
   return em.find(TradeData.class, tradeId);
}

更好的是,只是在执行读取操作时完全避免使用@Transactional批注,如清单10所示:

清单10.删除选择操作的@Transactional批注
public TradeData getTrade(long tradeId) throws Exception {
   return em.find(TradeData.class, tradeId);
}

REQUIRES_NEW交易属性陷阱

无论您使用的是Spring Framework还是EJB,使用REQUIRES_NEW事务属性都可能导致负面结果,并导致数据损坏和不一致。 启动方法时,无论是否存在现有事务, REQUIRES_NEW事务属性始终会启动一个新事务。 假定这是确保启动事务的正确方法,许多开发人员错误地使用了REQUIRES_NEW属性。 考虑清单11中的两种方法:

清单11.使用REQUIRES_NEW事务属性
@Transactional(propagation=Propagation.REQUIRES_NEW)
public long insertTrade(TradeData trade) throws Exception {...}

@Transactional(propagation=Propagation.REQUIRES_NEW)
public void updateAcct(TradeData trade) throws Exception {...}

请注意,在清单11中,这两种方法都是公共的,这意味着它们可以彼此独立地调用。 当通过服务间通信或业务流程在同一逻辑工作单元内调用使用它的方法时, REQUIRES_NEW属性会出现问题。 例如,假设在清单11中,在某些用例中,您可以独立于任何其他方法来调用updateAcct()方法,但是在某些情况下,在insertTrade()方法中也会调用updateAcct() insertTrade()方法。 现在,如果在updateAcct()方法调用之后发生异常,贸易订单将被回滚,但是帐户更新将被提交给数据库,如清单12所示:

清单12.使用REQUIRES_NEW事务属性的多个更新
@Transactional(propagation=Propagation.REQUIRES_NEW)
public long insertTrade(TradeData trade) throws Exception {
   em.persist(trade);
   updateAcct(trade);
   //exception occurs here! Trade rolled back but account update is not!
   ...
}

发生这种情况是因为在updateAcct()方法中启动了一个新事务,因此一旦updateAcct()方法结束,该事务便会提交。 当您使用REQUIRES_NEW事务属性时,如果存在现有事务上下文,则当前事务将被挂起并开始新的事务。 该方法结束后,将提交新事务,并继续原始事务。

由于这种行为,仅当被调用方法中的数据库操作需要保存到数据库时才使用REQUIRES_NEW事务属性,而不管重叠事务的结果如何。 例如,假设每次尝试进行的股票交易都必须记录在审计数据库中。 无论交易是否由于验证错误,资金不足或其他原因而失败,都必须保留此信息。 如果您未在审核方法上使用REQUIRES_NEW属性,则审核记录将与尝试的交易一起回滚。 使用REQUIRES_NEW属性可确保无论初始交易的结果如何,都将保存审核数据。 这里的重点始终是使用MANDATORYREQUIRED属性而不是REQUIRES_NEW除非您出于与审计示例类似的原因而有理由使用它。

交易回滚陷阱

最后,我已保存了最常见的交易陷阱。 不幸的是,我在生产代码中多次看到此代码。 我将从Spring框架开始,然后继续到EJB 3。

到目前为止,您一直在查看的代码类似于清单13:

清单13.不支持回滚
@Transactional(propagation=Propagation.REQUIRED)
public TradeData placeTrade(TradeData trade) throws Exception {
   try {
      insertTrade(trade);
      updateAcct(trade);
      return trade;
   } catch (Exception up) {
      //log the error
      throw up;
   }
}

假设该帐户没有足够的资金来购买有问题的股票,或者尚未设置为购买或出售股票,并且抛出了检查异常(例如, FundsNotAvailableException )。 贸易订单是否保留在数据库中,还是整个逻辑工作单元都回滚了? 令人惊讶的答案是,在检查到异常(在Spring Framework或EJB中)后,事务将提交尚未提交的任何工作。 使用清单13,这意味着如果在updateAcct()方法期间发生了检查异常,则交易订单将updateAcct() ,但不会更新帐户以反映交易。

使用事务时,这可能是主要的数据完整性和一致性问题。 运行时异常(即未检查的异常)会自动强制回滚整个逻辑工作单元,但检查的异常不会自动回滚。 因此,从事务的角度来看,清单13中的代码是无用的。 尽管看起来它使用事务来维护原子性和一致性,但实际上并非如此。

尽管这种行为可能看起来很奇怪,但是出于某些良好的原因,事务的行为却如此。 首先,并非所有检查的异常都是不好的; 它们可能用于事件通知或根据某些条件重定向处理。 但更重要的是,应用程序代码可能能够对某些类型的已检查异常采取纠正措施,从而使事务得以完成。 例如,考虑您正在为在线图书零售商编写代码的情况。 要完成预订,您需要在订购过程中发送电子邮件确认。 如果电子邮件服务器已关闭,则将发送某种SMTP检查的异常,表明无法发送邮件。 如果检查的异常导致自动回滚,则仅由于电子邮件服务器已关闭而回滚整个预订订单。 通过不自动回退已检查的异常,您可以捕获该异常并执行某种纠正措施(例如将消息发送到挂起的队列)并提交其余订单。

当使用声明式事务模型(在本系列的第2部分中将更详细描述)时,必须指定容器或框架应如何处理已检查的异常。 在Spring Framework中,您可以通过@Transactional批注中的rollbackFor参数来指定它,如清单14所示:

清单14.添加事务回滚支持-Spring
@Transactional(propagation=Propagation.REQUIRED, rollbackFor=Exception.class)
public TradeData placeTrade(TradeData trade) throws Exception {
   try {
      insertTrade(trade);
      updateAcct(trade);
      return trade;
   } catch (Exception up) {
      //log the error
      throw up;
   }
}

注意@Transactional批注中使用rollbackFor参数。 此参数接受单个异常类或一组异常类,或者可以使用rollbackForClassName参数将异常的名称指定为Java String类型。 您还可以使用此属性的否定版本( noRollbackFor )指定除某些例外之外,所有例外都应强制回滚。 通常,大多数开发人员将Exception.class指定为值,指示此方法中的所有异常都应强制回滚。

在回滚事务方面,EJB的工作方式与Spring框架略有不同。 EJB 3.0规范中的@TransactionAttribute批注不包含用于指定回滚行为的指令。 相反,您必须使用SessionContext.setRollbackOnly()方法将事务标记为回滚,如清单15所示:

清单15.添加事务回滚支持— EJB
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public TradeData placeTrade(TradeData trade) throws Exception {
   try {
      insertTrade(trade);
      updateAcct(trade);
      return trade;
   } catch (Exception up) {
      //log the error
      sessionCtx.setRollbackOnly();
      throw up;
   }
}

一旦调用setRollbackOnly()方法,您将无法改变主意。 唯一可能的结果是在完成启动事务的方法后回滚事务。 本系列后续文章中介绍的事务策略将提供有关何时何地使用回滚指令以及何时使用REQUIRED MANDATORY事务属性的指导。

结论

用于在Java平台中实现事务的代码并不是太复杂; 但是,如何使用和配置它可能会有些复杂。 许多陷阱与在Java平台中实现事务支持有关(包括一些我不曾在这里讨论的较不常见的陷阱)。 其中大多数的最大问题是没有编译器警告或运行时错误告诉您事务实现不正确。 此外,与本文开头的“ 迟到总比没有好 ”的轶事所反映的假设相反,实现事务支持不仅是编码活动。 开发整体交易策略需要大量的设计工作。 事务策略系列的其余部分将帮助您指导如何为从简单应用程序到高性能事务处理的用例设计有效的事务策略。


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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值