0. 引言
随着redis的普及,更多的同學對redis分布式緩存更加熟悉,但在一些實際場景中,其實并不需要用到redis,使用更加簡單的本地緩存即可實作我們的緩存需求。
今天,我們一起來看看本地緩存元件ehcache
1. ehcache簡介
1.1 簡介
ehcache是基于java開發的本地緩存元件,無需單獨安裝部署,隻要引入jar包就可利用它來實作緩存。
所謂本地緩存,就是指存儲在JVM堆記憶體中的臨時緩存資料,當然ehcache本身也支援Off-Heap Store機制來使用堆外記憶體,本地緩存相較于redis性能和響應速度更高。
Ehcache的本地緩存還支援過期時間、最大容量、持久化等特性,使得它可以适用于各種不同的緩存場景。
官方文檔位址:www.ehcache.org/documentati…
1.2 本地緩存與redis的差別
本地緩存與redis的差別在于:
- 架構:
- 本地緩存基于單機架構,即資料僅本機可用,無法共享給其他服務。除非使用服務調用來擷取。而redis本身基于分布式架構,支援跨服務調取。 是以當資料需要分布式調用時,則适用于redis,如果資料隻需要本地擷取,則可考慮本地緩存
- 性能:
- 本地緩存本身基于本機記憶體,沒有網絡IO消耗,是以性能上大大高于redis,但是如果資料量較大,則還是要考慮使用redis,本地緩存僅适用于資料量小、結構簡單的資料場景,不适合複雜的業務資料
- 功能拓展:
- redis支援持久化、訂閱模式、叢集、主從模式等,而ehcache更傾向于簡單的緩存功能場景,雖然也支援持久化,但是本身并不建議用它來做大型或複雜場景的緩存。如果場景比較簡單輕量,對延遲有較高要求,則可選擇本地緩存
2. ehcache使用
1、建立一個springboot項目,這裡我的springboot版本為2.6.13
2、引入ehcahe元件依賴
這裡需要注意的是net.sf.ehcache是ehcache2.X 與 org.ehcache是echcache3.X,兩個版本配置有差別
xml複制代碼 <dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>2.6.13</version>
</dependency>
3、在啟動類上添加@EnableCaching注解,開啟緩存
java複制代碼@SpringBootApplication
@EnableCaching
public class LocalCacheDemoApplication {
public static void main(String[] args) {
SpringApplication.run(LocalCacheDemoApplication.class, args);
}
}
4、在配置檔案application.yml中添加配置
yml複制代碼spring:
profiles:
active: dev
cache:
type: ehcache
ehcache:
config: classpath:ehcache.xml
5、在resources檔案夾下建立配置檔案ehcache.xml,注意這裡單獨建立了一個name為user的緩存,用于後續儲存使用者資訊緩存。如果有不同的緩存需要使用不同的name的,需要單獨建立cache标簽
标簽介紹:
defaultCache: 預設緩存配置标簽 cache 指定緩存标簽,name表示緩存名稱 diskStore 資料存儲磁盤路徑
屬性介紹:
eternal: 緩存是否永久有效,如果為 true 則忽略timeToIdleSeconds 和 timeToLiveSeconds maxElementsInMemory:最多緩存多少個key overflowToDisk: 緩存超限時是否寫入磁盤,預設為true overflowToOffHeap: 堆記憶體超限時是否使用堆外記憶體,企業版功能,收費 diskPersistent:緩存是否持久化 timeToLiveSeconds:緩存多久過期 timeToIdleSeconds:緩存多久沒有被通路就過期 diskExpiryThreadIntervalSeconds:磁盤緩存過期檢查線程運作時間間隔 memoryStoreEvictionPolicy:緩存淘汰政策, LFU:最近最少使用的元素先移出; FIFO:最先進入的元素被移出; LRU:使用越少的元素被移出 maxBytesLocalHeap:緩存最大占用JVM堆記憶體,0表示不限制,機關支援K、M或G maxBytesLocalOffHeap: 緩存最大占用堆外記憶體,0表示不限制,機關支援K、M或G,企業版功能,收費 maxBytesLocalDisk:緩存最大占用磁盤,0表示不限制,機關支援K、M或G
xml複制代碼<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<defaultCache
eternal="false"
maxElementsInMemory="10000"
overflowToDisk="false"
diskPersistent="false"
timeToLiveSeconds="3600"
timeToIdleSeconds="0"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"/>
<cache
name="user"
eternal="false"
maxElementsInMemory="10000"
overflowToDisk="false"
diskPersistent="false"
timeToLiveSeconds="3600"
timeToIdleSeconds="0"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"/>
<!-- 存儲到磁盤時的路徑-->
<diskStore path="/Users/wuhanxue/Downloads/ehcache" />
</ehcache>
6、緩存使用,在擷取方法中使用@Cacheable注解,在更新方法中使用@CachePut注解。 我這裡模拟就沒有通路資料庫查詢資料了,大家在實際書寫的時候可以連接配接上資料源測試
java複制代碼@RestController
@RequestMapping("user")
public class UserController {
@GetMapping("get")
@Cacheable(cacheNames = "user", key = "#id")
public User getById(Integer id) {
System.out.println("get第一次擷取,不走緩存");
User user = new User();
user.setId(id);
user.setAge(18);
user.setName("benjamin_"+id);
user.setSex(true);
return user;
}
@PostMapping("update")
@CachePut(cacheNames = "user", key = "#search.id")
public User update(@RequestBody User search) {
System.out.println("update更新緩存");
User user = new User();
Integer id = search.getId();
user.setId(id);
user.setAge(search.getAge() != null ? search.getAge()+1 : 0);
user.setName("update_benjamin_"+id);
user.setSex(true);
return user;
}
}
3. 測試
1、調用查詢接口:localhost:8080/user/get?id=1
2、第一次調用,列印"get第一次擷取,不走緩存"。再調用一次發現沒有列印了,但是資料正常查詢,說明走了緩存
3、調用更新接口
4、再調用查詢接口,查詢到的就是更新的資料,說明緩存更新成功
4. 注意事項
謹慎使用maxElementsInMemory
maxElementsInMemory表示的是最大緩存多少個key,這個配置項謹慎使用,一般我們應該根據占用多少記憶體空間來控制,而不是占用多少個key,如果出現某些key的資料量特别大時,就會導緻key數量沒超過,但記憶體占用超過導緻的OOM了
這個我們通過一個生成大資料量的接口來模拟,其中generateMemoryString方法可以在文末的源碼倉庫中
1、書寫接口
java複制代碼@GetMapping("build")
@Cacheable(cacheNames = "user", key = "#id")
public User build(Integer id) {
System.out.println("get第一次擷取,不走緩存");
User user = new User();
user.setId(id);
user.setAge(18);
// 生成指定大小的字元串
user.setName(generateMemoryString(id));
user.setSex(true);
return user;
}
2、限制項目JVM記憶體為100m,友善更快模拟出報錯
3、調用接口localhost:8080/user/build?id=100,因為該接口會生成大資料,占用本地緩存,而JVM緩存又給的100M,是以調用會報錯堆記憶體溢出,如圖所示
4、是以該配置項要謹慎使用,可以通過maxBytesLocalHeap,maxBytesLocalDisk設定占用多少記憶體、磁盤來替代
xml複制代碼<cache
name="user"
eternal="false"
maxBytesLocalHeap="50M"
maxBytesLocalDisk="200M"
overflowToDisk="false"
diskPersistent="false"
timeToLiveSeconds="3600"
timeToIdleSeconds="0"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"
/>
如果maxBytesLocalHeap和maxElementsInMemory都配置了的,誰先達到配置的值,就觸發
如果單個key值太大,仍然會導緻OOM
雖然我們上面配置了maxBytesLocalHeap來限制最大使用的記憶體,比如我們限制了該值為100M,則如果我們有4個30M的資料進來,那麼就會根據配置的淘汰政策去淘汰之前的key,以騰出空間來裝新的資料
但如果新進來的資料很大,比如超過100M了,那麼就會一下子裝滿記憶體,甚至淘汰之前的key也不行,是以這種情況下還是會導緻OOM的
遇到這種情況,兩種處理辦法,一種是保證不會有大于這個門檻值的資料産生,這個可以通過業務代碼控制,二是設定一個全局錯誤捕捉,捕捉産生的OOM報錯,然後傳回一個兜底或者其他的狀态碼,以此辨別