1. 程式人生 > >SpringBoot+EHcache實現快取

SpringBoot+EHcache實現快取

撰文背景

公司開發中的一個驅動模組,需要用到本地快取,來提高驅動模組的訪問速度和效能,然後就想到了Ehcache快取,Ehcache是Hibernate 中預設的CacheProvider,hibernate就是使用Ehcache來實現二級快取的。本質上來說Ehcache是一個快取管理器,不僅僅可以和Hibernate配合實現快取,也可以和其他框架比如spring boot 結合,作為一個快取管理器,快取管理器有很多,但是公司的專案說實話只是用到了Ehcache的本地快取,Ehcache還支援分散式的快取,雖然公司的專案沒有用到,但是本著深究到底的想法,還是想深入的把Ehcache詳細的學習下,下面就是我學習中的一些總結,算是做個記錄。

常見的快取管理器:

* Generic
* JCache (JSR-107)
* EhCache 2.x
* Hazelcast
* Infinispan
* Redis
* Guava

* Simple

spring boot本身是提供資料快取的功能,SpringBoot自帶的cache技術我想大家都應該用過,為了解決資料庫輸入輸出的瓶頸所以一般情況下我們都會引入非常多的快取策略,例如引入redis快取,引入Hibernate的二級快取等等。

SpringBoot在annotation的層面給我們實現了cache,當然這也是得益於Spring的AOP。所有的快取配置只是在annotation層面配置,完全沒有侵入到我們的程式碼當中,就像我們的宣告式事務一樣。

Spring定義了CacheManager和Cache介面統一不同的快取技術。其中CacheManager是Spring提供的各種快取技術的抽象介面。而Cache介面包含快取的各種操作,當然我們一般情況下不會直接操作Cache介面。

Spring針對不同的快取技術,需要實現不同的cacheManager,Spring定義瞭如下的cacheManger實現

cacheManager實現
CacheManger 描述
SimpleCacheManager 使用簡單的Collection來儲存快取,主要用於測試
ConcurrentMapCacheManager 使用ConcurrentMap作為快取技術(預設)
NoOpCacheManager 測試用
EhCacheCacheManager 使用Ehcache作為快取技術,以前在HIbernate的時候經常用
GuavaCacheManager 使用google guava的GuavaCache作為快取技術
HazelcastCacheManager 使用Hazelcast作為快取技術
JCacheCacheManager 使用JCache標準的實現作為快取技術,如Apache Commons JCS
RedisCacheManager 使用Redis作為快取技術

常規的SpringBoot已經為我們自動配置了EhCache、Collection、Guava、ConcurrentMap等快取,預設使用SimpleCacheConfiguration,即使用ConcurrentMapCacheManager。


EHcache官網: 點選開啟連結

一、EHcache簡介

Ehcache是​​一個開源的基於標準的快取,是一個純java的在程序中的快取,可提高效能,解除安裝資料庫並簡化可伸縮性。它是使用最廣泛的基於Java的快取記憶體,因為它非常強大,經過驗證,功能全面,並且與其他流行的庫和框架整合在一起。Ehcache從程序內快取擴充套件到混合程序內/程序外部署與TB級快取。

EHCache是一個快速的、輕量級的、易於使用的、程序內的緩存。它支援read-only read/write 快取,記憶體和磁碟快取。是一個非常輕量級的快取實現,而且從 1.2 之後就支援了叢集。

現在的Ehcache已經更新到了3.5版本,版本3加入一些新的功能,包括

  • 改進了API,可以利用Java泛型並簡化快取互動,
  • 與javax.cache API(JSR-107)完全相容,
  • Offheap儲存功能,包括僅堆快取記憶體,
  • Spring Caching和Hibernate整合得益於javax.cache支援,
  • 還有很多 ...

與Ehcache 2.x相比,Ehcache 3具有簡化,現代化的型別安全API(和配置),可以大大提高您的編碼體驗。

特點:

  1. 快速.
  2. 簡單.
  3. 多種快取策略
  4. 快取資料有兩級:記憶體和磁碟,因此無需擔心容量問題
  5. 快取資料會在虛擬機器重啟的過程中寫入磁碟
  6. 可以通過 RMI、可插入 API 等方式進行分散式快取
  7. 具有快取和快取管理器的偵聽介面
  8. 支援多快取管理器例項,以及一個例項的多個快取區域
  9. 提供 Hibernate 的快取實現

儲存方式:

  1. 記憶體
  2. 磁碟

對應的jar包下載地址:點選開啟連結

又或者可以採用新增pom依賴的方式來新增依賴包:

    <dependency>
      <groupId>org.ehcache</groupId>
      <artifactId>ehcache</artifactId>
      <version>3.5.2</version>
    </dependency> 

