U9C性能规范-U9C篇

栏目:u9cloud知识作者:用友来源:用友发布:2024-08-20浏览:1

U9C性能规范-U9C篇

1 U9编程模型

1.1 BP&BE

1.1.1 FindByID

这个方法根据ID查找并建立对象,由于使用了U9的对象缓存管理器,可以从对象缓存中命中,因此效率极高。而普通的Find方法,由于可以指定任意条件,因此难以利用对象缓存管理器。所以当查询条件明确只有一个ID的时候,应该使用FindByID而不是Find方法。

例如,下面就是一个效率低下的示例:

对于有些场景,需要获得物理数据库中最新的内容,由于FindByID获得的是缓存在内存中的内容。此种场景,则应该使用Find方法。

1.1.2 Session

U9的Session用于界定持久层逻辑处理范围,是个极轻量级的对象,可以大胆使用。Session支持嵌套,但最外层的Session建立时,会同时建立为该Session范围(包括嵌套)服务的对象缓存管理器。如果持久层代码没有被任何Session所包装,那么会为每个持久层方法创建自己的对象缓存管理器,并在方法调用完毕后销毁。也就是说,多个持久层方法只有包装在Session范围内,才会获得对象缓存的好处!

上面是销售单生成出货单BP的代码,没有在Do方法的开始部分就创建Session,只是在SOTOSM做实际生成时才包装到了一个Session中。由于GetSOLineList方法没有启用Session,所以它以及它所调用的方法都无法利用对象缓存的优势。例如下面的代码,即便FirstSubLine.SupplyOrg和SecSubLine. SupplyOrg指向相同对象,也会做两次数据库查询和两次对象建立工作:

我们建议:如果BP方法涉及实体对象访问和操作,则在Do方法开始就打开一个Session。

1.1.3 GetEntity

EntityKey.GetEntity方法内部会调用FindByID查找并建立对象,如果不能从对象缓存中命中,将需要一次数据库查询。有时,我们仅仅只为了判断EntityKey的具体类型,此时就不要调用GetEntity方法,而应使用EntityType属性来做判断。例如下面的代码效率就不高:

高效的做法如下:

holdMoneyList[0].HoldID.EntityType.Equals(typeof(APBillHead).toString())

1.1.4 GetEntityKey

在做性能分析时,看到一种效率较低的代码。如下例:

TransformPara.SrcDocLinesKey为EntityKey类型,上面的代码为得到EntityKey首先调用了一个FindByID方法。完全没有意义,直接new UFIDA.U9.SD.SaleOrder.SOShipline.EntityKey(25800101)即可!

1.1.5 FindValue

有时,我们只需要操作对象的某一属性,此时应使用FindValue方法而非Find方法。例如下例,只为得到物料信息的CustomerItemVer属性,却使用了Find方法加载整个对象:

高效的做法是:

ObjShipmentLine.CustomerItemVer = CustomerItemInfo.Finder.CreateDataQuery.FindValue(soql,m,q);

除EntityFinder.CreateDataQuery外,还可通过构造函数直接创建EntityDataQuery。示例如下:

 

这里需要注意的是:如果随后还要访问对象的其它属性,那么多次调用FindValue方法就反而是效率低下的做法了。

1.1.6 使用CustomerKey.ID代替Customer.ID

我们经常需要访问关联对象的ID。在以前,只能通过关联对象属性.ID的方式访问,这会导致首先加载关联对象。U9对此做了一个优化,为每个关联对象属性提供了一个对应的EntityKey属性,默认命名为:关联对象属性 + Key。这个属性是根据对象自身包含的关联对象ID直接建立的,不需要建立关联对象。例如下例,当访问OrderBy.Customer.ID时会建立Customer对象。如果这个对象随后不再被访问,那么这种方式效率就不高了:

高效的做法是:

