Java 持久化/快取
快取是最重要的效能最佳化技術。在持久化中可以快取很多東西,例如物件、資料、資料庫連線、資料庫語句、查詢結果、元資料、關係等等。物件持久化中的快取通常指的是物件或其資料的快取。快取也會影響物件標識,也就是說,如果你讀取了一個物件,然後再次讀取同一個物件,你應該獲得相同的物件(相同的引用)。
JPA 1.0 沒有定義 JPA 提供程式是否支援共享物件快取,但大多數提供程式都支援。JPA 中的快取需要在事務或擴充套件持久化上下文中才能保留物件標識,但 JPA 不要求跨事務或持久化上下文支援快取。
JPA 2.0 定義了共享快取的概念。@Cacheable 註解或 cacheable XML 屬性可用於啟用或停用類上的快取。
@Entity
@Cacheable
public class Employee {
...
}
SharedCacheMode 列舉也可以在 persistence.xml 中的 <shared-cache-mode> XML 元素中設定,用於配置整個持久化單元的預設快取模式。
<persistence-unit name="ACME">
<shared-cache-mode>NONE</shared-cache-mode>
</persistence-unit>
有兩種型別的快取。你可以快取物件本身,包括其所有結構和關係,或者你可以快取其資料庫行資料。兩者都有益處,但僅僅快取行資料會丟失很大一部分快取效益,因為檢索每個關係通常都涉及資料庫查詢,而讀取物件的絕大部分成本都花在了檢索其關係上。
Java 中的物件標識意味著如果兩個變數 (x, y) 引用同一個邏輯物件,那麼 x == y 返回 true。這意味著兩者都引用了同一個東西(兩者都指向同一個記憶體位置)。
在 JPA 中,物件標識在事務中(通常)在同一個 EntityManager 內得到維護。例外情況是 JEE 管理的 EntityManager,物件標識僅在事務內部得到維護。
因此,以下在 JPA 中是正確的
Employee employee1 = entityManager.find(Employee.class, 123);
Employee employee2 = entityManager.find(Employee.class, 123);
assert (employee1 == employee2);
無論如何訪問物件,這都成立
Employee employee1 = entityManager.find(Employee.class, 123);
Employee employee2 = employee1.getManagedEmployees().get(0).getManager();
assert (employee1 == employee2);
在 JPA 中,物件標識不跨 EntityManager 保持。每個 EntityManager 都維護自己的持久化上下文及其物件的自己的事務狀態。
因此,以下在 JPA 中是正確的
EntityManager entityManager1 = factory.createEntityManager();
EntityManager entityManager2 = factory.createEntityManager();
Employee employee1 = entityManager1.find(Employee.class, 123);
Employee employee2 = entityManager2.find(Employee.class, 123);
assert (employee1 != employee2);
物件標識通常是一件好事,因為它避免了應用程式管理物件的多個副本,並避免了應用程式更改一個副本,但沒有更改另一個副本。不同 EntityManager 或事務(在 JEE 中)不維護物件標識的原因是,每個事務必須將其更改與系統的其他使用者隔離。這通常也是一件好事,但是它確實要求應用程式瞭解副本、分離物件和合並。
一些 JPA 產品可能有一個只讀物件的概念,其中物件標識可能透過共享物件快取跨 EntityManager 保持。
物件快取是 Java 物件(實體)本身被快取的地方。物件快取的優點是資料以 Java 中使用的相同格式被快取。所有內容都儲存在物件級別,在獲取快取命中時不需要任何轉換。對於 JPA,EntityManager 仍然必須將物件複製到快取中和從快取中複製出來,因為它必須維護其事務隔離,但這是唯一的要求。物件不需要重新構建,關係已經可用。
使用物件快取,瞬態資料也可以被快取。這可能是自動發生的,或者可能需要一些工作。如果不需要瞬態資料,你可能還需要在物件被快取時清除資料。
一些 JPA 產品允許只讀查詢直接訪問物件快取。一些產品只允許對只讀資料進行物件快取。在只讀資料上獲取快取命中非常高效,因為物件不需要被複制,除了查詢之外,不需要任何工作。
你可以透過將物件從 JPA 載入到自己的物件快取或 JCache 實現中來建立自己的只讀資料物件快取。主要問題,這也是一般快取中的主要問題,是如何處理更新和陳舊的快取資料,但是如果資料是隻讀的,這可能不是問題。
- TopLink / EclipseLink : 支援物件快取。物件快取預設啟用,但可以在全域性或選擇性地為每個類啟用或配置。持久化單元屬性
"eclipselink.cache.shared.default"可以設定為"false"以停用快取。只讀查詢透過"eclipselink.read-only"查詢提示支援,實體也可以使用@ReadOnly註解標記為始終是隻讀的。
資料快取快取的是物件的資料,而不是物件本身。資料通常是物件資料庫行的表示。資料快取的優點是它更容易實現,因為你不必擔心關係、物件標識或複雜的記憶體管理。資料快取的缺點是它不會以應用程式中使用的方式儲存資料,也不會儲存關係。這意味著在快取命中時,物件仍然必須從資料中構建,並且關係必須從資料庫中獲取。一些支援資料快取的產品也支援關係快取或查詢快取以允許快取關係。
- Hibernate : 支援與第三方資料快取整合。快取預設情況下未啟用,必須使用 Ehcache 等第三方快取產品才能啟用快取。
一些產品支援用於快取關係的單獨快取。這通常對於 OneToMany 和 ManyToMany 關係是必需的。OneToOne 和 ManyToOne 關係通常不需要被快取,因為它們引用物件的 Id。但是,反向 OneToOne 將需要關係被快取,因為它引用的是外部索引鍵,而不是主鍵。
對於關係快取,結果通常只儲存相關物件的 Id,而不是物件本身或其資料(以避免重複和陳舊的資料)。關係快取的鍵是源物件的 Id 和關係名稱。有時關係被快取在資料快取中,如果資料快取儲存的是結構而不是資料庫行。當關系快取命中時,相關物件會一個接一個地在資料快取中查詢。這樣做的一個潛在問題是,如果相關物件不在資料快取中,它將需要從資料庫中選擇。這可能導致資料庫效能很差,因為物件可以一個接一個地載入。一些支援快取關係的產品也支援批處理選擇,以嘗試緩解這個問題。
有許多不同的快取型別。最常見的是 LRU 快取,或者一種逐出最不常使用物件的快取,並維護固定數量的最常使用 (MRU) 物件。
一些快取型別包括
- LRU - 在快取中保留 X 個最近使用的物件。
- Full - 快取所有讀取的內容,永遠保留。(如果資料庫很大,這並不總是最好的選擇)
- Soft - 使用 Java 垃圾收集提示,在記憶體不足時從快取中釋放物件。
- Weak - 通常與物件快取相關,在快取中保留所有當前正在使用的物件。
- L1 - 這是指每個
EntityManager都包含的事務性快取,它不是共享快取。 - L2 - 這是一個共享快取,概念上儲存在
EntityManagerFactory中,因此所有EntityManager都可以訪問。 - 資料快取 - 表示物件的的資料被快取(資料庫行)。
- 物件快取 - 物件被直接快取。
- 關係快取 - 物件的關係被快取。
- 查詢快取 - 來自查詢的結果集被快取。
- 只讀 - 僅儲存或僅允許只讀物件的快取。
- 讀寫 - 可以處理插入、更新和刪除(非只讀)的快取。
- 事務性 - 可以處理插入、更新和刪除(非只讀),並遵守事務的 ACID 屬性的快取。
- 叢集 - 通常指使用 JMS、JGroups 或其他機制在叢集中的其他伺服器上廣播失效訊息以更新或刪除物件的快取。
- 複製 - 通常指使用 JMS、JGroups 或其他機制在任何伺服器快取中讀取物件時將物件廣播到所有伺服器的快取。
- 分散式 - 通常指將快取物件分佈在叢集中的多個伺服器上,並且可以在其他伺服器的快取中查詢物件的快取。
- TopLink / EclipseLink : 支援 L1 和 L2 物件快取。支援 LRU、Soft、Full 和 Weak 快取型別。支援查詢快取。物件快取是讀寫的,並且始終是事務性的。為叢集提供了透過 RMI 和 JMS 進行快取協調的支援。TopLink 產品包含一個與 Oracle Coherence 整合的Grid元件,以提供分散式快取。
查詢快取
[edit | edit source]查詢快取快取查詢結果而不是物件。物件快取根據Id快取物件,因此通常對不是Id的查詢不太有用。一些物件快取支援二級索引,但即使是索引快取對於可以返回多個物件的查詢也不太有用,因為您始終需要訪問資料庫以確保您擁有所有物件。這就是查詢快取有用的地方,它不是根據Id儲存物件,而是快取查詢結果。快取鍵基於查詢名稱和引數。因此,如果您有一個經常執行的NamedQuery,您可以快取其結果,並且只需要第一次執行查詢。
查詢快取的主要問題,與一般快取一樣,是陳舊資料。查詢快取通常與物件快取互動,以確保物件至少與物件快取中的物件一樣更新。查詢快取通常也具有類似於物件快取的失效選項。
- TopLink / EclipseLink : 透過查詢提示
"eclipselink.query-results-cache"支援啟用查詢快取。支援幾個配置選項,包括失效。
陳舊資料
[edit | edit source]快取任何東西的主要問題是快取版本可能與原始版本不同步。這被稱為陳舊或不同步資料。對於只讀資料,這不是問題,但對於很少或經常更改的資料,這可能是一個主要問題。有很多技術可以處理陳舊資料和不同步資料。
一級快取
[edit | edit source]在事務或請求的持續時間內快取物件的狀態通常不是問題。這通常稱為一級快取或EntityManager快取,並且 JPA 為了正確的事務語義而需要它。如果您兩次讀取同一個物件,您必須獲得相同物件,並且具有相同的記憶體內更改。唯一的問題發生在查詢和 DML 中。
對於訪問資料庫的查詢,查詢可能不反映物件的未寫入狀態。例如,您已經持久化了一個新物件,但 JPA 尚未將該物件插入資料庫,因為它通常只在事務提交時寫入資料庫。因此,您的查詢不會返回這個新物件,因為它正在查詢資料庫,而不是一級快取。這通常透過使用者首先呼叫flush()或flushMode自動觸發重新整理來解決。EntityManager或Query上的預設flushMode是觸發重新整理,但這可以被停用,如果在每次查詢之前都希望寫入資料庫(通常不是,因為它可能很昂貴並導致較差的併發性)。一些 JPA 提供程式還支援使資料庫查詢結果與記憶體中的物件更改一致,這可以用來獲取一致的資料而無需觸發重新整理。這適用於簡單的查詢,但對於複雜的查詢,這通常變得非常複雜甚至不可能。應用程式通常在開始進行更改之前請求的開頭查詢資料,或者不查詢它們已經找到的物件,因此這通常不是問題。
如果您繞過 JPA 並直接對資料庫執行 DML,無論是透過原生 SQL 查詢、JDBC 還是 JPQL UPDATE 或 DELETE 查詢,那麼資料庫可能會與一級快取不同步。如果您在執行 DML 之前訪問過物件,它們將具有舊狀態,並且不包括更改。這可能取決於您正在做什麼,否則您可能需要從資料庫重新整理受影響的物件。
一級快取或EntityManager快取也可以跨越 JPA 中的事務邊界。JTA 管理的EntityManager僅在 JEE 中 JTA 事務的持續時間記憶體在。通常,JEE 伺服器會將EntityManager的代理注入應用程式,並且在每次 JTA 事務之後,將自動建立新的EntityManager或EntityManager將被清除,從而清除一級快取。在應用程式管理的EntityManager中,一級快取將存在於EntityManager的持續時間內。如果EntityManager被保留太長時間,這會導致陳舊資料,甚至記憶體洩漏和效能下降。這就是為什麼通常最好為每個請求或每個事務建立一個新的EntityManager。一級快取也可以使用EntityManager.clear()方法清除,或者可以使用EntityManager.refresh()方法重新整理物件。
二級快取
[edit | edit source]二級快取跨越事務和EntityManager,並且不是 JPA 的一部分。大多數 JPA 提供程式都支援二級快取,但實現和語義各不相同。一些 JPA 提供程式預設情況下啟用二級快取,而一些提供程式預設情況下不使用二級快取。
如果應用程式是唯一訪問資料庫的應用程式和伺服器,那麼二級快取幾乎沒有問題,因為它應該始終是最新的。唯一的問題是 DML,如果應用程式透過原生 SQL 查詢、JDBC 或 JPQL UPDATE 或 DELETE 查詢直接對資料庫執行 DML。JPQL 查詢應該自動使二級快取失效,但這可能取決於 JPA 提供程式。如果您直接使用原生 DML 查詢或 JDBC,您可能需要使受 DML 影響的物件失效、重新整理或清除。
如果有其他應用程式或其他應用程式伺服器訪問同一個資料庫,那麼陳舊資料可能會成為更大的問題。只讀物件和插入新物件不應該有問題。即使使用快取,新物件也應該被其他伺服器獲取,因為查詢通常仍然訪問資料庫。它通常隻影響find()操作和關係。其他應用程式或伺服器更新和刪除的物件會導致二級快取變得陳舊。
對於已刪除的物件,唯一的問題是find()操作,因為訪問資料庫的查詢不會返回已刪除的物件。如果物件被快取,則find()可以透過物件的Id返回該物件,即使它不存在。如果您從其他物件中添加了對該物件的關聯,或者如果嘗試更新該物件,這可能會導致約束問題或更新失敗。請注意,這些都可能在沒有快取的情況下發生,即使單個應用程式和伺服器訪問資料庫也是如此。在事務期間,應用程式的另一個使用者始終可以刪除另一個事務正在使用的物件,並且第二個事務將以相同的方式失敗。不同之處在於這種併發問題發生的可能性會增加。
對於更新的物件,任何對物件的查詢都可能返回陳舊資料。這可能會在更新時觸發樂觀鎖異常,或者如果未使用鎖定,則會導致一個使用者覆蓋另一個使用者的更改。再次注意,這些都可能在沒有快取的情況下發生,即使單個應用程式和伺服器訪問資料庫也是如此。這就是為什麼通常始終使用樂觀鎖非常重要的原因。陳舊資料也可能會返回給使用者。
重新整理
[edit | edit source]重新整理是解決陳舊資料最常見的解決方案。大多數應用程式使用者都熟悉快取的概念,並且知道何時需要最新資料並願意點選重新整理按鈕。這在網際網路瀏覽器中非常常見,大多數瀏覽器都有一個已經訪問過的網頁快取,並且會避免兩次載入同一個頁面,除非使用者點選重新整理按鈕。相同的概念可用於構建 JPA 應用程式。JPA 提供了幾個重新整理選項,請參閱重新整理。
一些 JPA 提供商還在其二級快取中支援重新整理選項。一種選項是在每次查詢資料庫時始終重新整理。這意味著 find() 操作仍然會訪問快取,但如果查詢訪問資料庫並帶回資料,二級快取將使用該資料重新整理。這避免了查詢返回陳舊資料,但也意味著快取的收益會更少。成本不僅僅在於重新整理物件,還在於重新整理它們的關係。一些 JPA 提供商支援將此選項與樂觀鎖結合使用。如果來自資料庫的行中的版本值比來自快取中物件的版本值更新,則物件將被重新整理,因為它已過時,否則將返回快取值。此選項提供最佳快取,並避免查詢時出現陳舊資料。但是,透過 find() 或透過關係返回的物件仍然可能過時。一些 JPA 提供商還允許 find() 操作配置為首先檢查資料庫,但這通常會違背快取的目的,因此最好根本不使用二級快取。如果要使用二級快取,則必須對陳舊資料有一定的容忍度。
JPA 2.0 快取 API
[edit | edit source]JPA 2.0 提供了一組標準查詢提示,以允許重新整理或繞過快取。查詢提示在兩個列舉類 CacheRetrieveMode 和 CacheStoreMode 上定義。
查詢提示
javax.persistence.cache.retrieveMode:CacheRetrieveModeBYPASS: 忽略快取,並直接從資料庫結果構建物件。USE: 允許查詢使用快取。如果物件/資料已在快取中,則將使用快取的物件/資料。
javax.persistence.cache.storeMode:CacheStoreModeBYPASS: 不要快取資料庫結果。REFRESH: 如果物件/資料已在快取中,則使用資料庫結果重新整理/替換它。USE: 快取從查詢返回的物件/資料。
快取提示示例
[edit | edit source]Query query = em.createQuery("Select e from Employee e");
query.setHint("javax.persistence.cache.storeMode", CacheStoreMode.REFRESH);
JPA 2.0 還提供了一個 Cache 介面。可以使用 getCache() API 從 EntityManagerFactory 獲取 Cache 介面。Cache 可用於手動驅逐/使快取中的實體失效。可以驅逐特定實體、整個類或整個快取。還可以檢查 Cache 以檢視它是否包含實體。
一些 JPA 提供商可能會擴充套件 getCache() 介面以提供其他 API。
- TopLink / EclipseLink : 提供擴充套件的
Cache介面JpaCache。它為失效、查詢快取、訪問和清除提供了其他 API。
快取驅逐示例
[edit | edit source]Cache cache = factory.getCache();
cache.evict(Employee.class, id);
快取失效
[edit | edit source]處理陳舊快取資料的常用方法是使用快取失效。快取失效在一定時間後或在一天中的特定時間刪除或使快取中的資料或物件失效。生存時間失效保證應用程式永遠不會讀取比一定時間更舊的快取資料。時間可以根據應用程式的要求進行配置。一天中的特定時間失效允許在一天中的特定時間使快取失效,通常在晚上進行,這確保資料永遠不會超過一天。如果已知批處理作業在晚上更新資料庫,也可以使用它,失效時間可以設定為批處理作業計劃執行之後。也可以手動使資料失效,例如使用 JPA 2.0 evict() API。
大多數快取實現都支援某種形式的失效,JPA 沒有定義任何可配置的失效選項,因此這取決於 JPA 和快取提供商。
- TopLink / EclipseLink : 使用
@Cache註釋和<cache>orm.xml 元素,提供對生存時間和一天中的特定時間快取失效的支援。快取失效也透過 API 支援,並且可以在叢集中用於使其他機器上更改的物件失效。
叢集中的快取
[edit | edit source]在叢集環境中快取很困難,因為每臺機器都將直接更新資料庫,但不會更新其他機器的快取,因此每臺機器的快取都可能過時。這並不意味著不能在叢集中使用快取,但您必須小心配置它。
對於只讀物件,仍然可以使用快取。對於大多數讀取物件,可以使用快取,但應使用一些機制來避免陳舊資料。如果陳舊資料只是寫入問題,那麼使用樂觀鎖將避免對陳舊資料進行寫入。當發生樂觀鎖異常時,一些 JPA 提供商會自動重新整理或使快取中的物件失效,因此如果使用者或應用程式重試事務,下次寫入將成功。您的應用程式也可以捕獲鎖異常並重新整理或使物件失效,並且如果使用者不需要收到鎖錯誤的通知,可以重試事務(但請小心這樣做,因為通常情況下使用者應該知道鎖錯誤)。快取失效還可以用於透過在快取上設定生存時間來降低陳舊資料的可能性。快取的大小也會影響陳舊資料的發生。
雖然向用戶返回陳舊資料可能是一個問題,但通常情況下,向剛剛更新資料的使用者返回陳舊資料是一個更大的問題。這通常可以透過會話親和性來解決,但要確保使用者在整個會話期間與叢集中的同一臺機器互動。這也可以提高快取使用率,因為同一個使用者通常會訪問相同的資料。通常在 UI 中新增一個重新整理按鈕也很有用,這將允許使用者重新整理他們的資料,如果他們認為他們的資料過時了,或者他們希望確保他們擁有最新的資料。應用程式還可以選擇在需要最新資料的地方重新整理物件,例如使用快取進行只讀查詢,但在進入事務以更新物件時重新整理。
對於大多數寫入物件,最佳解決方案可能是停用這些物件的快取。快取對插入沒有好處,避免更新時出現陳舊資料的成本可能意味著快取始終被更新的物件沒有好處。快取會給寫入新增一些開銷,因為必須更新快取,擁有一個大型快取也會影響垃圾收集,因此如果快取沒有提供任何好處,則應關閉它以避免這種開銷。但這取決於物件的複雜性,如果物件具有許多複雜的關係,並且只更新了物件的一部分,那麼快取仍然值得。
快取協調
[edit | edit source]在叢集環境中快取的一種解決方案是使用訊息框架在叢集中的機器之間協調快取。JMS 或 JGroups 可以與 JPA 或應用程式事件結合使用,以廣播訊息以使其他機器上的快取失效,當更新發生時。一些 JPA 和快取提供商在叢集環境中支援快取協調。
- TopLink / EclipseLink : 使用 JMS 或 RMI 在叢集環境中支援快取協調。快取協調透過
@Cache註釋或<cache>orm.xml 元素配置,並使用永續性單元屬性eclipselink.cache.coordination.protocol。
分散式快取
[edit | edit source]分散式快取是指將快取分佈在叢集中的每臺機器上的快取。每個物件只存在於一臺或一組機器上。這避免了陳舊資料,因為當訪問或更新快取時,物件始終從同一位置檢索,因此始終是最新的。此解決方案的缺點是快取訪問現在可能需要網路訪問。當叢集中的機器連線到同一個高速網路,而資料庫機器連線不好或負載過重時,此解決方案效果最佳。分散式快取減少了資料庫訪問,因此允許應用程式擴充套件到更大的叢集,而不會使資料庫成為瓶頸。一些分散式快取提供商還提供本地快取,並在快取之間提供快取協調。
- TopLink : 支援與 Oracle Coherence 分散式快取整合。
快取事務隔離
[edit | edit source]當使用快取時,快取的一致性和隔離性與資料庫事務隔離性一樣重要。對於基本的快取隔離,重要的是在資料庫事務提交後才將更改提交到快取中,否則其他使用者可能會訪問未提交的資料。
快取可以是事務性的,也可以是非事務性的。在事務性快取中,事務的更改作為一個原子單元提交到快取。這意味著物件/資料首先在快取中被鎖定(防止其他執行緒/使用者訪問物件/資料),然後在快取中更新,最後釋放鎖。理想情況下,在提交資料庫事務之前獲取鎖,以確保與資料庫的一致性。在非事務性快取中,物件/資料逐個更新,沒有任何鎖定。這意味著在快取中的資料與資料庫不一致的短暫時間內。這可能是或可能不是一個問題,這是一個複雜的問題,需要思考和討論,並涉及鎖定和應用程式的隔離需求。
樂觀鎖是快取隔離中另一個重要的考慮因素。如果使用樂觀鎖,快取應該避免用舊資料替換新資料。這在讀取和更新快取時很重要。
一些 JPA 提供者可能允許配置他們的快取隔離,或者不同的快取可能定義不同的隔離級別。雖然通常應該使用預設值,但瞭解快取的使用如何影響事務隔離以及效能和併發性至關重要。
- 這意味著您已在 JPA 配置中啟用了快取,或者您的 JPA 提供者預設情況下使用快取。您可以停用 JPA 配置中的二級快取,或者在直接更改資料庫後重新整理物件或使快取失效。請參閱 過時資料
- TopLink / EclipseLink:預設情況下啟用快取。要停用快取,請在您的 persistence.xml 或永續性屬性中將永續性屬性
"eclipselink.cache.shared.default"設定為false。您還可以針對每個類配置此屬性,如果希望在某些類中允許快取而在其他類中不允許快取。請參閱,EclipseLink 常見問題解答。
- TopLink / EclipseLink:預設情況下啟用快取。要停用快取,請在您的 persistence.xml 或永續性屬性中將永續性屬性