二、EHCache頁面快取的配置

1.EHCache 的類層次模型

主要為三層,最上層的是 CacheManager,他是操作Ehcache 的入口。我們可以通過CacheManager.getInstance()獲得一個單子的 CacheManger,或者通過CacheManger 的建構函式建立 一個新的CacheManger。每個 CacheManager 都管理著多個 Cache。而每個Cache 都以一種類 Hash 的方式,關聯著多個Element。Element 則是我們用於存放要快取內容的地方

2.ehcache配置檔案中元素說明

ehcach.xml配置檔案主要引數的解釋,其實檔案裡有詳細的英文註釋//DiskStore 配置,

cache 檔案的存放目錄,主要的值有

* user.home - 使用者主目錄

* user.dir - 使用者當前的工作目錄

* java.io.tmpdir - Default temp file path 預設的 temp 檔案目錄

以下有個範例:

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
         updateCheck="false">
    <diskStore path = "Java.io.tmpdir"/> 
    <defaultCache
            eternal="false" <--意味著該快取會死亡-->
            maxElementsInMemory="900"<--快取的最大數目-->
            overflowToDisk="false" <--記憶體不足時,是否啟用磁碟快取,如果為true則表示啟動磁碟來儲存,如果為false則表示不啟動磁碟-->
            diskPersistent="false" 
            timeToIdleSeconds="0"  <--當快取的內容閒置多少時間銷燬-->
            timeToLiveSeconds="60" <--當快取存活多少時間銷燬(單位是秒,如果我們想設定2分鐘的快取存活時間,那麼這個值我們需要設定120)-->
            memoryStoreEvictionPolicy="LRU" /> <--自動銷燬策略-->
    <!-- 這裡的 users 快取空間是為了下面的 demo 做準備 -->
    <cache
            name="data"
            eternal="false"
            maxElementsInMemory="200"
            overflowToDisk="false"
            diskPersistent="false"
            timeToIdleSeconds="0"
            timeToLiveSeconds="60"
            memoryStoreEvictionPolicy="LRU" />
</ehcache>
<!--<diskStore>==========當記憶體快取中物件數量超過maxElementsInMemory時,將快取物件寫到磁碟快取中(需物件實現序列化介面)  -->
<!--<diskStore path="">==用來配置磁碟快取使用的物理路徑,Ehcache磁碟快取使用的檔案字尾名是*.data和*.index  -->
<!--name=================快取名稱,cache的唯一標識(ehcache會把這個cache放到HashMap裡)  -->
<!--maxElementsOnDisk====磁碟快取中最多可以存放的元素數量,0表示無窮大  -->
<!--maxElementsInMemory==記憶體快取中最多可以存放的元素數量,若放入Cache中的元素超過這個數值,則有以下兩種情況  -->
                     <!--1)若overflowToDisk=true,則會將Cache中多出的元素放入磁碟檔案中  -->
                     <!--2)若overflowToDisk=false,則根據memoryStoreEvictionPolicy策略替換Cache中原有的元素  -->
<!--eternal==============快取中物件是否永久有效,即是否永駐記憶體,true時將忽略timeToIdleSeconds和timeToLiveSeconds  -->
<!--timeToIdleSeconds====快取資料在失效前的允許閒置時間(單位:秒),僅當eternal=false時使用,預設值是0表示可閒置時間無窮大,此為可選屬性  -->
                     <!--即訪問這個cache中元素的最大間隔時間,若超過這個時間沒有訪問此Cache中的某個元素,那麼此元素將被從Cache中清除  -->
<!--timeToLiveSeconds====快取資料在失效前的允許存活時間(單位:秒),僅當eternal=false時使用,預設值是0表示可存活時間無窮大  -->
                     <!--即Cache中的某元素從建立到清楚的生存時間,也就是說從建立開始計時,當超過這個時間時,此元素將從Cache中清除  -->
<!--overflowToDisk=======記憶體不足時,是否啟用磁碟快取(即記憶體中物件數量達到maxElementsInMemory時,Ehcache會將物件寫到磁碟中)  -->
                     <!--會根據標籤中path值查詢對應的屬性值,寫入磁碟的檔案會放在path資料夾下,檔案的名稱是cache的名稱,字尾名是data  -->
<!--diskPersistent=======是否持久化磁碟快取,當這個屬性的值為true時,系統在初始化時會在磁碟中查詢檔名為cache名稱,字尾名為index的檔案  -->
                     <!--這個檔案中存放了已經持久化在磁碟中的cache的index,找到後會把cache載入到記憶體  -->
                     <!--要想把cache真正持久化到磁碟,寫程式時注意執行net.sf.ehcache.Cache.put(Element element)後要呼叫flush()方法  -->