new OqlParam(ObjSoSubline.SOLine.SOHeader.OrderBy.CustomerKey.ID.ToString()

1.1.7 在OQL语句中使用A.B代替A.B.ID ★

假设B为属于A的关联属性,如果在OQL中用A.B.ID过滤,会导致生成的SQL语句必定包含与B表的连接,即便你根本不使用B表的任何属性。考察一个实际的例子:

生成的SQL语句如下:

可以看到,尽管没有用到A1表(Base_SOBOfOrg)的任何属性但却关联了A1表,因为过滤条件是A1.ID=1。

 

如果将OQL语句改为Org=@org and SOB.SOBType=@primary,则生成的SQL语句如下:

可见,在改写OQL语句后根本就不用查询A1表,这对提升性能很有帮助。当然,如果select用到了B表的属性,则A.B和A.B.ID两种写法效果是一样的。要求一律按A.B的方式组织OQL语句,这能使得系统在任何可能的情况下获取性能收益!

1.1.8 为对象建立关联关系的高效方法

以1to1关系为例,假设A对象通过B属性关联B对象。当我们修改A对象时,为对B 属性赋值,将首先建立一个B对象。但正如前一小节所提到的,如果我们不是对B 属性赋值,而是对BKey属性赋值的话,那么就只需要有B对象的ID就可以了,无需建立B对象!

下面示例中的方法效率较低,使用GetEntity建立的关联对象后面并没有用到:

高效的做法是:

newMiscBudget.OrganizationKey = bpObj.BudgetTo.CurrOrgKey;

1.1.9 不要循环打开Session 

Session虽然是个轻型的对象,但除创建自身之外,还要创建关联的ObjPersistor、ObjectManager等。在循环中打开Session,将违背“避免频繁创建对象”这一基本原则。

例如下例:

高效的做法是:

1.1.10 禁止使用Session.Open().Save(IEntity[])方法 ★

Session用于界定持久层处理边界,典型用法如下:

当超出Session边界时,系统依赖于Dispose方法做一些清理工作,例如正确处理Session嵌套情况。像这种写法,代码虽然感觉简洁些,但却会带来潜在的问题,因为这跳过了Dispose处理。而垃圾回收依赖于内存的紧张状况,时间不定。假设在一个嵌套的Session内部按这种方式调用后,Session.Current将仍然指向一个无效的对象。

除Session嵌套外,还有其它的清理工作也被跳过去了。尽管目前这种写法还能工作,但U9无法未来的行为,因此规定禁用这种写法!

1.1.11 使用IfExists方法判定对象是否存在 ★

在开发过程中,有许多校验逻辑的场景要求检索满足一定条件的对象是否存在。这里容易犯的一个错误是将对象集合加载到内存,然后通过遍历的方式检查对象是否满足条件。例如下例:

属于一个帐簿下的科目可能多达上万个,全部加载到内存中是效率极其低下的做法,严重时甚至会导致整个系统因为内存不足而崩溃。显然,高效的做法是在数据库一级按条件查询,只返回满足条件的记录。对于上例,这可能将上万条记录限定到只有一条或几条记录。进一步,判定对象是否存在实际只需要一个Bool结果,即便只返回一条记录其实也是没有必要的。因此,UBF在EntityFinder超类中提供了一个统一的IsExists方法,这个方法使用存储过程在数据库一级完成检索并只返回一个Bool结果。存储过程使用了SQL语句if exists(select A form B where xxx)的形式,数据库一旦检索到一条记录满足要求,就立即结束查询过程,这是效率最高的做法。

优化后的代码如下:

此项为强制性要求,所有开发人员均需遵守。

1.1.12 事务属性定义

为BP方法设置适当的事务属性是一项重要的工作。在审核U9部分BP方法定义时发现,部分同事对事务属性的作用不是很清楚,主要的问题有:

  1. 部分更新单据、基础资料的操作本应受事务保护,但却被设置为Supported
  2. 一些纯查询类的操作反倒被设置成Required 

 

选择事务属性的原则

一般主要使用Required和Supported两种事务属性。

  1. 如果方法涉及数据更新操作,通常应设置Required属性,这将确保操作受到事务保护
  2. 如果方法只是查询数据,不涉及数据更新操作,通常应设置Supported属性。这使得系统有机会选择是否启用事务,方法开销小,并且可以减少死锁发生的几率(总的事务大小可能变小)
  3. 如果方法需要受到事务保护并且不受调用者事务成败与否的控制,那么应设置RequiresNew属性。最典型的应用是操作日志:无论方法是否成功执行,操作日志都需要被记录,假如是Required属性的话,那么日志记录会随方法调用失败而回滚
  4. NotSupported属性一般不使用

1.1.13 警惕ToEntityData方法带来的性能消耗

1.1.13.1 问题描述

存在组合关系的Entity对象,其ToEntityData方法的消耗可能超乎想象!例如,测试中心在测试设置自然科目功能时,发现进入设置界面的时间长达11.5秒。经分析,发现是自然科目表对象的ToEntityData方法消耗过大。该方法的时序图如下(注:测试使用的自然科目表下有3000条自然科目):

即由于存在组合关系,调用自然科目表的ToEntityData方法除序列化自身属性外,还会把关联的下属科目集合对象也递归调用ToEntityData方法序列化出来。由于该科目表下有3000个科目,因此需要加载3000个自然科目实体。另外,自然科目实体还通过NaturalAccountSet属性关联回自然科目表对象。因此,每个自然科目实体的ToEntityData方法还将试图加载关联的自然科目表对象。在本例中,由于BP方法没有包装到Session中,UBF目前基于Session的对象缓存能力无法发挥作用,这导致3000个NatureAccount在存取NaturalAccountSet属性时,每次都要重新从数据库加载(缓存发生作用时则只有一次)。我们从SqlProfiler可以观察到访问相同NaturalAccountSet记录的SQL被执行了3000次!

 

在这样的情况下,性能变得低下就不足为奇了。

1.1.13.2 解决方案

问题的根源是在某些场景下,并不需要获取Entity的所有属性,尤其是组合的子Entity集合。例如本例,UI开发者实际只希望获得自然科目表的Code、Name和TreeRule三个属性,并没有用到子对象集合:

显然,在这类场景中,我们需要意识到存在组合关系的对象,其默认的ToEntityData方法可能太重了!因此,需要控制哪些属性参与序列化。

 

解决方案有三种:

<1> 通过对象属性的服务可见标志来控制

<2> 通过隔离到不同组件中来控制

<3> 构造为特定场景服务的DTO对象代替默认的EntityData

 

方案1:设置属性的服务可见标志

将SubNaturalAccounts属性的服务可见标志去掉,则UBF生成的ToEntityData方法将不会对该属性序列化。

 

方案2:隔离到不同的组件中

自然科目和自然科目表归属于相同的组件,如果把它们分布到不同组件中,也不会有前面出现的问题。因为ToEntityData方法不会序列化来自于其它组件的Entity,即便实体间存在组合关系。

方案3:构造特定的DTO对象

可以在UBFStudio中定义DTO对象:

在该DTO对象中只定义你需要的属性。在编写BP方法时,还是用FindByID得到Entity对象,但之后就不是直接返回Entity.ToEntityData了,而是把需要的属性从Entity对象中拷贝到DTO对象。

1.1.13.3  方案比较

方案1和方案2有局限性:因为它们控制的是Entity的ToEntityData方法,该方法只能有一种实现选择。你屏蔽掉了某些属性,就要意识到所有用到EntityData的地方,返回的对象就是不包含这些属性的。因此,应注意为EntityData选择适用于最广泛的场景。

 

方案3可以为不同的场景定义不同的DTO对象,有最好的灵活性。但缺陷也很明显,需要增加额外的工作量,尤其是可能需要前后台开发人员反复交互。

1.1.14 接口设计尽量用EntityKey代替Entity或EntityData

在优化U9性能时,经常可以看到代码中有显式调用GetEntity方法或ToEntityData方法的情况。通常,这意味着存在不合理的接口设计,它逼迫接口的使用者去加载BE对象或将BE对象转换为BEData,而这就是性能问题。

 

例1:额外付出的GetEntity消耗

ARInstalment类有一个SubtractBalance方法,最后一个参数为Entity对象:

这强迫使用该方法的代码做显式的GetEntity调用:

而方法内部并未使用Entity对象的任何属性,仅仅用于设置对象关系:

按性能规范1.1.8节的描述,设置关联关系用EntityKey就足够了,而且那是最高效的做法。

综上,此处应将PostPeriod参数由Entity改为EntityKey,可避免不必要的性能消耗。

 

例2:额外付出的ToEntityData消耗

下面是convertToPlanReceived方法中节选的部分代码。为匹配DTOData的接口设计,使用者必须显式访问BE对象(这导致对象被加载),再调用该对象的ToEntityData方法:

其中涉及ItemMaster这个比较重型的对象,它的ToEntityData方法消耗尤其大。从总的消耗看,有33%左右的消耗花在了准备DTO对象上,其内部消耗主要就集中上面的几个ToEntityData上:

1.1.15 优先使用DataReader而不是DataSet

在前面Ado.net一节中,我们阐述了DataSet和DataReader的主要差别。当我们只需要对数据进行一次性地单向遍历访问时,最高效的做法是使用DataReader。其实DataSet也是从DataReader来的,DataAdapter的Fill方法内部就是用DataReader读取数据,然后加载到DataSet中。因此,如果不需要反复操作获取的数据,例如反向遍历、跳转到特定行等,则无需加载成DataSet,不仅有时间消耗,更主要是会占用内存。对于100条以内的数据可能差异还不明显,随着数据增大对内存的占用就会变得显著。

DataSet由于是内存中的数据对象,在传统的编程设计中很多时候会作为接口参数使用。对于U9,我们一般使用Entity(EntityList)或DTO(DTOList)。在优化性能时,我们发现经常有使用EntityDataQuery得到DataSet,然后遍历DataSet构造DTOList的情况:

大部分情况下,DataSet只被一次单向遍历使用,但在遍历最后,内存中同时存在DataSet和DTOList,相当于内存中有两份数据,这会对内存造成额外的压力。因此,我们建议只要可能,则尽量使用DataReader而不是DataSet。每一处的节省效果或许不大,但整个系统累计的效果则是惊人的。

1.1.16 利用EntityDataQuery提高性能

在U9系统中,Entity对象属于比较重型的对象,加载Entity对象的时间消耗较大,同时占用的内存也较大。相对而言,Ado.net提供的DataReader/DataSet对象要轻得多,尤其是DataReader,效率高且占用内存极少。从与数据库交互的角度看,Entity采用的是懒加载方式,这是为了尽可能地减少不必要地关联对象加载,同时在需要时又能够以优雅的方式访问。但懒加载也会带来一系列问题:

  1. 可能导致N次SQL查询
    假设用FindAll得到了一个A.EntityList,然后遍历访问A.B.C.Name,这会导致与A关联的B以及与B关联的C均被加载。如果没有对象缓存,理论上,这需要1+2*N次SQL查询(N为EntityList中对象数目)。如果使用EntityDataQuery,则可以显式指定需要用到的对象属性,一个SQL查询就能得到所有数据。假设N=1000,则EntityDataQuery只需要查询1次,而EntityFinder最多需要查询2001次。
  2. 对象属性必须全部加载
    从上面的介绍中我们可以看到,不带缓存的懒加载机制几乎是不可用的。假设与1000个A对象关联的B对象只有5个不同,而与5个B对象关联的C对象只有2个不同,则在缓存系统的支持下查询次数可以由2001次大幅下降为8次。为支持缓存命中,对象的所有属性都必须加载。因为如果缓存对象只包含部分属性,那么将无法满足不同的匹配需求。这是导致Entity对象较重的一个重要因素。
  3. 对象存在的生命周期较长
    简单地表述,Entity缓存的生命周期为BP及其嵌套调用BP的整个会话过程。依据是否跨越服务边界,在细节上会有些不同处理。这意味着由EntityFinder加载的Entity对象会长时间驻留内存,直到App服务端处理结束,BP调用结果返回给Web服务器为止。较长的生命周期+较重型的对象意味着对内存的持续压力。U9出现的OOM问题,绝大部分都可以最终归结到是因为加载了过多的Entity对象上。

 

与此相对,使用EntityDataQuery + DataReader的好处包括:

  1. 数据加载速度比Entity快很多
  2. 占用内存少
  3. 只需1次查询
  4. 只加载需要的属性
  5. 数据访问后可立即丢弃,及时回收占用的内存

 

当然,编程的优雅与执行效率始终是一对矛盾。大家可以在深入理解两种模型差异的基础上,结合具体情况,选择最合适的做法。

 

案例分析:

这是在200真人测试时分析过的一个问题,该功能点导致系统出现OOM问题。分析发现,出现OOM问题时DTOList中对象达到了1.6万个:

与通常看到的使用FindDataSet/FindDataReader查询数据,然后拼装DTOList的情况不同,此处用的是EntityFinder.FindAll!这意味着内存中除了有1.6万个DTO对象外,还在EntityCache中至少有1.6万个重型的Entity对象,这将极大地占用内存资源,使整个系统面临出现OOM问题的窘境。

1.1.17 针对大数据使用批处理模式

在优化过程中遇到过一些加载上万个Entity对象在AppServer层进行处理的情况。对于这种数据规模,在AppServer层进行处理其实已经不合适了,合理的设计是将主要的处理过程放到DBServer层。需要理解一点,在DBServer层完成主要的处理并不一定就要写存储过程,关键是不要把大量数据加载到内存中。

对于一些场景,由于处理并不限于服务组内,而需要访问其它服务组提供的接口,完全在DBServer层进行处理变得不可行。在这种情况下,可以对数据分批加载,分批处理。

需要注意一点,如果不是使用DataReader而是使用Finder加载Entity对象,则需要在每次循环处理后清空EntityCache,否则尽管是分批加载,但加载过的所有对象都会停留在缓存中,这就违背了我们进行分解处理的本意!清除方法如下:

public static void ClearEntityCache()

{

if (OperatorContext.Current.Cache != null)

    {

OperatorContext.Current.Cache.Flush();

}

UFSoft.UBF.PL.Tool.ConfigParm.EntityCache.Flush();

}

 

上面介绍的是大数据对内存占用的危害,另外一个需要考虑的主要问题是事务粒度。由于事务属性在U9中是通过BP定义的,因此当所有处理在一个BP中完成时,会导致事务粒度较大,对系统的并发处理带来伤害。如果业务需求并不要求整个处理过程必须在一个事务内完成,我们建议将每次循环中进行的工作分离到另外一个BP中,形成主控BP循环调用子BP的处理模式。

 

此时,有两种事务模式配置方案供选择:

1. 主控BP选NoSupported,子BP选Required

   这适合于主控BP自身的处理工作不需要受事务保护的场景

 

2. 主控BP选Required,子BP选RequiresNew

   这适合于主控BP自身的处理工作也需要受到事务保护的场景

 

这样的话,每次循环内的处理工作构成一个事务处理粒度,可以有效地把大事务分解为小事务,提升系统的并发处理能力。另外,根据子BP的处理复杂程度,也可以考虑不是将每次循环打包为一个子BP,而是N次循环打包为一个子BP。因为粒度过细时可能在诸如网络往返行程之类上的消耗已超过事务处理本身。这需要设计师根据经验,把握好平衡。

1.2  UI

1.2.1 禁止使用httpSession和ViewState ★

为方便U9以后移植到其它平台,例如由Web程序变为Win32程序,U9禁止程序员在代码中直接使用httpSession和ViewState。U9为状态信息提供的存储机制是UIState,这个也是名称-值对的访问接口,可以象httpSession一样使用。

1.2.2 提高控件批处理的基本技巧

象TreeView、Grid等很多控件都提供了BeginUpdate和EndUpdate方法。这两个方法本身会修改一个计数器,BeginUpdate加1、EndUpdate减1。当EndUpdate方法检查到计数器的值变为0的时候,会启动刷新屏幕的动作。由于屏幕刷新是一个代价相当昂贵的操作,通常,我们在做批操作之前(例如为TreeView插入一批节点)应调用BeginUpdate关闭屏幕刷新,在finally块中调用EndUpdate恢复屏幕刷新。这样,可以显著提高处理效率。

从原理上讲,BeginUpdate/EndUpdate只对批处理才有意义,象下面这样对单个操作使用BeginUpdate/EndUpdate其实毫无意义,因为一旦调用EndUpdate时就已触发屏幕刷新操作了:

1.2.3  禁止在UI端使用Entity对象 ★

Entity对象不能直接在UI端使用。原因是Entity对象支持关联对象加载,在分布式部署模式下,WebServer上可能并无持久层,无法加载关联对象。

 

分析过程中看到的一个示例如下:

在WebPart中调用FromEntityData方法构造Entity对象,这是不对的,会限制U9分布式部署的能力。

 

在UI端,除了UIModel外,可以使用的是EntityData对象。这是U9设计的一种DTO对象(Data Transfer Object),是Entity对象的轻量级表达形式,供WebServer和AppServer之间传输数据用。这意味着AppServer对UI端暴露的接口,只允许EntityData对象。所以,FromEntityData和ToEntityData方法只应当在AppServer端的BP/BE方法中使用。逻辑示意图如下:

 

 

U9C性能规范-U9C篇

1 U9编程模型1.1 BP&BE1.1.1 FindByID这个方法根据ID查找并建立对象,由于使用了U9的对象缓存管理器,可以从对象缓存中命中,因此效率极...
点击下载文档
标签: # U9C
分享:
确认删除?
回到顶部
客服QQ
  • 客服QQ点击这里给我发消息