《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 方法進行覆蓋。