<!--diskExpiryThreadIntervalSeconds==磁碟快取的清理執行緒執行間隔,預設是120秒  -->
<!--diskSpoolBufferSizeMB============設定DiskStore(磁碟快取)的快取區大小,預設是30MB  -->
<!--memoryStoreEvictionPolicy========記憶體儲存與釋放策略,即達到maxElementsInMemory限制時,Ehcache會根據指定策略清理記憶體  -->
                                 <!--共有三種策略,分別為LRU(Least Recently Used 最近最少使用)、LFU(Less Frequently Used最不常用的)、FIFO(first in first out先進先出)  -->

Ehcache 的三種清空策略

1 FIFO,first in first out,這個是大家最熟的,先進先出。

2 LFU, Less Frequently Used,就是上面例子中使用的策略,直白一點就是講一直以來

最少被使用的。如上面所講,快取的元素有一個 hit 屬性,hit 值最小的將會被清出快取。

3 LRU,Least Recently Used,最近最少使用的,快取的元素有一個時間戳,當快取容量

滿了,而又需要騰出地方來快取新的元素的時候,那麼現有快取元素中時間戳離當前時

間最遠的元素將被清出快取。


接著我們來看一下SimplePageCachingFilter 的配置,

XML/HTML 程式碼

<filter>
<filter-name>indexCacheFilterfilter-name>Page9 of 26
<filter-class>
net.sf.ehcache.constructs.web.filter.SimplePageCachingFilter
<filter-class>
<filter>
<filter-mapping>
<filter-name>indexCacheFilterfilter-name>
<url-pattern>*index.actionurl-pattern>
<filter-mapping>

就只需要這麼多步驟,我們就可以給某個頁面做一個快取的,把上面這段配置放到你的web.xml 中,那麼當你開啟首頁的時候,你會發現,2 分鐘才會有一堆 sql 語句出現在控制臺上。當然你也可以調成5 分鐘,總之一切都在控制中。好了,快取整個頁面看上去是非常的簡單,甚至都不需要寫一行程式碼,只需要幾行配置就行了,夠簡單吧,雖然看上去簡單,但是事實上內部實現卻不簡單哦,有興趣的話,大家可以看看SimplePageCachingFilter 繼承體系的原始碼。

上面的配置針對的情況是快取首頁的全部,如果你只想快取首頁的部分內容時,你需要使用SimplePageFragmentCachingFilter 這個 filter。我們看一下如下片斷:

XML/HTML 程式碼

<filter>
<filter-name>indexCacheFilterfilter-name>
<filter-class>
net.sf.ehcache.constructs.web.filter.SimplePageFragmentCachingFilter
<filter-class>
filter>
<filter-mapping>
<filter-name>indexCacheFilterfilter-name>
<url-pattern>*/index_right.jspurl-pattern>
<filter-mapping>

這個 jsp 需要被 jsp:include 到其他頁面,這樣就做到的區域性頁面的快取。這一點貌似沒有 oscache 的 tag 好用。事實上在cachefilter 中還有一個特性,就是 gzip,也就是說快取中的元素是被壓縮過的,如果客戶瀏覽器支援壓縮的話,filter會直接返回壓縮過的流,這樣節省了頻寬,把解壓的工作交給了客戶瀏覽器,如果客戶的瀏覽器不支援 gzip,那麼 filter 會把快取的元素拿出來解壓後再返回給客戶瀏覽器(大多數爬蟲是不支援 gzip 的,所以 filter 也會解壓後再返回流),這樣做的優點是節省頻寬,缺點就是增加了客戶瀏覽器的負擔(但是我覺得對當代的計算機而言,這個負擔微乎其微)。好了,如果你的頁面正好也需要用到頁面快取,不防可以考慮一下 ehcache,因為它實在是非常簡單,而且易用。

三、spring boot + mybatis + Ehcache 實現本地快取

這個例子其實關於本地快取我這寫了一個例子,這個例子是一個springboot工程專案,集成了mybatis來進行資料庫的訪問,只是一個簡單的資料庫表操作,只是在具體的方法上添加了相應的註解,從而實現了,本地快取。沒有用到Ehcache叢集和分散式,只是將資訊快取到記憶體中,從而降低資料庫的之間的訪問,提高資料的訪問速度。

github地址:點選開啟連結

核心的程式碼我來講解下:

(1)EhcacheProviderApplication啟動類

package com.huntkey.rx.ehcache.provider;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.config.server.EnableConfigServer;
/**
 * 連結Mysql資料庫簡單的整合Mybatis、ehcache框架採用MapperXml訪問資料庫。
 *
 * 簡單使用者連結Mysql資料庫微服務(通過 mybatis 連結 mysql 並用 MapperXml 編寫資料訪問,並且通過 EhCache 快取來訪問)。
 */
