热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

事务策略:高并发策略

我在本系列的前几篇文章中所介绍的API层和客户端编排策略事务策略是应用于大多数标准业务应用程序的核心策略。它们简单、可靠、相对易于实现,并且提供了最高水平的数据完整性

我在本 系列 的前几篇文章中所介绍的 API 层 和 客户端编排策略 事务策略是应用于大多数标准业务应用程序的核心策略。它们简单、可靠、相对易于实现,并且提供了最高水平的数据完整性和一致性。但有时,您可能需要减小事务的作用域以获取吞吐量、改善性能并提高数据库的并发性。您如何才能实现这些目的,同时仍然维持高水平的数据完整性和一致性呢?答案是使用 High Concurrency 事务策略。

High Concurrency 策略源自 API 层 策略。API 层策略虽然非常坚固和可靠,但它存在一些缺点。始终在调用栈的最高层(API 层)启动事务有时会效率低下,特别是对于具有高用户吞吐量和高数据库并发性需求的应用程序。限制特定的业务需求,长时间占用事务和长时间锁定都会消耗过多资源。

关于本系列

事务可以改善数据的质量、完整性和一致性,并使您的应用程序更加健壮。在 Java 应用程序中实现成功的事务处理并非易事,它涉及到设计和编码。在这个 系列文章中,Mark Richards 将指导您为从简单应用程序到高性能事务处理等各种用例设计有效的事务策略。

与 API 层策略类似,High Concurrency 策略释放了客户机层的任何事务责任。但是,这还意味着,您只能通过客户机层调用一次任何特定的逻辑工作单元(LUW)。High Concurrency 策略旨在减小事务的总体作用域,以便资源锁定的时间更短,从而增加应用程序的吞吐量、并发性以及性能。

通过使用此策略所获取的好处在一定程度上将由您所使用的数据库以及它所采用的配置决定。一些数据库(比如说使用 InnoDB 引擎的 Oracle 和 MySQL)不会保留读取锁,而其他数据库(比如没有 Snapshot Isolation Level 的 SQL Server)则与之相反。保留的锁越多,无论它们是共享还是专用的,它们对数据库(以及应用程序)的并发性、性能和吞吐量的影响就越大。

但是,获取并在数据库中保留锁仅仅是高并发性任务的一个部分。并发性和吞吐量还与您释放锁的时间有关。无论您使用何种数据库,不必要地长时间占用事务将更长地保留共享和专用锁。在高并发性下,这可能会造成数据库将锁级别从低级锁提高到页面级锁,并且在一些极端情况下,从页面级锁切换到表级锁。在多数情况下,您无法控制数据引擎用于选择何时升级锁级别的启发方法。一些数据库(比如 SQL Server)允许您禁用页面级锁,以期它不会从行级锁切换到表级锁。有时,这种赌博有用,但大多数情况下,您都不会实现预期中的并发性改善。

底线是,在高数据库并发性的场景中,数据库锁定(共享或专用)的时间越长,则越有可能出现以下问题:

  • 数据库连接耗尽,从而造成应用程序处于等待状态
  • 由共享和专用锁造成的死锁,从而造成性能较差以及事务失败
  • 从页面级锁升级到表级锁

换句话说,应用程序在数据库中所处的时间越长,应用程序能处理的并发性就越低。我所列出的任何问题都会造成您的应用程序运行缓慢,并且将直接减少总体吞吐量和降低性能 — 以及应用程序处理大型并发性用户负载的能力。

折衷

High Concurrency 策略解决了高并发性需求,因为它能将事务在体系结构中的作用域尽可能减小。其结果是,事务会比在 API 层事务策略中更快地完成(提交或回滚)。但是,就像您从 Vasa 中学到的(见 参考资料),您不能同时拥有它们。生活中充满了折衷,事务处理也不例外。您不能期望提供与 API 层策略同样可靠的事务处理,同时提供最大的用户并发性和最高的吞吐量。

因此,您在使用 High Concurrency 事务策略时放弃了什么呢?根据您的应用程序的设计,您可能需要在事务作用域外部执行读取操作,即使读取操作用于更新目的。“等一等!”您说:“您不能这样做 — 您可能会更新在最后一次读取之后发生了变化的数据!”这是合理的担忧,并且也是需要开始考虑折衷的地方。通过此策略,由于您未对数据保持读取锁,因此在执行更新操作时遇到失效数据异常的机率会增加。但是,与 Vasa的情况一样,所有这些都可以归结为一个问题,即哪个特性更加重要:可靠、坚固的事务策略(如 API 层策略),还是高用户并发性和吞吐量。在高并发性情形中,同时实现两者是极为困难的。如果您尝试这样做,则可能会适得其反。

