WebObjects/EOF/使用 EOF/缓存和新鲜度
关于内存管理的内容与缓存和新鲜度之间存在很大重叠。为了全面理解缓存和内存管理如何在应用程序设计中发挥作用,应该阅读这两部分内容。
与 WebObjects 相关的最常见问题之一,也是最难解决的问题之一是:“我的应用程序一直在使用数据库中的旧值。如何确保它获取最新的数据?”
在有人愿意撰写关于实际情况和企业对象框架正在做什么的章节之前,这些内容将不会在这个站点上出现。但请理解,EOF 试图提高效率,尽可能地减少对数据库的访问次数,因为数据库事务相对昂贵。你在这个领域做出的每一个选择实际上都是应用程序效率和数据库值“新鲜度”之间的权衡。根据应用程序的要求,可能需要不同的选择。
这些方法中的一些也可能影响“乐观锁定”的行为,具体取决于需要的乐观锁定行为。关于 WO/EOF 中的 OptimisticLockingTechniques 的一章将受到欢迎,因为有效地使用乐观锁定(同时考虑到其他应用程序实例更改的值以及同一实例中的其他会话)可能比想象的要棘手。
注意:有些人建议使用 EOFetchSpecification.setRefreshesRefetchedObjects 和 EOEditingContext.setDefaultFetchTimestampLag 结合起来会导致“无法递减快照”异常。不幸的是,目前我们对此并不了解。
但以下是一些确保数据库值“新鲜度”的方法
- 在应用程序构造函数中,调用
EOEditingContext.setDefaultFetchTimestampLag(2);
这意味着,每个 EOEditingContext 在创建时,都将坚持从数据库获取最新的数据,这些数据不比 EOEditingContext 本身早 2 秒。如果 EOEditingContext 在 1:23:00 创建,并且有记录在 1:22:58 之前创建的快照,则不会使用这些快照,而是会执行新的数据库获取操作。
上面的参数“2”表示 EOEditingContext 愿意接受数据的创建时间之前的秒数。你可以使用任何想要的值。仔细想想,你会意识到,将它设置为“0”(你可能最初会想要这样),你的应用程序可能会在负载非常大的情况下执行更多的数据库事务,而不会在数据“新鲜度”方面获得多少提升,因此非零值可能更可取。
[哎呀,根据你相信的哪个版本的文档,参数可能是毫秒而不是秒。注意这一点。
[在 WO5.2.3 中,默认值为 3600000,因此显然是毫秒]
[来自 WO 5.3 文档:public static void setDefaultFetchTimestampLag(long lag)
将新实例化编辑上下文的默认时间戳滞后设置为 lag。默认滞后为 3,600,000 毫秒(一小时)。当初始化新的编辑上下文时,它会被分配一个获取时间戳,该时间戳等于当前时间减去默认时间戳滞后。将滞后设置为较大的数字可能会导致每个新的编辑上下文都接受非常旧的缓存数据。将滞后设置为太低的值可能会由于过度获取而导致性能下降。负滞后值将被视为 0。
为了说明清楚:滞后构成固定时间,而不是滚动时间窗口。因此,它只确保新创建的 EC 中的任何数据不早于创建时间的减去滞后。
- 对于一些(或所有!)EOFetchSpecifications,在其上调用 setRefreshesRefetchedObjects(true)。这意味着,当你实际执行这个获取规范时,并且它执行与数据库的交易,数据库返回的值将被实际使用!与 refreshesRefetchedObjects==false 的默认值相反,WO/EOF 有时会忽略数据库返回的新值,而选择缓存快照中的旧值。
注意,将两种方法(setDefaultFetchTimestampLag 和 setRefreshesRefetchedObjects)组合起来会产生错误。不推荐这样做。如果你这样做,你会看到 decrementSnapshotCountForGlobalID 错误。
注意,如果你在 EOFetchSpecification 上使用 setPrefetchingRelationshipKeyPaths,则提到的关系将被跟踪,并且它们的目的地将使用此 EOFetchSpecification 进行获取,并且 setRefreshesRefetchedObjects 指令也将应用于这些目的地对象。
- 有些人使用各种方法来使他们的企业对象失效和/或重新获取,以确保下次访问这些对象时将获得最新数据。我对这些方法有一些顾虑,因为文档令人困惑,在某些情况下实际上建议不要使用它们。尽管如此,有些人表示他们已经有效地使用了这些方法。如果你可以的话,我建议使用其他方法。也许其他人愿意贡献更多信息?
- 我不建议尝试使用 databaseContextShouldUpdateCurrentSnapshot EODatabaseContext 委托方法。文档暗示你可以实现此委托方法,这样你将始终获得最新数据,这类似于在每个获取规范上都调用 setRefreshesRefetchecObjects(true) 的效果,而无需在每个获取规范上都这样做。但根据我自己的经验,尝试这样做会导致 EOF 出现各种问题,并且会发生各种神秘的异常。因此我不建议这样做。
在我的高度交互式和协作应用程序中,我高度重视数据的最新性,而不是应用程序的效率。实施 setDefaultTimestampLag,使用非常低的值,并在大多数获取操作中调用 setRefreshesRefetchedObjects(true) 会在我的应用程序中生成可接受的和相当新鲜的数据。我并不认为效率受到很大影响,但我没有对此进行充分的调查,而且我不关心可以分派的请求数量。在每个时间段内的最大数量。
当一个 EO 绝对需要是最新的时候(例如 1,在一个每天高峰时有超过 500,000 场比赛的游戏中选择每小时的获胜者;例如 2,一个异步事件处理器,它通过网络和内部线程接收事件),我使用带有锁列的两步获取-保存方法,以及一个带有随机、递增休眠时间的轮询循环。 这是多实例、多应用服务器安全的,但它不会在 Oracle 上锁定一个线程超过 0.8 秒。 到目前为止。 我尝试过使用数据库锁定,但这会导致几秒钟的锁定。
Jesse Barnum
[edit | edit source]当我想确保我的企业对象的详细信息视图是最新的时,我使用这段代码。 我并没有显式地执行获取操作,我只是使特定的对象及其相关对象失效,当页面显示时,它们将自动重新设置并获取。 如代码示例所示,您必须显式地使相关对象失效。 如果您可以依靠您的用户知道信息何时过时,那么一个巧妙的技术是在用户在浏览器中点击重新加载时触发这段代码。
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);
刷新多对多关系
[edit | edit source]Chuck Hill
[edit | edit source]上面描述的刷新方法是有限的,因为它们只刷新属性和一对一关系(换句话说,就是行中的直接数据)。 它们不会刷新多对多关系,以显示哪些对象在关系中的变化。 这意味着,如果对象被另一个进程(或在另一个对象存储中)添加到关系中,您的代码将不会看到它们是相关的。 相反,如果对象从另一个对象存储中的关系中删除,它们仍然会出现在关系中。 我不知道为什么是这样,也许这仅仅是 EOF 的一个缺点。 刷新多对多关系不幸的是代价昂贵且费力。
一个解决方案是使您需要刷新多对多关系的对象失效。 虽然这有效,但在处理方面可能很昂贵,并且如果失效的对象有未保存的更改,则会产生意想不到的副作用。
我使用过另一种选择,但我仍然不确定当编辑正在进行时是否没有任何不良副作用。 这种刷新多对多关系的方法通过将多对多关系的快照设置为 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
[edit | edit source]虽然上面的方法将在下次创建 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
[edit | edit source]据我了解,当使用设置为刷新对象的获取规范预取关系时,该关系应该被刷新。 不幸的是,这不是 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 的内存中缓存设置
[edit | edit source]当您为 EOEntity 设置内存中缓存为 true 时,它告诉 EOF 您希望它始终尝试使用内存中缓存来存储此实体的所有实例。 当您第一次获取标记为内存中缓存的实体的对象时,该实体的所有实例都将被获取到内存中。 这对于相对静态的数据(如枚举类型 EO 或其他类似类型的在您的应用程序中很少修改的数据)非常有用。 请注意,这完全独立于 EOF 的正常快照缓存,无论此设置的值如何,该缓存都会被使用。 此设置仅用于确定您的实体的整个数据集是否应该始终被缓存。 在使用此设置时,有一些非常重要的实现细节您应该注意。
下面描述的所有行为可能会在 WO 版本中发生变化,并且不是内存中缓存功能的固有要求,但对于使用此功能的任何人都应谨慎考虑。 所有这些都被验证为截至 WebObjects 5.3 为止是正确的
第一个是内存中缓存绕过了快照引用计数。 内存中缓存的对象不会被 EOF 释放。 最后,这应该没什么大不了的,因为您不应该在具有非常大量对象的实体上使用此标志。
在性能方面,请注意,内存中缓存的对象仅按 EOGlobalID(主键)索引。 如果您使用 EOQualifier 来查找您的对象(而不是遍历包含该对象的到一或到多关系,该关系使用主键查找),您将在内存中对您的对象进行“全表扫描”。 这是仅在小基数实体上使用内存中缓存的另一个原因。 如果您缓存了 200 万行的实体,那么使用 EOQualifier 获取 EO 可能比让数据库首先处理它的业务要慢。
内存中缓存的另一个主要性能细节是,如果您对缓存实体类型的任何 EO 进行更改,该类型的所有 EO 将从缓存中清除,并在下次访问时重新加载。 这进一步支持了不将内存中缓存用于可变 EO 的最佳实践。 如果您有一个设置为内存中缓存的人员 EO,并且您更改了一个人员的姓名,您的整个缓存将被清空。 下次您获取任何人员时,整个人员表将被重新加载。 这在一系列大型更改中可能是灾难性的(您最终可能会清空缓存 - 您保存一个 EO,缓存被清空,然后它重新加载整个缓存,您保存下一个,缓存被清空,等等等等)。
最后,内存中缓存 EO 的一个怪癖是,除非您的 EOFetchSpecification 在其上设置了 setDeep(true),否则缓存将不会被使用。 如果 isDeep 在您的获取规范上为 false,则将发生正常的数据库获取(否定您缓存的用处)。 这也适用于内部使用 EOFetchSpecifications 的 EOUtilities 方法。 Project Wonder 提供了几个常用 EOUtilities 获取方法的替代实现,这些方法设置了 setDeep(true)。
invalidateAllObjects 很糟糕!
[edit | edit source]首先,也是最重要的是,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 的内存管理,所以回到主题。
EOEditingContext 做了很多工作。它与许多其他对象有关联(包括注册自身以接收通知),并且主要负责维护一个非常大的对象循环图(EO)。dispose() 告诉 EC 它将不再被使用,并且它应该执行尽可能多的清理工作。这会递减快照引用计数,取消注册各种内容,并断开引用。根据您的 JVM,这可能会或可能不会帮助 GC 更快地回收内存。如果您不调用 dispose(),EC 的终结器将会这样做。大多数应用程序不需要直接调用 dispose(),但您的情况可能有所不同。
dispose() 不会调用任何 invalidate 方法。
对于批处理样式的 EOF 操作,撤消功能通常被忽视为问题(强引用)的来源。默认情况下,EOF 使用无限撤消/重做堆栈。如果您不打算使用撤消/重做,那么您应该认真考虑将 EC 的撤消管理器设置为 null,或者在逻辑检查点(例如,保存更改后)对 EC 的撤消管理器执行 removeAllActions。将 EC 的撤消管理器设置为 null 的唯一缺点是您将无法从验证异常中恢复。根据您的批处理操作,这可能不是问题。对于健壮的应用程序,我建议保留撤消管理器,但使用 setLevelsOfUndo() 来保持堆栈较小,并定期调用 removeAllActions()。
对于在 WebObjects 应用程序之外使用 EOF 的应用程序,一些处理会延迟到当前事件结束时完成。您可能需要手动调用 NSDelayedCallbackCenter 上的 eventEnded()。在 WebObjects 应用程序中,这将在请求结束时为您完成。
在 WO 5.1 中,EOEditingContexts 对其所有已注册 EO 具有强引用,而 EO 对其编辑上下文没有引用。对 GC 的影响应该相当明显。
invalidate 不会清除 EO 对象本身。它会强制数据库缓存的内存消失,但 EO 仍将保留在内存中并由其 EC 保留...
在优化性能时,我们通常会打开 EOAdaptorDebugEnabled(或其 NSLog 等效项)。经常出现的一个问题是,如何减少/消除冗余的数据库提取?
EOF 有一个快照,它存储表示您从数据库请求的对象的字典。这基本上是一个应用程序范围的缓存。当您第一次将对象提取到 EOEditingContext 中时,字典会存储在快照中(使用用于过期字典的时间戳滞后)。editingcontext 会增加该快照条目上的引用计数。当您处置该 editingcontext 时,该条目在快照中的引用计数会递减。当该字典的快照计数降至 0 时,它会在某个时刻从快照中删除。因此,下次您再次请求该对象时,即使它相当新鲜,它也必须访问数据库来提取它...
如果您使用的是会话的 defaultEditingContext(不推荐 - 但有时在旧版应用程序中可能),请注意,当会话终止时,它会处置其 editingcontext。这意味着,如果您只将对象提取到该 editingcontext 中,它很可能从快照中释放。如果您从另一个 editingcontext 对同一个对象进行重复提取,它将不得不返回数据库。我曾经遇到过一个应用程序,该应用程序通过直接操作使用会话默认的 editingcontext。当直接操作完成创建其页面后,它会终止会话。对相同内容的重复调用会导致对相同数据的重复数据库访问。
解决此问题的一种方法是将 EO 提取到具有更长生命周期的 EOEditingcontext 中(例如,一个跨会话生存的 EOEditingcontext),并使用 EOUtilities 为任何临时 editingcontext 使用创建该对象的本地实例。
开发人员面临的第二个常见问题是按键值查询。例如,考虑以下提取
EOQualifier qualifier = EOQualifier.qualifierWithQualifierFormat ("lastName = 'Smith'", null); EOFetchSpecification fetchspec = new EOFetchSpecification("Person", qualifier, null); editingContext.objectsWithFetchSpecification(fetchSpec,editingContext);
这会导致每次都向数据库发出请求,即使始终返回相同对象也是如此。原因是 EOF 无法知道匹配查询的对象数量是否发生了变化。
如果您处于查询主键或知道返回的对象不会经常更改的情况,请考虑实现缓存。您的缓存应该只存储返回对象的 EOGlobalID。这样,当您将来进行此提取时,您可以将键与您的缓存进行比较,并且只需返回与该键匹配的 EOGlobalID *无需* 提取数据库。
原始行结果从不缓存。如果您的应用程序正在使用原始行进行其批处理操作,并且仍然遇到内存问题,那么您需要评估您的代码。OptimizeIt 或 JProbe 绝对值得拥有。即使您没有进行原始行工作,也是如此。