@EnableDiscoveryClient
@SpringBootApplication
@EnableCaching
public class EhcacheProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(EhcacheProviderApplication.class,args);
        System.out.println("【【【【【【 連結MysqlMybatisMapperEhCache資料庫微服務 】】】】】】已啟動.");
    }
}

如果想用Ehcache快取,在啟動類上一定要加上 @EnableCaching註解,否則快取會不起作用。

(2)UserServiceImpl實現類

package com.huntkey.rx.ehcache.provider.service.impl;
import com.github.pagehelper.util.StringUtil;
import com.huntkey.rx.ehcache.common.model.User;
import com.huntkey.rx.ehcache.common.util.Result;
import com.huntkey.rx.ehcache.provider.dao.UserDao;
import com.huntkey.rx.ehcache.provider.service.UserService;
import com.mysql.jdbc.StringUtils;
import com.sun.org.apache.regexp.internal.RE;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
    private Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
    private static final String CACHE_KEY = "'user'";
    private static final String CACHE_NAME_B = "cache-b";
    //* @Cacheable : Spring在每次執行前都會檢查Cache中是否存在相同key的快取元素,如果存在就不再執行該方法,而是直接從快取中獲取結果進行返回,否則才會執行並將返回結果存入指定的快取中。
    //* @CacheEvict : 清除快取。
    //* @CachePut : @CachePut也可以宣告一個方法支援快取功能。使用@CachePut標註的方法在執行前不會去檢查快取中是否存在之前執行過的結果,而是每次都會執行該方法,並將執行結果以鍵值對的形式存入指定的快取中。
@Autowired
    private UserDao userDao;
    @CacheEvict(value = CACHE_NAME_B, key = CACHE_KEY)
    @Override
    public int insert(User user) {
        return userDao.insert(user);
    }
    @CacheEvict(value = CACHE_NAME_B, key = "'user_' + #id") //這是清除快取
    @Override
    public int deleteByPrimaryKey(String id) {
        Result result = new Result();
        return userDao.deleteByPrimaryKey(id);
    }
    @CacheEvict(value = CACHE_NAME_B, key = "'user_'+ #user.id")
    @Override
    public User updateByPrimaryKey(User user) {
        userDao.updateByPrimaryKey(user);
        return user;
    }
    @Cacheable(value = CACHE_NAME_B, key = "'user_'+ #id")
    @Override
    public User selectByPrimaryKey(String id) {
        return userDao.selectByPrimaryKey(id);
    }
    @Override
    public List<User> selectAllUser() {
        return userDao.selectAllUser();
    }
    @Cacheable(value = CACHE_NAME_B, key = "#userId+'_'+#userName")
    @Override
    public Result selectUserByAcount(Integer userId, String userName) {
        Result result = new Result();
        try {
            List<User> list = userDao.selectUserByAcount(userId, userName);
            if (list.size() == 0 || list.isEmpty()) {
                result.setRetCode(Result.RECODE_ERROR);
                result.setErrMsg("查詢資料不存在");
                return result;
            }
            result.setData(list);
        } catch (Exception e) {
            result.setRetCode(Result.RECODE_ERROR);
            result.setErrMsg("方法執行出錯");
            logger.error("方法執行出錯", e);
            throw new RuntimeException(e);
        }
        return result;
    }
}

然後我們開始講講四個註解:

@Cacheable 在方法執行前Spring先是否有快取資料,如果有直接返回。如果沒有資料,呼叫方法並將方法返回值存放在快取當中。

這個註解會先查詢是否有快取過的資料,如果有,就直接返回原來快取好的資料,如果沒有,則再執行一次方法,將方法的返回結果放到快取中。

@CachePut 無論怎樣,都將方法的返回結果放到快取當中。

這個註解不會詢問是否有快取好的資料,而是每次都會執行方法,將方法的返回結果放到快取中,相當於每次都更新快取中的資料,每次快取中的資料都是最新的一次快取資料。

@CacheEvict 將一條或者多條資料從快取中刪除。

這個是刪除一條或者多條的快取資料。

@Caching 可以通過@Caching註解組合多個註解集合在一個方法上

這個註解可以組合多個註解,從而實現自定義註解

注意:快取其實存放的是以註解裡面的key為key 方法的返回值作為key的value,不是註解裡面的value。

總結:

ehcache 是一個非常輕量級的快取實現,而且從 1.2 之後就支援了叢集,目前的最新版本是 1.3,而且是 hibernate 預設的快取provider。雖然本文是介紹的是 ehcache 對頁面快取的支援,但是ehcache 的功能遠不止如此,當然要使用好快取,對 JEE 中快取的原理,使用範圍,適用場景等等都需要有比較深刻的理解,這樣才能用好快取,用對快取。