高并发策略实例分析_高并发策略

本文探讨了高并发交易策略,一种旨在减少事务范围以提高数据库并发性和应用性能的方法。通过对比API层策略,介绍了先读技术和低级技术两种实现方式,帮助理解如何在保持数据完整性和一致性的同时,满足高并发需求。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我在本系列前几篇文章中介绍的API层客户编排交易策略是适用于大多数标准业务应用程序的核心策略。 它们简单,相对容易实施且健壮,并且它们提供了最高级别的数据完整性和一致性。 但是,有时您需要缩小事务范围以提高吞吐量,提高性能并增加数据库的并发性。 您如何做到这一点,并且仍然保持高水平的数据完整性和一致性? 答案是使用高并发交易策略。

高并发策略源自API层策略。 API层策略虽然牢固可靠,但确实有一些缺点。 始终在调用堆栈(API层)的最高级别上启动事务可能会效率低下,尤其是在用户吞吐量和数据库并发需求很高的情况下。 除非特定的业务要求,否则将事务保留的时间超过必要的时间会消耗额外的资源,更长的锁定时间以及将资源的保留时间保持在必要的时间之外。

像API层策略一样,高并发策略使客户端层免于承担任何交易责任。 但是,这也意味着对于任何给定的逻辑工作单元(LUW),您只能从客户端层进行一次呼叫。 高并发策略旨在减少事务的总体范围,以便锁定资源的时间更少,从而提高了应用程序的吞吐量,并发性和性能特征。

使用此策略可能获得的收益在某种程度上取决于您正在使用哪个数据库以及如何配置它。 某些数据库(例如使用InnoDB引擎的Oracle和MySQL)不持有读取锁,而其他数据库(例如不具有快照隔离级别SQL Server)则没有读取锁。 您持有的锁越多,无论是共享锁还是排他锁,对数据库(并因此影响您的应用程序)的并发性,性能和吞吐量的影响就越大。

但是在数据库中获取和持有锁只是高并发故事的一部分。 并发性和吞吐量也与释放锁有关。 无论使用哪种数据库,将事务保留的时间长于所需时间,将使共享和独占锁的保留时间长于必要时间。 在高并发情况下,这可能导致数据库将锁升级级别从行级锁提升到页级锁,并且在极端情况下将页级锁提升到表级锁。 在大多数情况下,您无法控制数据库引擎用于选择何时升级锁定级别的启发式方法。 一些数据库(例如SQL Server)允许您禁用页面级锁定,以希望它不会从行级锁定升级为表级锁定。 有时候,这种赌博是可行的,但是在大多数情况下,您可能不会注意到您可能希望的并发改善。

最重要的是,在高数据库并发情况下,持有数据库锁(共享或独占)的时间越长,发生以下任何问题的可能性就越大:

  • 数据库连接用完,从而导致应用程序中的等待状态
  • 由于共享锁和排他锁导致的死锁,导致性能不佳和事务失败
  • 将锁定升级为页面级或表级锁定

换句话说,应用程序在数据库中的时间越长,应用程序可以处理的并发性就越低。 我列出的任何问题都将导致您的应用程序运行缓慢,并直接降低整体吞吐量和性能-从而导致应用程序处理大量并发用户负载的能力。

权衡取舍

高并发策略通过将事务的范围缩小到体系结构中的最低级别来解决高并发需求。 这导致事务完成(提交或回滚)的速度比API层事务策略中的速度更快。 但是,您可能已经从好船Vasa的故事中学到了(请参阅参考资料 ),但您不可能一无所有 。 生命全在于权衡,交易处理也不例外。 您根本无法期望通过API层策略提供相同类型的健壮事务处理,并且同时在峰值负载下提供最大的用户并发性和吞吐量。

那么,使用高并发交易策略时您会放弃什么呢? 根据应用程序的设计方式,即使将读取操作用于更新意图,也可能会强制执行读取操作。 “等待!” 您说:“您不能这样做-您可能最终会更新自上次读取以来已更改的数据!” 这是一个有效的关注点,也是折衷游戏开始的地方。 使用此策略,因为您没有持有数据的读取锁,所以在执行更新操作时出现陈旧数据异常的机会增加了。 但是,就像使用Vasa一样 ,一切都取决于哪个特征更重要:可靠的防弹交易策略(例如API层策略)或高用户并发性和吞吐量。 在高并发情况下,很难同时拥有这两种情况,如果尝试这样做,最终可能会遇到应用程序运行不佳的情况。

