跳转到内容

WebObjects/EOF/使用 EOF/缓存和新鲜度

来自 Wikibooks,开放世界中的开放书籍

关于 内存管理 的内容与缓存和新鲜度的内容有很大重叠。应该阅读两者以完全理解缓存和内存管理如何在应用程序设计中发挥作用。

新鲜度

[编辑 | 编辑源代码]

与 WebObjects 相关的最常被问及,也是最难弄清楚的问题之一是:“我的应用程序一直在使用来自数据库的旧值。如何确保它获得最新数据?”

注意事项和警告

[编辑 | 编辑源代码]

除非有人愿意写一章关于到底发生了什么以及企业对象框架在做什么,否则这个网站上不会有说明。但请理解,EOF 试图高效地工作,避免不必要地访问数据库——因为数据库事务相对昂贵。你在这一领域做出的每个选择实际上都是应用程序效率和数据库值“新鲜度”之间的权衡。根据应用程序的要求,可能需要不同的选择。

这些方法中的一些也可能影响“乐观锁定”行为,具体取决于乐观锁定行为的要求。在 WO/EOF 中关于 OptimisticLockingTechniques 的一章将受到欢迎,因为有效地使用乐观锁定(考虑由其他应用程序实例以及同一实例中的其他会话更改的值)可能比想象的要棘手。

注意:有些人建议使用 EOFetchSpecification.setRefreshesRefetchedObjects 和 EOEditingContext.setDefaultFetchTimestampLag 结合使用会导致“无法递减快照”异常。不幸的是,我们目前对此没有更多了解。

但以下是一些确保数据库值“新鲜度”的方法

  • 在应用程序构造函数中,调用
 EOEditingContext.setDefaultFetchTimestampLag(2);

这意味着每个 EOEditingContext 在创建时都会坚持从数据库获取不早于 EOEditingContext 本身两秒钟的数据。如果 EOEditingContext 在 1:23:00 创建,并且记录中存在来自 1:22:58 之前的快照,则不会使用这些快照,而是会执行新的数据库提取操作。

上面的参数 '2' 表示 EOEditingContext 愿意接受的创建 EOEditingContext 之前的时间(以秒为单位)。可以使用任何你想要的值。想想看,你会意识到,将它设置为 '0'(你可能最初想要做的事情),你的应用程序在非常重的负载下可能会进行更多数据库事务,而不会在数据“新鲜度”方面获得多少收益,因此非零值可能更可取。

[哎呀,根据你相信的有关哪个版本的文档,参数可能是毫秒而不是秒。注意]。

[在 WO5.2.3 中,默认值为 3600000,因此显然是毫秒]。

[来自 WO 5.3 文档:public static void setDefaultFetchTimestampLag(long lag)

将新建的编辑上下文的默认时间戳滞后设置为滞后。默认滞后为 3,600,000 毫秒(一小时)。当初始化新的编辑上下文时,会分配一个提取时间戳,该时间戳等于当前时间减去默认时间戳滞后。将滞后设置为较大的数字可能会导致每个新的编辑上下文都接受非常旧的缓存数据。将滞后设置为过小的值可能会因过度提取而降低性能。负滞后值将被视为 0。]

为了使这一点更加清楚:滞后构成固定时间,而不是滚动时间窗口。因此它只确保新建的 EC 中的任何数据不早于创建时间减去滞后。

  • 对于某些(或所有!)EOFetchSpecification,在其上调用 setRefreshesRefetchedObjects(true)。这意味着当你实际执行这个提取规范时,它会与数据库进行事务,并且数据库返回的值将被实际使用!与默认值 refreshesRefetchedObjects==false 相反,WO/EOF 有时会忽略从数据库返回的新值,而选择缓存快照中的旧值。

请注意,组合使用这两种方法(setDefaultFetchTimestampLag 和 setRefreshesRefetchedObjects)是有问题的。不建议这样做。如果你这样做,你将看到 decrementSnapshotCountForGlobalID 错误。

请注意,如果你在 EOFetchSpecification 上使用 setPrefetchingRelationshipKeyPaths,则将跟随提到的关系并提取其目标,并且 setRefreshesRefetchedObjects 指令也将应用于这些目标对象。

不确定是否推荐

[编辑 | 编辑源代码]
  • 有些人使用各种方法来使他们的企业对象失效和/或重新获取错误,以确保下次访问这些对象时将产生最新数据。我对这些方法有些谨慎,因为文档令人困惑,在某些情况下实际上建议不要使用它们。尽管如此,有些人说他们使用它们取得了很好的效果。如果可以的话,我建议使用其他方法。也许其他人想提供更多信息?
