WebObjects/EOF/使用 EOF/快取和新鮮度
關於 記憶體管理 的內容與快取和新鮮度的內容有很大重疊。應該閱讀兩者以完全理解快取和記憶體管理如何在應用程式設計中發揮作用。
與 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 出現各種問題,並且會發生各種難以理解的異常。因此我不建議這樣做。
在我的高度互動式和協作型應用程式中,我更重視資料的即時性,而不是應用程式的效率。透過將 setDefaultTimestampLag 設定為非常低的數值,並在大多數獲取操作中呼叫 setRefreshesRefetchedObjects(true),我的應用程式中已經實現了可以接受的、相對即時的資料。我不認為效率受到了很大影響,但我沒有對此進行充分的調查,也不擔心最大化每段時間內可以發出的請求數量。
當一個 eo 絕對必須是即時的(例如,在每天峰值時間有超過 500,000 場遊戲進行的遊戲中選擇每小時的獲勝者;第二個例子是非同步事件處理器,它接收來自網路和內部執行緒的事件),我使用兩步獲取-儲存操作,並使用一個鎖列,以及一個帶有隨機遞增睡眠時間的輪詢迴圈。這是多例項、多應用程式伺服器安全的,但它不會讓執行緒在執行 Oracle 時鎖定超過 0.8 秒。到目前為止。我嘗試使用資料庫鎖定,但它會導致幾秒鐘的鎖定。
當我想確保我的企業物件是最新的時,我會在我的企業物件的詳細資訊檢視上使用這段程式碼。我沒有明確地進行獲取,我只是使特定的物件及其相關物件失效,並且它們將在頁面顯示時自動重新錯誤和獲取。如程式碼示例所示,必須顯式地使相關物件失效。如果您能指望使用者知道資訊何時過時,那麼一個巧妙的技術是在使用者在瀏覽器中點選重新載入時觸發這段程式碼。
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);
上面描述的即時方法是有限的,因為它們只重新整理屬性和一對一關係(換句話說,就是行中的資料)。它們不重新整理多對多關係,以顯示哪些物件處於關係中發生了變化。這意味著如果物件被另一個程序(或另一個物件儲存)新增到關係中,您的程式碼將不會看到它們是相關的。相反,如果物件從另一個物件儲存中從關係中刪除,它們仍然會出現在關係中。我不知道這是為什麼,也許只是 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);
雖然上面的方法將在下次建立 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);
}
據我瞭解,當使用設定為重新整理物件的獲取規範預取關係時,關係應該被重新整理。不幸的是,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 的“記憶體快取”設定為 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)。
首先,也是最重要的是,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() 不會呼叫任何失效方法。
對於批處理風格的 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 絕對值得擁有。即使你沒有做原始行工作,也是如此。