1. 程式人生 > >《SpringBoot從入門到放棄》之第(九)篇——EhCache快取

《SpringBoot從入門到放棄》之第(九)篇——EhCache快取

一個較大的專案,如果使用者數量不斷的增多,而程式裡都是直接操作資料庫的話,並定會造成資料庫出現瓶頸,無法處理高併發的問題。此時使用快取是解決問題的一個良好辦法之一,讀取快取的資料的速度往往比連線資料庫查詢快很多。

在 pom.xml 配置檔案加上 jar 依賴:

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-cache</artifactId>
		</dependency>

在SpringBoot主類中增加 @EnableCaching 註解開啟快取功能:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

//@MapperScan("com.test.dao") //在啟動類新增掃描 Mybatis 層的註解或者在 Dao 層新增 @Mapper 註解
@SpringBootApplication
@EnableCaching
public class MyTest02Application {

	public static void main(String[] args) {
		SpringApplication.run(MyTest02Application.class, args);
	}
}

我們在 Dao 層的類裡添加註解 @CacheConfig(cacheNames = "userDaoCache"),在查詢資料庫的方法裡添加註解:@Cacheable 完整程式碼:

package com.test.dao;

import com.test.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;

import java.util.List;

@CacheConfig(cacheNames = "userDaoCache")
@Mapper
public interface UserDao {

    /**
     * 新增使用者
     * @param user
     * @return
     */
    Integer addUserMybatis(User user);

    /**
     * 根據id刪除使用者
     * @param id
     * @return
     */
    Integer deleteUserMybatis(Integer id);

    /**
     * 更新使用者資訊
     * @param user
     * @return
     */
    Integer updateUserMybatis(User user);

    /**
     * 查詢所有使用者資訊
     * @return
     */
    @Cacheable
    List<User> getUserMybatis();
}

為了測試效果,我們在 Controller 層裡查詢方法列印一些標記:

    /**
     * 查詢所有使用者資訊
     * @return
     */
    @RequestMapping(value = "/getUserMybatis")
    public List<User> getUserMybatis(){
        List<User> list = userServiceMybatis.getUserMybatis();
        System.out.println("執行查詢完畢,時間戳="+System.currentTimeMillis());
        return list;
    }

O的K,啟動測試類,用 postman 測試查詢功能:

檢視控制檯資訊:

JDBC Connection [[email protected] wrapping [email protected]] will not be managed by Spring
==>  Preparing: select * from t_user 
==> Parameters: 
<==    Columns: id, name, signature
<==        Row: 1, 流放深圳, 讓天下沒有難寫的程式碼
<==        Row: 4, 騰訊NBA, 讓我遇見你
<==        Row: 5, 農夫山泉, 大自然的搬運工
<==        Row: 7, 小米, 為發燒而生
<==      Total: 4
Closing non transactional SqlSession [[email protected]]
執行查詢完畢,時間戳=1539915266526

然後,我們使用更新使用者的功能,把id=4的使用者修改:

然後,再次點選查詢,再檢視控制檯資訊:

檢視查詢的資料,發現並不是我們更新後的資料,而是快取的舊資料:

說明:

①@CacheConfig(cacheNames = "userDaoCache"):配置了該資料訪問物件中返回的內容將儲存於名為userDaoCache的快取物件中。

②@Cacheable:配置了 getUserMybatis 方法的返回值將被加入快取。在查詢時,會先從快取中獲取,若不存在才去查詢資料庫獲取資料。

按住 Ctrl + 滑鼠左鍵,進入 @Cacheable 註解,可以看到如下的內容:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
    @AliasFor("cacheNames")
    String[] value() default {};

    @AliasFor("value")
    String[] cacheNames() default {};

    String key() default "";

    String keyGenerator() default "";

    String cacheManager() default "";

    String cacheResolver() default "";

    String condition() default "";

    String unless() default "";

    boolean sync() default false;
}

1、value、cacheNames:兩個等同的引數,用於指定快取儲存的集合名。由於Spring 4中新增了@CacheConfig
key:快取物件儲存在Map集合中的key值,非必需,預設按照函式的所有引數組合作為key值,若自己配置需使用SpEL表示式,比如:@Cacheable(key = "#A1"):使用函式第一個引數作為快取的key值。
2、condition:快取物件的條件,非必需,也需使用SpEL表示式,只有滿足表示式條件的內容才會被快取,比如:@Cacheable(key = "#A1", condition = "#A1.length() < 3"),表示只有當第一個引數的長度小於3的時候才會被快取。
3、unless:另外一個快取條件引數,非必需,需使用SpEL表示式。它不同於condition引數的地方在於它的判斷時機,該條件是在函式被呼叫之後才做判斷的,所以它可以通過對result進行判斷。
4、keyGenerator:用於指定key生成器,非必需。若需要指定一個自定義的key生成器,我們需要去實現org.springframework.cache.interceptor.KeyGenerator介面,並使用該引數來指定。需要注意的是:該引數與key是互斥的
5、cacheManager:用於指定使用哪個快取管理器,非必需。只有當有多個時才需要使用
6、cacheResolver:用於指定使用那個快取解析器,非必需。需通過org.springframework.cache.interceptor.CacheResolver介面來實現自己的快取解析器,並用該引數指定。

在SpringBoot中通過啟動類的 @EnableCaching 註解自動化配置合適的快取管理器(CacheManager),SpringBoot根據下面的順序去偵測快取提供者:Generic、JCache (JSR-107)、EhCache 2.x、Hazelcast、Infinispan、Redis、Guava、Simple。