第二个折衷方案是总体缺乏交易稳定性。 与API层或客户端编排策略相比,此策略难以实施,需要更长的开发和测试时间,并且更容易出错。 考虑到这些折衷,您应该首先分析您的当前情况,以确定使用此策略是否正确。 由于“高并发性”策略是API层策略的衍生产品,因此一种好的方法是从“ API层”策略入手,并以较高的用户负载(高于峰值负载时的预期负载)对应用程序进行负载测试。 如果发现吞吐量,性能低下,等待时间长甚至出现死锁,请开始使用高并发策略。

在本文的其余部分,我将向您展示High Concurrency交易策略的其他一些特性以及实现它的两种方法。

基本结构和特点

图1通过我在整个交易策略系列中使用的逻辑应用程序堆栈说明了高并发交易策略。 包含事务逻辑的类以红色阴影显示。

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

API层策略的某些特征和规则适用-但并非全部。 注意, 图1中的客户端层没有事务逻辑,这意味着任何类型的客户端都可以用于此事务策略,包括基于Web的,桌面的,Web服务和Java消息服务(JMS)。 并观察到事务处理策略分布在客户端下面的所有层中,但并不完全。 有些事务可能始于API层,某些事务可能始于业务层,有些甚至始于DAO层。 缺乏一致性是这是一项难以实施,维护和管理的硬策略的原因之一。

在大多数情况下,您会发现需要使用程序化交易模型来缩小交易范围,但有时仍可以使用声明性交易模型 。 但是,通常不能在同一应用程序中混合使用程序化和声明性事务模型。 使用这种交易策略时,最好坚持使用程序化交易模型,这样您就不会遇到麻烦。 但是,如果你发现自己能够使用声明式事务模型与此策略,你必须标记所有公共写方法(插入,更新和删除)在哪个层开始的事务属性的交易REQUIRED 。 此属性指示需要进行交易,如果尚不存在,则将由该方法启动。

与其他事务策略一样,无论您选择启动事务的组件或层如何,都将启动事务的方法视为事务所有者 。 只要有可能,事务所有者就应该是对事务执行提交和回滚的唯一方法。

交易策略实施

您可以使用两种主要技术来实现高并发交易策略。 先读技术涉及在可能的最高应用程序层(通常是API层)的事务范围之外对读取操作进行分组。 低级技术涉及在体系结构中尽可能最低的层启动事务,同时仍然能够保持原子性和更新操作的隔离性。

先读技术

先读技术涉及重构(或编写)应用程序逻辑和工作流,以便所有处理和读操作首先在事务范围之外发生。 这种方法消除了不必要的共享或读取锁定,但是如果在您有机会提交工作之前更新并提交了数据,则会引入陈旧的数据异常。 为了解决这种可能性,如果您在此事务处理策略中使用对象关系映射(ORM)框架,请确保使用版本控制。

为了说明先读技术,我将从一些实现API层事务策略的代码开始。 在清单1中,事务从API层开始,包含整个工作单元,包括所有读取操作,处理和更新操作:

清单1.使用API​​层策略
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void processTrade(TradeData trade) throws Exception {
   try {
      //first validate and insert the trade
      TraderData trader = service.getTrader(trade.getTraderID());
      validateTraderEntitlements(trade, trader);
      verifyTraderLimits(trade, trader);
      performPreTradeCompliance(trade, trader);
      service.insertTrade(trade);

      //now adjust the account
      AcctData acct = service.getAcct(trade.getAcctId());
      verifyFundsAvailability(acct, trade);
      adjustBalance(acct, trade);
      service.updateAcct(trade);

      //post processing
      performPostTradeCompliance(trade, trader);
   } catch (Exception up) {
      ctx.setRollbackOnly();
      throw up;
   }
}

请注意,在清单1中 ,所有处理都包含在Java Transaction API(JTA)事务的范围内,包括所有验证,确认和合规性检查(之前和之后)。 如果要通过事件探查器工具运行processTrade()方法,对于每个方法调用,您将看到与表1相似的执行时间:

表1. API层方法概要-事务范围
方法名称 执行时间(毫秒)
service.getTrader() 100
validateTraderEntitlements() 300
verifyTraderLimits() 500
performPreTradeCompliance() 2300
service.insertTrade() 200
service.getAcct() 100
verifyFundsAvailability() 600
adjustBalance() 100
service.updateAcct() 100
performPostTradeCompliance() 1800

