目前的項目是一個極少寫沖突,多讀,多重復HQL語句的項目,因此非常適合使用Hibernate的二級緩存進行查詢優化。目前項目使用的均是最新版本的框架,配置成功后很快就成功使用了,大概講講配置方法。
1. Hibernate L2緩存
1.1. 緩存的分類
- 事務緩存:作用於事務范圍,session結束則緩存清除,Hibernate的L1緩存為事務緩存,默認開啟,我們在純Hibernate項目中手寫過回滾的代碼,能夠回滾就是因為事務緩存。
- 應用緩存:作用於應用范圍,被所有事務共享,依賴於應用的生命周期。所以,非常適合使用一個同樣依賴於應用生命周期的輕量級緩存來實現,ehcache幾乎是最好的選擇。
- 集群緩存:該緩存類似於真正的數據庫被一個集群共享,典型的如Redis就很適合做集群緩存。
1.2. L2緩存工作原理
Hibernate的L1,L2緩存均是通過id進行工作,當Hibernate根據id訪問對象時會先在一級緩存中查找,如果查不到則在二級緩存中查找。
SessionFactory二級緩存根據功能和目的又可以划分為內置緩存和外置緩存,內置緩存存放映射元數據和預定義SQL語句,前者為映射文件中數據的副本,后者為根據副本推導出的SQL語句。內置緩存是只讀的,因此不需要與映射文件進行同步。外置緩存是Hibernate的一個插件,默認不啟用,即Hibernate的L2緩存。外置緩存的數據是數據庫數據的副本,外置緩存的介質可以是內存或者硬盤。
1.3. 放入二級緩存的數據
一般包含以下幾種:
- 很少被修改的數據
- 不是很重要的數據,允許出現偶爾並發的數據。
- 不會被並發訪問的數據。
- 常量數據。
- 不會被第三方修改的數據。
2. Ehcache
Ehcache是一個健壯的簡潔的輕量的純Java進程的內存緩存框架,因此其存在與Java進程直接相關聯。通過在硬盤和內存里對數據進行拷貝,實現了數據庫的緩存。由於Apache的支持,Ehcache非常穩健。
2.1. 依賴
<!--ehcache依賴slf4j-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<!--slf4j依賴log4j-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!--ehcache-->
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.3.1</version>
</dependency>
<!--hibernate.ehcache-->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
<version>${org.hibernate.version}</version>
</dependency>
2.2. ehcache.xml
該文件需要放置src中(Maven項目的resources中),以便編譯后在根目錄內,也可以顯示指定位置。這個文件給出了ehcache的基本配置。
<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
<diskStore path="java.io.tmpdir"/>
<!--沒有特殊設置時系統默認使用此設置-->
<defaultCache
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
maxElementsOnDisk="10000"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"
/>
<!--想使用查詢緩存,這兩個類需要添加-->
<cache name="org.hibernate.cache.spi.UpdateTimestampsCache"
maxElementsInMemory="5000"
eternal="true"
overflowToDisk="true" />
<cache name="org.hibernate.cache.internal.StandardQueryCache"
maxElementsInMemory="10000"
eternal="false"
timeToLiveSeconds="120"
overflowToDisk="true" />
<cache name="javaClassName" maxElementsInMemory="2000" eternal="false"
timeToIdleSeconds="120" timeToLiveSeconds="120"
overflowToDisk="true" />
</ehcache>
ehcache的各屬性介紹如下:
- name:緩存名稱。
- maxElementsInMemory:緩存最大個數。
- eternal:對象是否永久有效,一但設置了,timeout將不起作用。
- timeToIdleSeconds:設置對象在失效前的允許閑置時間(單位:秒)。僅當eternal=false對象不是永久有效時使用,可選屬性,默認值是0,也就是可閑置時間無窮大。
- timeToLiveSeconds:設置對象在失效前允許存活時間,最大時間介於創建時間和失效時間之間。僅當eternal=false對象不是永久有效時使用,默認是0,也就是對象存活時 間無窮大。
- overflowToDisk:當內存中對象數量達到maxElementsInMemory時,Ehcache將會對象寫到磁盤中。
- diskSpoolBufferSizeMB:這個參數設置DiskStore(磁盤緩存)的緩存區大小。默認是30MB。每個Cache都應該有自己的一個緩沖區。
- maxElementsOnDisk:硬盤最大緩存個數。
- diskPersistent:是否緩存虛擬機重啟期數據,默認false。
- diskExpiryThreadIntervalSeconds:磁盤失效線程運行時間間隔,默認是120秒。
- memoryStoreEvictionPolicy:當達到maxElementsInMemory限制時,Ehcache將會根據指定的策略去清理內存。默認策略是LRU。你可以設置為 FIFO或是LFU。
- clearOnFlush:內存數量最大時是否清除。
2.3. 常用的memoryStoreEvictionPolicy(緩存算法)
關於常用的緩存算法主要有三種:
- LRU:(Least Rencently Used)新來的對象替換掉使用時間算最近很少使用的對象。
- LFU:(Least Frequently Used)替換掉按命中率高低算比較低的對象。
- FIFO: (First In First Out)把最早進入二級緩存的對象替換掉。
2.4. ehcache使用
ehcache不支持事務,有三種模式:
- READ_ONLY: 適用於僅讀取,如果有數據的更新操作則會異常。
- READ_WRITE: 用讀寫鎖控制緩存
- NON_STRICT_READ_WRITE: 不加鎖控制緩存,寫寫會有沖突,適用於很難發生寫沖突的系統。
具體使用時,在hibernate持久化生成的Entity上使用類似這樣的標簽,即可為該數據庫添加二級緩存。
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
通常情況下,緩存用於多讀少寫的表,在這種表中,最高效,最符合緩存本身行為的應該是READ_ONLY模式,即,在讀取時使用緩存,發生寫操作時清空緩存。
3. Spring配置
3.1. sessionFactory配置
當我們使用Spring的hibernateTemplate時,需要對sessionFactory進行配置,其中有無關於ehcache的部分可以參考Spring4托管Hibernate5並利用HibernateTemplate進行數據庫操作,這里主要講解和ehcache相關的設置。
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
……
<property name="hibernateProperties">
<props>
……
<prop key="hibernate.cache.use_second_level_cache">true</prop>
<prop key="hibernate.cache.use_query_cache">true</prop>
<prop key="hibernate.cache.region.factory_class">
org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory</prop>
</props>
……
- hibernate.cache.use_second_level_cache 是hibernate中L2緩存的開關,必須為true。
- hibernate.cache.use_query_cache 是hibernate的查詢緩存的開關,可以自己決定是否開啟。
- hibernate.cache.region.factory_class 承載L2緩存的方法,即選擇L2緩存數據庫。官方很坑的從hibernate4開始就存在文檔問題,文檔中仍為provider_class,實際上早已換為了這個方法(idea的默認提示中找不到,但運行后如果沒添加,錯誤日志里可以顯示出)。需要注意的是,需要使用Singleton模式的Factory,否則會有沖突問題。具體原因還不明了。
另外有幾個可以開啟的選項,包括
- hibernate.generate_statistics 生成統計日志,如果項目在調試,這是一個很好的開發選項。記得實際運行時關閉掉。
- hibernate.cache.provider_configuration_file_resource_path 提供配置文件的路徑,如果你不想使用默認路徑,那么需要在這里配置,其格式和web.xml中的路徑一致。
3.2. hibernateTemplate配置
其實就是開啟一下查詢緩存,一條
<bean id="hibernateTemplate" class="org.springframework.orm.hibernate5.HibernateTemplate">
<property name="sessionFactory" ref="sessionFactory" />
<property name="cacheQueries" value="true"/>
</bean>
4. Hiberante二級緩存的使用
Hibernate的所有查詢方法均用到事務緩存,但對於SessionFactory緩存,只有部分方法會使用。
4.1. 不使用二級緩存的方法
Hibernate的各種查詢方式中,以下幾種方式不使用緩存,直接從數據庫讀寫:
- get()
- find()
- list()
其中后兩者在使用hibernateTemplate時均為find()方法。但當開啟了查詢緩存后,使用這些方法時,同樣也會把查詢的結果存入緩存,這會造成一定的時間消耗,但是可以有效的避免使用緩存時的N+1問題。
4.2. 使用二級緩存的方法
Hibernate的以下方法使用二級緩存
- load()
- iterate()
這里面特別說明一下iterate()方法,該方法返回的是一個指向查詢結果的指針,當方法返回指針后,如果想通過指針獲取整個查詢結果,則需要使用事務,並在表上加如下標簽:
@Proxy(lazy = false)
關閉hibernate的懶加載。否則,當想要通過返回的iterator獲取其下一方法,iterator.next(),則會因為變量已經進入游離態,無法找到下一方法。即使如此,尋找下一指針的方法也需要和返回iterator的方法處於同一事務內才能成功。
一個對lazy=false產生的損耗的補救方案是使用Spring的OpenSessionInViewFilter來管理session,在web.xml中添加
<filter>
<filter-name>OpenSessionInViewFilter</filter-name>
<filter-class>org.springframework.orm.hibernate5.support.OpenSessionInViewFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>OpenSessionInViewFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
即可。
4.3. iterate()查詢原理和N+1問題
使用iterate()方法時,Hibernate會先訪問數據庫,查詢所有要查詢對象的id,再訪問緩存,通過id查詢所有要查詢對象,當對象在緩存中時,直接返回結果,當對象不再緩存中時,訪問數據庫查詢該對象。因此,當緩存沒有建立時,這樣的查詢方法會產生N+1次查詢,遠比find()方法的1次數據庫查詢效率低下。所以,簡單的使用iterator對數據進行查詢是十分不合理的,兩種方案可以考慮。
- 在用戶訪問前,對數據庫中常用數據進行緩存,比如,在程序啟動后自動執行一次find()行為把常用數據進行存儲。
- 用戶的第一次訪問使用find()方法,並獲取緩存,之后的訪問使用iterate()方法。
5. 參考文檔
CacheConcurrencyStrategy的五種緩存方式的簡單介紹