[编辑 | 编辑源代码]
  • 我**不**建议尝试使用 databaseContextShouldUpdateCurrentSnapshot EODatabaseContext 代理方法。文档暗示你可以实现这个代理方法,这样你就可以始终获得最新数据——这有点像在每个提取规范上都使用 setRefreshesRefetchecObjects(true),而无需在每个提取规范上都这样做。但在我的个人经验中,尝试这样做会导致 EOF 出现各种问题,并且会发生各种难以理解的异常。因此我不建议这样做。

各种开发人员的实际做法

[编辑 | 编辑源代码]
Jonathan Rochkind
[编辑 | 编辑源代码]

在我的高度交互式和协作型应用程序中,我更重视数据的实时性,而不是应用程序的效率。通过将 setDefaultTimestampLag 设置为非常低的数值,并在大多数获取操作中调用 setRefreshesRefetchedObjects(true),我的应用程序中已经实现了可以接受的、相对实时的數據。我不认为效率受到了很大影响,但我没有对此进行充分的调查,也不担心最大化每段时间内可以发出的请求数量。

Michael Johnston
[编辑 | 编辑源代码]

当一个 eo 绝对必须是实时的(例如,在每天峰值时间有超过 500,000 场游戏进行的游戏中选择每小时的获胜者;第二个例子是异步事件处理器,它接收来自网络和内部线程的事件),我使用两步获取-保存操作,并使用一个锁列,以及一个带有随机递增睡眠时间的轮询循环。这是多实例、多应用程序服务器安全的,但它不会让线程在运行 Oracle 时锁定超过 0.8 秒。到目前为止。我尝试使用数据库锁定,但它会导致几秒钟的锁定。

Jesse Barnum
[编辑 | 编辑源代码]

当我想确保我的企业对象是最新的时,我会在我的企业对象的详细信息视图上使用这段代码。我没有明确地进行获取,我只是使特定的对象及其相关对象失效,并且它们将在页面显示时自动重新错误和获取。如代码示例所示,必须显式地使相关对象失效。如果您能指望用户知道信息何时过时,那么一个巧妙的技术是在用户在浏览器中点击重新加载时触发这段代码。

 EOEditingContext ec = object.editingContext();
 NSMutableArray ids = new NSMutableArray();
 
 // Invalidate the object
 ids.addObject(ec.globalIDForObject(object));
 // Invalidate a to-one related item
 ids.addObject(ec.globalIDForObject((EOEnterpriseObject)object.valueForKey("adreq"))); 
 // Invalidate a to-many relationship
 Enumeration en = ((NSArray)object.valueForKey("correspondences")).objectEnumerator();
 while(en.hasMoreElements()) 
   ids.addObject(ec.globalIDForObject((EOEnterpriseObject)en.nextElement()));
 ec.invalidateObjectsWithGlobalIDs(ids);

刷新多对多关系

[编辑 | 编辑源代码]

Chuck Hill

[编辑 | 编辑源代码]

上面描述的实时方法是有限的,因为它们只刷新属性和一对一关系(换句话说,就是行中的数据)。它们不刷新多对多关系,以显示哪些对象处于关系中发生了变化。这意味着如果对象被另一个进程(或另一个对象存储)添加到关系中,您的代码将不会看到它们是相关的。相反,如果对象从另一个对象存储中从关系中删除,它们仍然会出现在关系中。我不知道这是为什么,也许只是 EOF 的一个缺点。不幸的是,刷新多对多关系既昂贵又费力。

一个解决方案是使您需要刷新多对多关系的对象失效。虽然这有效,但它在处理方面可能很昂贵,并且如果失效的对象有未保存的更改,可能会产生意想不到的副作用。

我使用过另一种方法,但我仍然不能 100% 确定在编辑进行时没有任何不良副作用。这种刷新多对多关系的方法通过将多对多关系的快照设置为 null 来实现。它一次只对一个对象和一个关系起作用,因此刷新多个对象可能有点烦人。实现方式有点复杂,因为我们需要深入到 EODatabase 级别。