processTrade()方法的持续时间略超过6秒(6100毫秒)。 因为事务从方法的开头开始并在方法的结尾终止,所以事务持续时间也是6100 ms。 根据您使用的数据库类型和特定的配置设置,在事务期间(从执行读取操作时开始),您将同时持有共享锁和排他锁。 此外,从由processTrade()方法调用的方法执行的任何读取操作也可能在数据库中持有锁。 如您所料,在这种情况下,将数据库锁保持6秒钟无法扩展以支持高用户负载。

清单1中的代码在没有高用户并发或高吞吐量需求的环境中可能会很好地工作。 不幸的是,这是大多数人通常测试的环境。一旦此代码进入生产环境,成百上千的交易员(可能是全球)正在进行交易,则该系统很可能会表现不佳,吞吐能力很差,并且很可能会体验数据库死锁(取决于您使用的数据库)。

现在,我将应用High Concurrency事务策略的先读技术来修复清单1中的代码。 在清单1的代码中要观察的第一件事是,更新操作(插入和更新)总共花费300毫秒。 (我在这里假设由processTrade()方法调用的其他方法不执行更新操作。)基本技术是在事务范围之外执行读取操作和非更新处理,并且仅将更新包装在事务中。 清单2中的代码说明了减少事务范围并仍然保持原子性所必需的重构:

清单2.使用高并发策略(先读技术)
public void processTrade(TradeData trade) throws Exception {
   UserTransaction txn = null;
   try {
      //first validate the trade
      TraderData trader = service.getTrader(trade.getTraderID());
      validateTraderEntitlements(trade, trader);
      verifyTraderLimits(trade, trader);
      performPreTradeCompliance(trade, trader);

      //now adjust the account
      AcctData acct = service.getAcct(trade.getAcctId());
      verifyFundsAvailability(acct, trade);
      adjustBalance(acct, trade);
      performPostTradeCompliance(trade, trader);

      //start the transaction and perform the updates
      txn = (UserTransaction)ctx.lookup("UserTransaction");
      txn.begin();
      service.insertTrade(trade);
      service.updateAcct(trade);
      txn.commit();
   } catch (Exception up) {
      if (txn != null) {
         try {
            txn.rollback();
         } catch (Exception t) {
            throw up;
         }
      }
      throw up;
   }
}

注意,我将insertTrade()updateAcct()方法移到processTrade()方法的末尾,并将它们包装在程序化事务中。 这样,所有读取操作和相应的处理都在事务的上下文之外执行,因此在事务期间不会在数据库中持有锁。 新代码中的事务持续时间仅为300 ms,大大低于清单1中的6100 ms。 同样,目标是减少花费在数据库中的时间,从而增加总体数据库并发性,从而提高应用程序处理更大的并发用户负载的能力。 使用清单2中的代码在数据库中仅花费300毫秒,就可以(理论上)使吞吐量提高20倍。

如表2所示,在事务范围内执行的代码减少为300 ms:

表2. API层方法概要-修改了交易范围
方法名称 执行时间(毫秒)
service.insertTrade() 200
service.updateAcct() 100

尽管从数据库并发性的角度来看这是一个重大的改进,但先读技术会带来一个风险:因为在指定用于更新的对象上没有锁,所以任何人都可以在此LUW期间更新那些未锁定的实体。 因此,您必须确保通常不会一次由一个以上的用户来更新要插入或更新的对象。 在以前的交易场景中,可以安全地假设在任何给定时间只有一名交易者将从事特定交易和账户。 但是,这可能并不总是正确的,并且可能会发生陈旧的数据异常。

还有一件事:使用Enterprise JavaBeans(EJB)3.0时,必须告诉容器您打算使用编程式事务管理。 您可以使用@TransactionManagement(TransactionManagementType.BEAN)批注进行此操作。 请注意,该注释是在类级别(而不是方法级别)的,这表明您显然不能在同一类中组合声明式和程序化事务模型。 选择一个或另一个并坚持下去。

低级技术

假设您要坚持使用声明式事务处理模型来简化事务处理,但是在高用户并发情况下仍然可以提高吞吐量。 那时,您将在此交易策略中使用较低级别的技术。 使用这种技术,您通常会遇到与先读技术相同的折衷:读操作通常在事务范围的上下文之外进行。 而实施这种技术很可能需要代码重构。