除了按順序監測外,我們可以通過配置檔案 spring.cache.type 來指定快取的型別。比如我們在 application.properties 配置檔案裡新增如下配置,指定使用 ehcache 快取:

spring.cache.type=ehcache
spring.cache.ehcache.config=classpath:cache/ehcache.xml

在SpringBoot中開啟EhCache比較簡單,只需要在工程中加入 ehcache.xml 配置檔案並在 pom.xml 中增加ehcache依賴,框架只要發現該檔案,就會建立EhCache的快取管理器。

在 pom.xml 配置檔案中新增 jar 依賴:

		<dependency>
			<groupId>net.sf.ehcache</groupId>
			<artifactId>ehcache</artifactId>
		</dependency>

然後在 resources 目錄下建立 cache 資料夾,在cache 資料夾下建立 ehcache.xml 檔案:

<?xml version="1.0" encoding="UTF-8"?>
<ehcache updateCheck="false" name="defaultCache">
    <!-- 磁碟儲存位置:會在對應的目錄下生成相應的快取檔案 -->
    <diskStore path="src/main/resources/cache/temp"/>
    <!-- 預設快取配置. -->
    <defaultCache maxEntriesLocalHeap="100" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600"
                  overflowToDisk="true" maxEntriesLocalDisk="100000"/>
    <!-- 使用者系統快取 -->
    <cache name="cacheUserName" maxEntriesLocalHeap="100" eternal="true" overflowToDisk="true"/>
</ehcache>

說明:在 <cache name="***"> </cache>  定義好全域性快取的名字,就不需要在 Dao 層方法再指定名稱了,也無需在方法裡增加什麼註解。因此,UserDao 就可以解放:

package com.test.dao;

import com.test.entity.User;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface UserDao {

    /**
     * 新增使用者
     * @param user
     * @return
     */
    Integer addUserMybatis(User user);

    /**
     * 根據id刪除使用者
     * @param id
     * @return
     */
    Integer deleteUserMybatis(Integer id);

    /**
     * 更新使用者資訊
     * @param user
     * @return
     */
    Integer updateUserMybatis(User user);

    /**
     * 查詢所有使用者資訊
     * @return
     */
    List<User> getUserMybatis();
}

在類 MybatisController 裡增加一個測試快取的方法 cacheTest,並修改 getUserMybatis 方法:

    @Autowired(required = false)
    private CacheManager cacheManager;
    private final static String CACHE_NAME = "cacheUserName";
    private final static String GET_CACHE_KEY = "getCacheKey";

    /**
     * 測試快取
     * @return
     */
    @RequestMapping(value = "/cacheTest")
    public List<User> cacheTest(){
        Cache cache = cacheManager.getCache(CACHE_NAME);
        if(cache.get(GET_CACHE_KEY) != null){
            List<User> list = (List)cache.get(GET_CACHE_KEY).get();
            return list;
        }else{
            return new ArrayList<>();
        }
    }

    /**
     * 查詢所有使用者資訊
     * @return
     */
    @RequestMapping(value = "/getUserMybatis")
    public List<User> getUserMybatis()throws Exception{
        List<User> list;
        Cache cache = cacheManager.getCache(CACHE_NAME);
        //如果快取裡有資料,則取快取的資料;如果快取裡沒有資料,則查詢資料庫,並將結果放入快取中。
        if(cache.get(GET_CACHE_KEY) == null){
            list = userServiceMybatis.getUserMybatis();
            cache.put(GET_CACHE_KEY,list);
        }else{
            list = (List)cache.get(GET_CACHE_KEY).get();
        }
        System.out.println("執行查詢完畢,時間戳="+System.currentTimeMillis());
        return list;
    }

說明:cacheUserName 對應的是配置檔案 ehcache.xml 裡定義的快取name。

啟動服務,用 postman 測試,http://localhost:9090/getUserMybatis

發現有問題:

O的K,那就把 User 實體類序列化,很簡單,就是實現序列化介面即可: implements Serializable

public class User implements Serializable

重啟,再次測試 http://localhost:9090/getUserMybatis:完美無誤。這時候在目錄下也生成了快取檔案:

測試:http://localhost:9090/cacheTest,完美無誤。

接下來學習一下,如何清空快取:

    /**
     * 清空快取
     * @return
     */
    @RequestMapping(value = "/clearCache")
    public Map<String,Boolean> clearCache(){
        Cache cache = cacheManager.getCache(CACHE_NAME);
        cache.clear();
        Map<String, Boolean> map = new HashMap<>();
        map.put("flag", true);
        return map;
    }

重啟,測試,先執行 http://localhost:9090/getUserMybatis,快取裡有資料,再測試:http://localhost:9090/clearCache

再測試查詢使用者: http://localhost:9090/getUserMybatis,在 debug 模式下打斷點,可以看到快取裡沒有資料,重新去資料庫查詢資料了。

這樣的設計適用於一種場景:把使用者經常訪問的內容加入到快取,能加快資料的獲取,提高使用者的使用體驗。當需要修改內容時,可以清空快取的資料。第一個使用者點選獲取,查詢快取沒有資料了,就去資料庫查詢資料,然後加入快取。為了保證“順時”問題,可以使用資料覆蓋的形式,不一定需要清空,可以使用 put 方法進行覆蓋。