以下是如何刷新 sourceObject 上的 relationshipName 的代码的要点

 sourceEditingContext = sourceObject.editingContext();
 EOEntity sourceObjectEntity = EOUtilities.entityForObject(sourceEditingContext, sourceObject);
 EOModel sourceObjectModel = sourceObjectEntity.model();
 EOGlobalID sourceGlobalID = sourceEditingContext.globalIDForObject(sourceObject);
 EODatabaseContext dbContext = EODatabaseContext.registeredDatabaseContextForModel(sourceObjectModel, sourceEditingContext);
 EODatabase database = dbContext.database();
 database.recordSnapshotForSourceGlobalID(null, sourceGlobalID, relationshipName);

Anjo Krank

[编辑 | 编辑源代码]

虽然上面的方法将在下次创建 EO 时获得实时数据,但您需要执行额外的步骤才能在现有对象中看到新数据。

 Object o = eo.storedValueForKey(relationshipName);
 if(o instanceof EOFaulting) {
   EOFaulting toManyArray = (EOFaulting)o;
   if (!toManyArray.isFault()) {
       EOFaulting tmpToManyArray = (EOFaulting)((EOObjectStoreCoordinator)ec.rootObjectStore()).arrayFaultWithSourceGlobalID(gid, relationshipName, ec);
       toManyArray.turnIntoFault(tmpToManyArray.faultHandler());
   }
 } else {
       // we should check if the existing object is an array, too
       EOFaulting tmpToManyArray = (EOFaulting)((EOObjectStoreCoordinator)ec.rootObjectStore())
             .arrayFaultWithSourceGlobalID(gid, relationshipName, ec);
       eo.takeStoredValueForKey(tmpToManyArray, relationshipName);
 }

Pierre Bernard

[编辑 | 编辑源代码]