我将再次从清单1中的示例开始。 您可以将更新操作移至调用堆栈中的另一个公共方法,而不是在同一方法中使用程序化事务。 然后,在完成读取操作和处理后,可以调用新的update方法; 它将启动一个事务,调用更新方法,然后返回。 清单3演示了这种技术:

清单3.使用高并发策略(低级技术)
@TransactionAttribute(TransactionAttributeType.SUPPORTS)
public void processTrade(TradeData trade) throws Exception {
   try {
      //first validate the trade
      TraderData trader = service.getTrader(trade.getTraderID());
      validateTraderEntitlements(trade, trader);
      verifyTraderLimits(trade, trader);
      performPreTradeCompliance(trade, trader);

      //now adjust the account
      AcctData acct = service.getAcct(trade.getAcctId());
      verifyFundsAvailability(acct, trade);
      adjustBalance(acct, trade);
      performPostTradeCompliance(trade, trader);

      //Now perform the updates
      processTradeUpdates(trade, acct);
   } catch (Exception up) {
      throw up;
   }
}

@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void processTradeUpdates(TradeData trade, AcctData acct) throws Exception {
   try {
      service.insertTrade(trade);
      service.updateAcct(trade);
   } catch (Exception up) {
      ctx.setRollbackOnly();
      throw up;
   }
}

使用此技术,您可以在调用堆栈中的较低级别上有效地启动事务,从而减少您在数据库中的时间。 请注意, processTradeUpdates()方法仅更新在父方法(或更高版本)中修改或创建的实体。 再一次,您将其仅保留300毫秒,而不是将事务保留6秒钟。

现在是困难的部分。 与“ API层”策略或“客户编排”策略不同,“高并发”策略并不适合采用一致的实施方法。 这就是为什么图1看起来像是经验丰富的曲棍球运动员的面Kong(牙齿缺失)。 对于某些API调用,该事务可能在API层开始和结束,而在其他时候,它可能仅在DAO层范围内(尤其是LUW中的单表更新)。 诀窍是识别在多个客户端请求之间共享的方法,并确保如果事务以较高级别的方法启动,则它将在较低级别使用。 不幸的是,此特性的结果是,不是事务所有者的较低级别的方法可能会在发生异常时执行回滚。 结果,启动事务的父方法无法对异常采取纠正措施,并且如果它尝试回滚(或提交)已经标记为回滚的事务,则将获得异常。

实施准则

在某些情况下,只需要稍微缩小事务范围即可满足您的吞吐量和并发性要求,而在另一些情况下,则需要大幅减少事务范围以实现所需的结果。 无论您的情况如何,都可以使用以下实施准则来帮助设计和实施高并发策略:

  • 从先读先入技术开始,然后着手进行低级技术。 这样,事务至少包含在应用程序体系结构的API层中,并且不会分布在其他所有层中。
  • 使用声明式事务时,请始终使用REQUIRED事务属性而不是MANDATORY事务属性来保护自己免受启动事务的方法调用另一种事务方法的机会。
  • 在开始执行此事务策略之前,请确保在事务范围之外执行读取操作相对安全。 查看您的实体模型,并询问多个用户一次对同一个实体进行操作是常见,罕见还是不可能的。 例如,两个用户可以同时修改同一个帐户吗? 如果您的答案是普遍的,那么您将有发生陈旧数据异常的更大风险,从而使该策略对于您的应用程序配置文件而言不是一个好的选择。
  • 不需要所有读取操作都在事务范围之外。 如果您有一个同时被多个用户频繁更改的特定实体,则一定要将其添加到事务范围中。 但是,请注意,添加到事务范围中的读取操作和处理越多,减少吞吐量和用户负载功能的可能性就越大。

结论

一切都归结为权衡。 为了支持应用程序或子系统中的高吞吐量和高用户并发性要求,您需要高数据库并发性。 为了支持高数据库并发性,您需要减少数据库锁定并尽可能少地占用资源。 某些数据库类型和配置可以处理一些工作,但是在大多数情况下,解决方案归结为您如何设计代码和事务处理。 对这些问题有一些了解将减轻以后繁重的重构工作。 选择正确的交易策略对于应用程序的成功至关重要。 对于高用户并发需求,可以将“高并发”事务策略作为一种可能的解决方案,以确保在保持高并发和吞吐量要求的同时确保高水平的数据完整性和一致性。


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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值