第二个折衷之处是事务可靠性的总体缺乏。此策略难以实现,并且需要更长的时间进行开发和测试,并且比 API 层或 Client Orchestration 策略更易于出错。考虑到这些折衷,您首先应该分析当前的情形以确定使用此策略是否是正确的方法。由于 High Concurrency 策略派生自 API 层策略,因此一种比较好的方法是先使用 API 层策略,并使用较高的用户负载对应用程序执行负载测试(比您预期的峰值负载更高)。如果您发现吞吐量较低、性能较第、等待次数非常多,或者甚至出现死锁,则要准备迁移到 High Concurrency 策略。

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

回页首

基本结构和特性

图 1 通过我在 事务策略 系列中所使用的逻辑应用程序栈展示了 High Concurrency 事务策略。包含事务逻辑的类显示为红色阴影。

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

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

一些 API 层策略的特性和规则是有效的 — 但并非所有。注意,图 1 中的客户机层没有事务逻辑,这意味着任何类型的客户机都可以用于此事务策略,包括基于 Web 的客户机、桌面、Web 服务和 Java Message Service (JMS)。并且事务策略遍布于客户机下面的层中,但这不是绝对的。一些事务可能在 API 层中开始,一些在业务层中开始,还有一些甚至在 DAO 层中开始。这种一致性的缺乏是造成策略难以实现、维护和治理的原因之一。

在大多数情况下,您会发现您需要使用 Programmatic Transaction 模型 来减小事务作用域,但有时您仍然会使用 Declarative Transaction 模型。但是,您通常不能在相同的应用程序中混用 Programmatic 和 Declarative Transaction 模型。在使用这种事务策略时,不应该坚持使用这种 Programmatic Transaction 模型,这样您就不会遇到各种问题。但是,如果您发现自己可以在此策略中使用 Declarative Transaction 模型,那么您应该在使用 REQUIRED 事务属性开始事务的层中标记所有公共写方法(插入、更新和删除)。此属性表示需要一个事务,并且如果事务不存在,则由方法启动。

与其他事务策略一样,无论您选择开始事务的组件或层是什么,启动事务的方法都被认为是事务拥有者。只要可能,事务拥有者应该是对事务执行提交和回滚的唯一方法。

回页首

事务策略实现

您可以使用两个主要技巧来实现 High Concurrency 事务策略。先读取(read-first)技巧涉及在尽可能高的应用层(通常为 API 层)对事务作用域范围外的读取操作进行分组。低级(lower-level)技巧涉及在体系结构中尽可能低的层启动事务,同时仍然能够更新操作的原子性和隔离。

先读取技巧

先读取技巧涉及重构(或编写)应用程序逻辑和工作流,以便所有的处理和读取操作在事务作用域的外部首先发生。这种方法消除了不必要的共享或读取锁,但是如果数据在您能够提交工作之前更新或提交,则可能会引入失效数据异常。为了应对可能的这种情况,如果在此事务策略中使用对象关系映射(ORM)框架,则应确保使用了版本验证功能。

为了演示这种先读取技巧,我们从一些实现 API 层事务策略的代码入手。在清单 1 中,事务在 API 层中开始,并且包围了整个工作单元,包括所有的读取、处理和更新操作:

清单 1. 使用 API 层策略

@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void processTrade(TradeData trade) throws Exception {try {//first validate and insert the tradeTraderData trader = service.getTrader(trade.getTraderID());validateTraderEntitlements(trade, trader);verifyTraderLimits(trade, trader);performPreTradeCompliance(trade, trader);service.insertTrade(trade);//now adjust the accountAcctData acct = service.getAcct(trade.getAcctId());verifyFundsAvailability(acct, trade);adjustBalance(acct, trade);service.updateAcct(trade);//post processingperformPostTradeCompliance(trade, trader);} catch (Exception up) {ctx.setRollbackOnly();throw up;}
}

注意在 清单 1 中,所有的处理都包含在 Java Transaction API (JTA) 事务的作用域内,包括所有的确认、验证和兼容性检查(提前和事后)。如果您通过探查器工具来运行 processTrade() 方法,那么就会看到每个方法调用的执行时间将与表 1 相似:

表 1. API 层方法探查 — 事务作用域

方法名称执行时间 (ms)
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 ms)。由于事务的起始时间与方法相同,因此事务的持续时间也是 6100 ms。根据您所使用的数据库类型以及特定的配置设计,您将在事务执行过程中保持共享和专用锁(从执行读取操作开始)。此外,在由 processTrade() 方法调用的方法中执行的任何读取操作也可以在数据库中保持一个锁。您可能会猜想,在本例中,在数据库中保持锁持续 6 秒以上将不能扩展以支持高用户负载。