据我了解,当使用设置为刷新对象的获取规范预取关系时,关系应该被刷新。不幸的是,WebObjects 5.1 和 5.2 并非如此。我尚未测试更高版本。为了解决这个问题,您需要将以下内容添加到 EODatabaseContext 的子类中

 /**
  * Internal method that handles prefetching of to-many relationships.
* // TBD This is a workaround to what looks like a bug in WO 5.1 & WO 5.2. * Remove as soon as it's no longer needed * * The problem is that even refreshing fetches don't refresh the to-many * relationships they prefetch. */ public void _followToManyRelationshipWithFetchSpecification(EORelationship relationship, EOFetchSpecification fetchspecification, NSArray objects, EOEditingContext editingcontext) { int count = objects.count(); for (int i = 0; i < count; i++) { EOEnterpriseObject object = (EOEnterpriseObject) objects.objectAtIndex(i); EOGlobalID sourceGlobalID = editingcontext.globalIDForObject(object); String relationshipName = relationship.name(); if (!object.isFault()) { EOFaulting toManyArray = (EOFaulting) object.storedValueForKey(relationshipName); if (!toManyArray.isFault()) { EOFaulting tmpToManyArray = (EOFaulting) arrayFaultWithSourceGlobalID( sourceGlobalID, relationshipName, editingcontext); // Turn the existing array back into a fault by assigning it // the fault handler of the newly created fault toManyArray.turnIntoFault(tmpToManyArray.faultHandler()); } } } super._followToManyRelationshipWithFetchSpecification(relationship, fetchspecification, objects, editingcontext); }

EOEntity 的内存缓存设置

[编辑 | 编辑源代码]

当您将 EOEntity 的“内存缓存”设置为 true 时,它告诉 EOF 您希望它尝试始终使用内存缓存来存储该实体的所有实例。当您第一次获取标记为“内存缓存”的实体的对象时,该实体的所有实例都将被获取到内存中。这对相对静态的数据(例如枚举类型 EO 或其他类似类型的数据,这些数据在您的应用程序中很少修改)非常有用。请注意,这与 EOF 的正常快照缓存完全独立,无论该设置的值如何,快照缓存都会被使用。此设置仅用于确定是否应该始终缓存实体的整个数据集。在使用此设置时,有一些非常重要的实现细节需要注意。

以下描述的所有行为可能会随着 WO 版本的变化而变化,并且不是“内存缓存”功能的内在要求,但对于任何使用该功能的人来说都是重要的考虑因素。所有这些在 WebObjects 5.3 中都得到验证。

第一个是“内存缓存”会绕过快照引用计数。“内存缓存”对象不会被 EOF 释放。最终,这并不重要,因为您不应该在拥有大量对象的实体上使用此标志。

在性能方面,请注意,“内存缓存”对象仅通过 EOGlobalID(主键)进行索引。如果您使用 EOQualifier 来查找您的对象(而不是遍历包含该对象的“一对一”或“多对多”关系,它使用主键查找),您将在内存中对您的对象进行“全表扫描”。这是只对小基数实体使用内存缓存的另一个理由。如果您缓存了包含 200 万行的实体,用 EOQualifier 获取 EO 可能比让数据库在第一个地方处理它的速度更慢。

“内存缓存”的另一个主要性能细节是,如果您对缓存的实体类型的任何 EO 进行更改,该类型的 ALL EO 将从缓存中刷新并在下次访问时重新加载。这进一步支持了不将“内存缓存”用于可变 EO 的最佳实践。如果您有一个设置为“内存缓存”的 Person EO,并且您更改了其中一个 Person 的姓名,您的整个缓存将被清空。下次您获取任何 Person 时,整个 Person 表格将被重新加载。这在一系列大型更改中可能是灾难性的(您可能会最终清空缓存——您保存一个 EO,缓存刷新,然后它重新加载整个缓存,您保存下一个,缓存刷新,等等等等)。

最后,“内存缓存”EO 有一个怪癖,即除非您的 EOFetchSpecification 在其上设置了 setDeep(true),否则不会使用缓存。如果 isDeep 在您的获取规范上为 false,将执行正常的数据库获取(抵消了缓存的用处)。这对于内部使用 EOFetchSpecification 的 EOUtilities 方法也是如此。Project Wonder 提供了几个常见的 EOUtilities 获取方法的替代实现,这些方法设置了 setDeep(true)。

invalidateAllObjects 很糟糕!

[编辑 | 编辑源代码]

首先,也是最重要的是,EOF 中失效的概念是关于与数据库的缓存一致性,而不是内存管理。

现在,失效确实有一些副作用,这些副作用可能会对 Java 内存使用产生积极的影响,但出于这个目的使用它就像用 50 磅重的锤子来敲打那些微型图钉一样。

我强烈建议您只在您作为应用程序程序员拥有有关数据库状态更改的外部信息(EOF 无法访问)时才使用失效。也就是说,您刚刚执行了一个任意存储过程,该过程对您的表产生了副作用。或者您从另一个进程收到一个 RMI 消息,说它更新了一行。或者今天是星期一凌晨 3 点,而星期一凌晨 2 点,您的 DBA 的 cron 作业总是删除“temp”表中的所有内容。或者您在 EOF 中发现了一个错误(唉,这种情况会发生),而这只是唯一的解决方法。

正如其他人所说,失效会强行清除 EODatabase 缓存中的快照。这会增加缓存未命中率,降低应用程序的性能。根据您触发错误的模式,最初只需要 1 次获取就可以检索到的 10,000 行可能需要 10,000 次获取才能恢复到缓存中。很糟糕。预取和批量错误可以稍微改善这种情况。实际上,它们比简单的错误要好得多,但没有什么能取代第一次执行正确的获取。没有什么能比它高出几个数量级。

清除快照会导致许多其他有害后果。此缓存是所有应用程序中的 EC 共享的资源。当一个 EC 摧毁所有其他 EC 都依赖的资源时……因此,EOF 会发布有关失效的通知。每当发生失效时,应用程序中的所有 EC 都会受到影响,并且必须处理通知。在一个具有多个并发会话的大型 Web 应用程序中,这可能会产生大量不必要的闲聊。失效及其随后的通知会给并发运行的线程带来很大压力。失效会传播到整个应用程序中,即使它们是来自嵌套 EC 的。

[更准确地说:整个以 EOObjectStoreCoordinator 为中心的 EOF 栈。在 5.1 中,理论上可以拥有多个这样的栈,每个栈都有自己的缓存和 EOObjectStoreCoordinator。大多数 EOF 通知不会在使用不同 OSC 的 EC 之间传递。实际上,对于 5.1 来说,这就是整个应用程序。]

重新获取更加温和。它只影响调用它的 EC,因此不会干扰其他用户的 EC。它将减少快照的引用计数(这可能会或可能不会释放快照)。它将破坏该 EC 内的出站引用。如果缓存中的当前快照根据该 EC 的 fetchTimestamp 足够“新鲜”,则触发故障将使用缓存而不是生成另一个获取。重新获取不会发布通知(尽管如果快照不新鲜,则触发故障将导致获取,进而发布)

好的,所以所有这些都与数据库有关。这个主题实际上是关于 Java 中 EOF 的内存管理,所以回到正题。

ec.dispose() 做了什么?

[编辑 | 编辑源代码]

EOEditingContext 做了很多工作。它与许多其他对象(包括注册自己以接收通知)相关联,它主要负责维护一个非常大的循环对象图(EO)。dispose() 告诉 EC 它将不再被使用,并且应该执行尽可能多的清理工作。这将减少快照引用计数,取消注册各种内容,并断开引用。根据你的 JVM,这可能会或可能不会帮助 GC 更快地回收内存。如果你没有调用 dispose(),EC 的终结器将调用。大多数应用程序不需要直接调用 dispose(),但你的情况可能有所不同。

dispose() 不会调用任何失效方法。

对于批处理风格的 EOF 操作,撤销功能通常被忽视为问题(强引用)的来源。默认情况下,EOF 使用无限的撤销/重做栈。如果你不打算使用撤销/重做,那么你应该认真考虑将 EC 的撤销管理器设置为 null,或者在逻辑检查点(例如保存更改之后)对 EC 的撤销管理器执行 removeAllActions。将 EC 的撤销管理器设置为 null 的唯一缺点是,你将无法从验证异常中恢复。根据你的批处理操作,这可能不是问题。对于健壮的应用程序,我会保留撤销管理器,但使用 setLevelsOfUndo() 保持栈很小,并定期调用 removeAllActions()。

对于在 WebObjects 应用程序之外使用 EOF 的应用程序,一些处理将在当前事件结束时进行。你可能需要在 NSDelayedCallbackCenter 上手动调用 eventEnded()。在 WebObjects 应用程序中,这会在请求结束时为你完成。

在 WO 5.1 中,EOEditingContext 对所有注册的 EO 具有强引用,而 EO 对其编辑上下文没有引用。GC 的含义应该相当明显。

失效不会清除 EO 对象本身。它将强制数据库缓存的内存消失,但 EO 仍然存在于内存中并由它们的 EC 保留......

避免不必要的数据库访问 - 一些技巧

[编辑 | 编辑源代码]

在优化性能时,我们通常会打开 EOAdaptorDebugEnabled(或其 NSLog 等效项)。经常出现的一个问题是,如何减少/消除冗余的数据库获取?

EOF 有一个快照,用于存储代表你从数据库请求的对象的字典。这基本上是一个应用程序范围的缓存。当你第一次将对象获取到 EOEditingContext 中时,字典将存储在快照中(使用用于过期字典的时间戳滞后)。编辑上下文将增加该快照条目的引用计数。当你处理该编辑上下文时,该条目在快照中的引用计数将减少。当该字典的快照计数降至 0 时,它将在某个时刻从快照中删除。因此,当你下次再次请求该对象时,它必须访问数据库才能获取它,即使它非常新鲜......

如果你使用的是会话的默认编辑上下文(不推荐 - 但有时在遗留应用程序中可能)请注意,当会话终止时,它将处理其编辑上下文。这意味着,如果你只将一个对象获取到该编辑上下文,它很可能会从快照中释放。如果你从另一个编辑上下文对同一个对象进行重复获取,它将必须返回数据库。我曾经遇到过一个应用程序,该应用程序通过直接操作使用会话的默认编辑上下文。当直接操作完成创建其页面时,它将终止会话。对相同内容的重复调用导致对相同数据的重复数据库访问。

解决此问题的一种方法是将 EO 获取到具有更长生命周期的 EOEditingContext 中(例如,一个跨会话生存的 EOEditingContext),并使用 EOUtilities 为任何临时编辑上下文使用创建该对象的本地实例。

开发人员遇到的第二个常见问题是按键值查询。例如,请考虑以下获取

EOQualifier qualifier = EOQualifier.qualifierWithQualifierFormat ("lastName = 'Smith'", null); EOFetchSpecification fetchspec = new EOFetchSpecification("Person", qualifier, null); editingContext.objectsWithFetchSpecification(fetchSpec,editingContext);

每次都会导致对数据库的请求,即使总是返回相同对象。原因是 EOF 无法知道匹配查询的对象数量是否发生了变化。

如果你处于对主键进行查询的情况,或者知道返回的对象不会经常改变,请考虑实现一个缓存。你的缓存应该只存储返回对象的 EOGlobalID。这样,当你将来执行此获取时,你可以将键与你的缓存进行比较,并仅返回与该键匹配的 EOGlobalID *而不会* 对数据库进行获取。

原始行

[编辑 | 编辑源代码]

原始行结果永远不会被缓存。如果你的应用程序正在为其批处理操作使用原始行,并且仍然遇到内存问题,那么你需要评估你的代码。OptimizeIt 或 JProbe 绝对值得拥有。即使你没有做原始行工作,也是如此。

华夏公益教科书