清单 1 中的代码在没有高用户并发性或高吞吐量需求的环境中可能会非常出色地运行。遗憾的是,这只是大多数人用于测试的一种环境。一旦此代码进入生产环境,其中数以百计的交易者(或者是全球的)都在进行交易,则该系统最有可能会运行得非常糟糕,并且极有可能会遇到数据库死锁(根据您所使用的数据库而定)。

现在,我将修复 清单 1 中的代码,方法是应用 High Concurrency 事务策略的先读取技巧。在 清单 1 所示的代码中,第一个要注意的地方是总共只用了 300 ms 的更新操作(插入和更新)。(此处,我假定 processTrade() 方法调用的其他方法不执行更新操作。基本技巧是在事务作用域之外执行读取操作和非更新处理,并且仅将更新封装在事务内部。清单 2 中的代码演示了减小事务作用域并仍然维持原子性的必要性:

清单 2. 使用 High Concurrency 策略(先读取技巧)

public void processTrade(TradeData trade) throws Exception {UserTransaction txn = null;try {//first validate the tradeTraderData trader = service.getTrader(trade.getTraderID());validateTraderEntitlements(trade, trader);verifyTraderLimits(trade, trader);performPreTradeCompliance(trade, trader);//now adjust the accountAcctData acct = service.getAcct(trade.getAcctId());verifyFundsAvailability(acct, trade);adjustBalance(acct, trade);performPostTradeCompliance(trade, trader);//start the transaction and perform the updatestxn = (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 ms,从理论上说,吞吐量将实现 20 倍的提升。

如表 2 所示,在事务作用域中执行的代码至减少至 300 ms:

表 2. API 层方法探查 — 修改后的事务作用域

方法名称执行时间 (ms)
service.insertTrade()200
service.updateAcct()100

虽然这从数据库并发性的角度来说是一种显著的改善,但先读取技巧带来了一个风险:由于为更新指定的对象上没有任何锁,因此任何人都可以在此 LUW 过程中更新这些未锁定的实体。因此,您必须确保被插入或更新的对象一般情况下不会由多个用户同时更新。在之前的交易场景中,我做了一个安全的假设,即只有一个交易者会在特定的时间操作特定的交易和帐户。但是,并非始终都是这种情况,并且可能会出现失效数据异常。

另外需要注意:在使用 Enterprise JavaBeans (EJB) 3.0 时,您必须通知容器您计划使用编程事务管理。为此,您可以使用@TransactionManagement(TransactionManagementType.BEAN) 注释。注意,这个注释是类级的(而不是方法级的),这表示您不能在相同的类中结合 Declarative 和 Programmatic 事务模型。选择并坚持其中之一。

低级技巧

假设您希望坚持使用 Declarative Transaction 模型来简化事务处理,但是仍然能在高用户并发性场景中增加吞吐量。同时,您应该在这种事务策略中使用低级技巧。通过此技巧,您通常会遇到与先读取技巧相同的折衷问题:读取操作通常是在事务作用域的外部完成的。并且,实现这种技巧最有可能需要代码重构。

我仍然从 清单 1 中的示例入手。不用在相同的方法中使用编程事务,而是将更新操作移动到调用栈的另一个公共方法中。然后,完成读取操作和处理时,您可以调用更新方法;它会开始一个事务,调用更新方法并返回。清单 3 演示了这个技巧:

清单 3. 使用 High Concurrency 策略(低级技巧)

@TransactionAttribute(TransactionAttributeType.SUPPORTS)
public void processTrade(TradeData trade) throws Exception {try {//first validate the tradeTraderData trader = service.getTrader(trade.getTraderID());validateTraderEntitlements(trade, trader);verifyTraderLimits(trade, trader);performPreTradeCompliance(trade, trader);//now adjust the accountAcctData acct = service.getAcct(trade.getAcctId());verifyFundsAvailability(acct, trade);adjustBalance(acct, trade);performPostTradeCompliance(trade, trader);//Now perform the updatesprocessTradeUpdates(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() 方法仅更新在父方法(或以上)中修改中创建的实体。再次,保持事务的时间不再是 6 秒,您只需要 300 ms。

现在是最难的部分。与 API 层策略或 Client Orchestration 策略不同,High Concurrency 策略并未使用一致的实现方法。这便是 图 1 看上去为何像一名经验丰富的曲棍球员(包括缺少的牙齿)的原因。对于一些 API 调用,事务可能会在 API 层的末端开始,而其他时候,它可能仅限于 DAO 层(特别是对于 LUW 中的单表更新)。技巧是确定在多个客户机请求之间共享的方法,并确保如果某个事务是在较高级的方法中开始的,则它将在较低级的方法中使用。遗憾的是,此特性的效果是,作为非事务拥有者的较低级方法可以对异常执行回滚。结果,开始事务的父方法不能对异常采取正确的措施,并且在尝试回滚(或提交)已经标记为回滚的事务时会出现异常。

回页首

实现指南

有些情况仅需要稍微小些的事务作用域来满足吞吐量和并发性需求,而另一些情况需要大大缩小事务作用域来实现所需的目的。不管具体情况如何,您都可以遵循以下的实现指导,它们能够帮助您设计和实现 High Concurrency 策略:

  • 在着手使用低级别技术之前,首先要从先读技术开始。这样,事务至少包含在应用程序架构的 API 层,并且不扩散到其他层中。
  • 当使用声明性事务时,经常使用 REQUIRED 事务属性而不是 MANDATORY 事务属性来获得保护,避免启动某个事务的方法调用另一个事务方法。
  • 在采用此事务策略之前,确保您在事务作用域外部执行读取操作时是相对安全的。查看您的实体模型并问自己多个用户同时操作相同的实体是常见的、少见还是不可能的。举例来说,两个用户可以同时修改相同的帐户吗?如果您的回答是常见,则面临着极高的失效数据异常风险,那么这个策略对于您的应用程序探查来说是一个很差的选择。
  • 并不需要让所有 读取操作都处于事务作用域之外。如果有一个特定的实体经常会被多个用户同时更改,则应该想尽一切办法将它添加到事务作用域中。但是应该清楚,添加到事务作用域中的读取操作和处理越多,吞吐量和用户负载功能的下降就越大。

回页首

结束语

一切都归结于如何在问题之间取得折衷。为了在应用程序或子系统中支持高吞吐量和高用户并发性,您需要高数据库并发性。要支持高数据库并发性,则需要减少数据库锁,并尽可能缩短保持资源的时间。某些数据库类型和配置可以处理一些这种工作,但在大多数情况下,解决方案最终归结为如何设计代码和事务处理。对这些问题有一些了解之后,您在稍后可以更加轻松地完成复杂的重构工作。选择正确的事务策略对应用程序的成功至关重要。对于高用户并发性需求,可以使用 High Concurrency 事务策略作为确保高水平数据完整性,同时维持高并发性和吞吐量需求的解决方案。


转:https://my.oschina.net/vshcxl/blog/884027



推荐阅读
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • Python SQLAlchemy库的使用方法详解
    本文详细介绍了Python中使用SQLAlchemy库的方法。首先对SQLAlchemy进行了简介,包括其定义、适用的数据库类型等。然后讨论了SQLAlchemy提供的两种主要使用模式,即SQL表达式语言和ORM。针对不同的需求,给出了选择哪种模式的建议。最后,介绍了连接数据库的方法,包括创建SQLAlchemy引擎和执行SQL语句的接口。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • ZSI.generate.Wsdl2PythonError: unsupported local simpleType restriction ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • flowable工作流 流程变量_信也科技工作流平台的技术实践
    1背景随着公司业务发展及内部业务流程诉求的增长,目前信息化系统不能够很好满足期望,主要体现如下:目前OA流程引擎无法满足企业特定业务流程需求,且移动端体 ... [详细]
  • 本文介绍了如何使用C#制作Java+Mysql+Tomcat环境安装程序,实现一键式安装。通过将JDK、Mysql、Tomcat三者制作成一个安装包,解决了客户在安装软件时的复杂配置和繁琐问题,便于管理软件版本和系统集成。具体步骤包括配置JDK环境变量和安装Mysql服务,其中使用了MySQL Server 5.5社区版和my.ini文件。安装方法为通过命令行将目录转到mysql的bin目录下,执行mysqld --install MySQL5命令。 ... [详细]
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
  • 在Java中,我会做这样的事情:classPerson{privateRecordrecord;publicStringname(){record().get(name);}p ... [详细]
  • Java自带的观察者模式及实现方法详解
    本文介绍了Java自带的观察者模式,包括Observer和Observable对象的定义和使用方法。通过添加观察者和设置内部标志位,当被观察者中的事件发生变化时,通知观察者对象并执行相应的操作。实现观察者模式非常简单,只需继承Observable类和实现Observer接口即可。详情请参考Java官方api文档。 ... [详细]
  • 解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法
    本文介绍了解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法,包括检查location配置是否正确、pass_proxy是否需要加“/”等。同时,还介绍了修改nginx的error.log日志级别为debug,以便查看详细日志信息。 ... [详细]
author-avatar
格个蝎子_844
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有