前言
緩存技術在實際的項目中是必不可少的,合理的利用緩存技術能極大的提升網站的訪問速度,提升用戶體驗。 本片文章就介紹如何在spring boot中使用ehcache這個緩存框架。
ehcache介紹
在java中有很多技術都可以實現緩存功能,最簡單直接就是使用java自帶的Map容器,或者就是使用現有的緩存框架,例如memcache,ehcache ,以及非常熱門的redis。這裡介紹ehcache的主要是因為它真的很方便,而且memcache和redis都需要額外搭建服務,更適合分佈式部署的項目以便於各個模塊之間的使用共有的緩存內容。而ehcache主要是內存緩存,也可以緩存到磁盤中,速度快,效率高,功能也強大,適合我們一般的單個項目使用。
spring boot 配置ehcache
在spring boot中配置ehcahce主要有以下四步:
- pom.xml中添加依賴
- 配置ehcache.xml配置文件
- 開啟緩存
- 利用註解使用緩存
下面我們詳細介紹每一步。
添加依賴
要想在spring boot中使用緩存,首先需要開啟緩存,然後添加ehcache的依賴,所以我們在pom.xml中添加如下連個依賴項:
<!--開啟緩存-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- EhCache -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
編寫配置文件
添加了依賴之後,spring boot會自動默認加載src/mian/resources目錄下的ehcache.xml文件,所以我們需要在該目錄下手動創建該文件,這裡先給出一個樣例:
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
<!-- 磁盤緩存文件路徑 -->
<diskStore path="java.io.tmpdir"/>
<!-- 默認配置 -->
<defaultCache eternal="false"
maxElementsInMemory="1000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="0"
timeToLiveSeconds="600"
memoryStoreEvictionPolicy="LRU"/>
<!-- 自定義配置 -->
<cache name="userCache"
eternal="false"
maxElementsInMemory="1000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="0"
timeToLiveSeconds="600"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>
下面介紹樣例中出現的三個節點:
- <diskStore>:這個節點是非必須的,只有在使用了磁盤存儲的情況下才需要配置,表示緩存文件在磁盤中保存的路徑,該路徑通過path屬性來指定,磁盤緩存使用的文件後綴名是*.data和*.index,主要有以下幾個值:
- user.home:用戶主目錄
- user.dir:用戶當前的工作目錄
- java.io.tmpdir:默認臨時路徑
- ehcache.disk.store.dir:cache的配置目錄
- 自定義絕對路徑
如果對於這幾個目錄不熟悉,可以在java中獲取,如下:
public static void main(String[] args) {
System.out.println(System.getProperty("user.home"));
System.out.println(System.getProperty("user.dir"));
System.out.println(System.getProperty("java.io.tmpdir"));
}
下面是我本機打印出來的路徑,僅做參考:
C:\Users\Administrator
D:\Program Data\eclipse-workspace\springboot-ehcache
C:\Users\Administrator\AppData\Local\Temp\
這裡需要注意一點,要想某個對象被緩存到磁盤中,需要該對象實現序列化接口。
- <ehcache>:自定義緩存區,可以有零個或者多個,重要屬性如下:
- name:緩存區名字,必須屬性,用來區分緩存區的唯一標識。
- eternal:設置緩存區中的內容是否永久有效,可選值true或false,如果選擇true那麼設置的timeToIdleSeconds以及timeToLiveSeconds將失效。
- maxElementsInMemory:該緩存區中最多可以存放的對象數量,超過這個數量時,會根據overflowToDisk屬性的值有不同的操作。
- overflowToDisk:緩存對象超出最大數量時是否啟用磁盤保存,可選值true或false,值為true時,會將超出的內容緩存到磁盤中,為false時則會根據memoryStoreEvictionPolicy屬性配置的策略替換掉原來的內容。
- diskPersistent:磁盤存儲是否在虛擬機重啟後持續存在,默認是false,如果為true系統在初始化時會將磁盤中的內容加載到緩存。
- timeToIdleSeconds:設置一個元素在過期前的空閒時間(單位:秒),即訪問該元素的最大間隔時間,超過這個時間該元素就會被清除,默認值為0,表示一個元素可以無限的空閒。
- timeToLiveSeconds:設置一個元素在緩存區中的生存時間(單位:秒),即從創建到清除的時間,超過這個時間,該元素就會被清除,默認值為0,表示一個元素可以無限的保存。
- memoryStoreEvictionPolicy:緩存存儲與清除策略。即達到maxElementsInMemory限制並且overflowToDisk值為false時ehcache就會根據這個屬性的值執行相應的清空策略,該屬性有以下三個值分別代表ehcache的三種緩存清理策略,默認值為LRU:
- FIFO:先進先出策略(First In First Out)。
- LFU:最少被使用(Less Frequently Used),所有的緩存元素都會有一個屬性記錄該元素被使用的次數,清理元素時最小的那個將會被清除。
- LRU:最近最少使用(Least Resently Used),所有緩存的元素都會有一個屬性記錄最後一次使用的時間,清理元素時時間最早的那個元素將會被清除。
- diskExpiryThreadIntervalSeconds:磁盤緩存的清理線程運行間隔,默認是120秒。
- diskSpoolBufferSizeMB:設置磁盤緩存區的大小,默認為30MB。
- maxEntriesLocalDisk:設置磁盤緩存區最多能存放元素的數量。
- <defaultCache>:默認緩存區,即是一個name屬性為default的<ehcache>節點,屬性和<ehcache>節點都一樣,一個ehcache.xml文件中只能有一個<defaultCache>節點,當我們沒有自定義的<ehcache>時,默認使用該緩存區。
對於defaultCache這裡有需要注意的地方,因為他是一個特殊的<ehcache>,所以我們在自定義緩存區的時候不能再定義名為default的<ehcache>,並且在使用的時候也不能通過value=default來指定默認的緩存區。
這裡補充一點,項目中如果不想使用默認的路徑以及名字我們也可以自定義ehcache配置文件的名字以及路徑,在application.properties配置文件中配置如下內容:
#後邊的路徑可以自己指定
spring.cache.ehcache.config=classpath:ehcache.xml
開啟緩存
spring boot中開啟緩存非常簡單,只需要在在啟動類上添加一個@EnableCaching註解即可。
使用註解
spring boot中使用ehcache緩存主要是通過註解來使用,而且我們一般在service實現層使用緩存功能,常用的註解如下:
@Cacheable
該註解主要用在方法上邊,每當程序進入被該註解標記的方法時,系統會首先判斷緩存中是否存在相同key的元素,如果存在就直接返回緩存區中存放的值,並且不會執行方法的內容,如果不存在就執行該方法,並且判斷是否需要將返回值添加到緩存區中,常用屬性:
- value:指定使用哪個緩存區,就是我們在配置文件裡邊配置的<ehcache>節點的name屬性對應的值,可以指定多個值。
//指定一個
@Cacheable(value="userCache")
//指定多個
@Cacheable(value={"userCache","userCache2"})
- key:緩存元素的key,需要按照SpEL表達式編寫,這個我們一般按照指定方法的參數來確定。
//#p0表示將第一個參數當成key,也可以直接寫參數名字例如:#id,兩者表達意思一樣
@Cacheable(value="userCache",key="#p0")
public SysUser getById(Integer id){//內容省略...};
- condition:添加緩存的條件,需要按照SpEL表達式編寫,僅當該屬性返回true時才添加緩存。
//僅當id>10時才緩存
@Cacheable(value="userCache",key="#p0",condition="#p0>10")
public SysUser getById(Integer id){//內容省略...};
@CachePut
該註解主要用在方法上邊,能夠根據方法的參數以及返回值以及自定義的條件判斷是否添加緩存,該註解標記的方法一定會執行,其屬性與@Cacheable一致。
@CachePut(value="userCache",key="#entity.id")
public SysUser insertSysuser(SysUser entity) {
// TODO Auto-generated method stub
//省略內容
}
@CacheEvict
該註解主要用在方法上邊,能根據條件對緩存進行清空,常用屬性如下:
- value:同上
- key:同上
- condition:同上
- allEntries:是否清空所有緩存內容,默認為false,如果設置為true,那麼在方法執行完成之後並且滿足condition條件時會清空該緩存區的所有內容。
- beforeInvocation:清除內容操作是否發生在方法執行之前,默認為false,表示清除操作在方法執行完之後再進行,如果方法執行過程中拋出異常,那麼清除操作就不執行,如果為true,則表示在方法執行之前執行清除操作。
@CacheEvict(value="userCache",key="#p0",allEntries=false, beforeInvocation=true)
public int deleteByPrimarykey(Integer key) {
// TODO Auto-generated method stub
//省略內容
}
效果測試
上邊介紹了spring boot配置ehcache的步驟,接下來測試緩存效果,本項目在整合了Mybatis以及日誌框架的前提下進行,基本的代碼就不貼出來了,直接給出最關鍵的service實現層以及controller的代碼:
SysuserServiceImpl.java
package com.web.springbootehcache.service.impl;
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.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import com.web.springbootehcache.dao.SysUserMapper;
import com.web.springbootehcache.entity.SysUser;
import com.web.springbootehcache.service.IsysUserService;
/**
* @author Promise
* @createTime 2019年3月19日
* @description
*/
@Service("sysuserService")
public class SysUserServiceImpl implements IsysUserService{
private final static Logger log = LoggerFactory.getLogger(SysUserServiceImpl.class);
@Autowired
private SysUserMapper sysuserMapper;
@Override
@Cacheable(value="userCache",key="#p0")
public SysUser fingByPrimarykey(Integer key) {
// TODO Auto-generated method stub
log.debug("去數據庫查詢了數據!");
return sysuserMapper.selectByPrimaryKey(key);
}
@Override
@CachePut(value="userCache",key="#p0.id")
public SysUser updateSysuser(SysUser entity) {
// TODO Auto-generated method stub
log.debug("更新了數據庫數據!");
int res = sysuserMapper.updateByPrimaryKey(entity);
if(res >0)
return entity;
else
return null;
}
@Override
@CachePut(value="userCache",key="#entity.id")
public SysUser insertSysuser(SysUser entity) {
// TODO Auto-generated method stub
int res = sysuserMapper.insert(entity);
log.debug("新增了數據!id為:{}",entity.getId());
if(res >0)
return entity;
else
return null;
}
@Override
@CacheEvict(value="userCache",key="#p0",beforeInvocation=true)
public int deleteByPrimarykey(Integer key) {
// TODO Auto-generated method stub
log.debug("刪除了數據!");
return sysuserMapper.deleteByPrimaryKey(key);
}
}
該類中給出了基本的CRUD操作對應的緩存操作,當然不是絕對的,實際使用中根據自己需要改動。
IndexController.java
package com.web.springbootehcache.controller;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.web.springbootehcache.entity.SysUser;
import com.web.springbootehcache.service.IsysUserService;
/**
* @author Promise
* @createTime 2019年3月19日
* @description
*/
@RestController
public class IndexController {
private final static Logger log = LoggerFactory.getLogger(IndexController.class);
@Autowired
private IsysUserService sysuserService;
@RequestMapping(value="/select/{id}")
public Object index(@PathVariable Integer id) {
Map<String, Object> map = new HashMap<>();
SysUser sysuser = sysuserService.fingByPrimarykey(id);
log.debug("查詢了id為:{}的用戶信息!",sysuser.getId());
SysUser sysuser2 = sysuserService.fingByPrimarykey(id);
log.debug("查詢了id為:{}的用戶信息!",sysuser2.getId());
map.put("res", sysuser);
return map;
}
@RequestMapping(value="/update")
public Object update() {
Map<String, Object> map = new HashMap<>();
//第一次修改
SysUser sysuser = new SysUser(1, "eran", "eran1", 20, "M");
sysuserService.updateSysuser(sysuser);
//第一次查詢
sysuser = sysuserService.fingByPrimarykey(1);
log.debug("查詢了id為:{}的用戶信息!",sysuser.getId());
//第2次修改
sysuser = new SysUser(1, "eran", "eran2", 20, "M");
sysuserService.updateSysuser(sysuser);
//第2次查詢
sysuser = sysuserService.fingByPrimarykey(1);
log.debug("查詢了id為:{}的用戶信息!",sysuser.getId());
map.put("res", sysuser);
return map;
}
@RequestMapping(value="/insert")
public Object insert() {
Map<String, Object> map = new HashMap<>();
SysUser sysuser = new SysUser();
sysuser.setName("admin");
sysuser.setAge(22);
sysuser.setPass("admin");
sysuser.setSex("M");
sysuserService.insertSysuser(sysuser);
//查詢
sysuser = sysuserService.fingByPrimarykey(sysuser.getId());
map.put("res", sysuser);
return map;
}
@RequestMapping(value="/delete/{id}")
public Object delete(@PathVariable Integer id) {
Map<String, Object> map = new HashMap<>();
sysuserService.deleteByPrimarykey(id);
//查詢
SysUser sysuser = sysuserService.fingByPrimarykey(id);
map.put("res", sysuser);
return map;
}
}
數據庫測試數據
啟動項目,訪問localhost:1188/select/1,控制檯日誌如下:
預期效果:執行查詢操作兩次,訪問數據庫一次。
[default]2019-03-20 17:35:05,287 [http-nio-1188-exec-2 32] DEBUG >> 去數據庫查詢了數據! >> c.w.s.s.i.SysUserServiceImpl
[default]2019-03-20 17:35:05,327 [http-nio-1188-exec-2 110] INFO >> HikariPool-1 - Starting... >> c.z.h.HikariDataSource
[default]2019-03-20 17:35:05,332 [http-nio-1188-exec-2 68] WARN >> Registered driver with driverClassName=com.mysql.jdbc.Driver was not found, trying direct instantiation. >> c.z.h.u.DriverDataSource
[default]2019-03-20 17:35:06,106 [http-nio-1188-exec-2 123] INFO >> HikariPool-1 - Start completed. >> c.z.h.HikariDataSource
[default]2019-03-20 17:35:06,113 [http-nio-1188-exec-2 159] DEBUG >> ==> Preparing: select id, `name`, pass, sex, age from sys_user where id = ? >> c.w.s.d.S.selectByPrimaryKey
[default]2019-03-20 17:35:06,141 [http-nio-1188-exec-2 159] DEBUG >> ==> Parameters: 1(Integer) >> c.w.s.d.S.selectByPrimaryKey
[default]2019-03-20 17:35:06,176 [http-nio-1188-exec-2 159] DEBUG >> <== Total: 1 >> c.w.s.d.S.selectByPrimaryKey
[default]2019-03-20 17:35:06,185 [http-nio-1188-exec-2 33] DEBUG >> 查詢了id為:1的用戶信息! >> c.w.s.c.IndexController
[default]2019-03-20 17:35:06,186 [http-nio-1188-exec-2 35] DEBUG >> 查詢了id為:1的用戶信息! >> c.w.s.c.IndexController
可以很直白的看出,我們執行了兩次查詢操作,但是從數據庫中取數據的操作就執行了一次,可見還有一次直接從緩存中取數據,達到了我們預期的效果。
訪問localhost:1188/update,代碼中我們對id為2的數據做了兩次修改以及兩次查詢操作,並且在執行修改操作時緩存了數據,執行該方法之前,id為2的數據還不在緩存中。
預期效果:執行兩次修改操作,訪問兩次數據庫,兩次查詢操作不訪問數據庫。
[default]2019-03-20 17:47:37,254 [http-nio-1188-exec-1 40] DEBUG >> 更新了數據庫數據! >> c.w.s.s.i.SysUserServiceImpl
[default]2019-03-20 17:47:37,291 [http-nio-1188-exec-1 110] INFO >> HikariPool-1 - Starting... >> c.z.h.HikariDataSource
[default]2019-03-20 17:47:37,299 [http-nio-1188-exec-1 68] WARN >> Registered driver with driverClassName=com.mysql.jdbc.Driver was not found, trying direct instantiation. >> c.z.h.u.DriverDataSource
[default]2019-03-20 17:47:37,953 [http-nio-1188-exec-1 123] INFO >> HikariPool-1 - Start completed. >> c.z.h.HikariDataSource
[default]2019-03-20 17:47:37,964 [http-nio-1188-exec-1 159] DEBUG >> ==> Preparing: update sys_user set `name` = ?, pass = ?, sex = ?, age = ? where id = ? >> c.w.s.d.S.updateByPrimaryKey
[default]2019-03-20 17:47:38,006 [http-nio-1188-exec-1 159] DEBUG >> ==> Parameters: eran(String), eran1(String), M(String), 20(Integer), 2(Integer) >> c.w.s.d.S.updateByPrimaryKey
[default]2019-03-20 17:47:38,104 [http-nio-1188-exec-1 159] DEBUG >> <== Updates: 1 >> c.w.s.d.S.updateByPrimaryKey
[default]2019-03-20 17:47:38,237 [http-nio-1188-exec-1 48] DEBUG >> 查詢了id為:2的用戶信息! >> c.w.s.c.IndexController
[default]2019-03-20 17:47:38,239 [http-nio-1188-exec-1 40] DEBUG >> 更新了數據庫數據! >> c.w.s.s.i.SysUserServiceImpl
[default]2019-03-20 17:47:38,239 [http-nio-1188-exec-1 159] DEBUG >> ==> Preparing: update sys_user set `name` = ?, pass = ?, sex = ?, age = ? where id = ? >> c.w.s.d.S.updateByPrimaryKey
[default]2019-03-20 17:47:38,243 [http-nio-1188-exec-1 159] DEBUG >> ==> Parameters: eran(String), eran2(String), M(String), 20(Integer), 2(Integer) >> c.w.s.d.S.updateByPrimaryKey
[default]2019-03-20 17:47:38,286 [http-nio-1188-exec-1 159] DEBUG >> <== Updates: 1 >> c.w.s.d.S.updateByPrimaryKey
[default]2019-03-20 17:47:38,287 [http-nio-1188-exec-1 54] DEBUG >> 查詢了id為:2的用戶信息! >> c.w.s.c.IndexController
結果符合我們預期。
新增操作和更新操作原理一樣都是使用@CachePut註解,這裡就不重複演示,直接測試刪除數據清除相應緩存功能,訪問localhost:1188/delete/2,此時緩存區中有id為1,2的兩條數據,我們刪除id為2的數據,再做查詢操作。
預期效果:刪除數據訪問數據庫一次,並清除緩存區中那個相應的數據,因為清除了緩存區的內容所以查詢數據會訪問數據庫一次,但是數據庫中相應的內容也已經被刪除,所以查詢不到任何數據。
[default]2019-03-20 17:57:28,337 [http-nio-1188-exec-4 64] DEBUG >> 刪除了數據! >> c.w.s.s.i.SysUserServiceImpl
[default]2019-03-20 17:57:28,341 [http-nio-1188-exec-4 159] DEBUG >> ==> Preparing: delete from sys_user where id = ? >> c.w.s.d.S.deleteByPrimaryKey
[default]2019-03-20 17:57:28,342 [http-nio-1188-exec-4 159] DEBUG >> ==> Parameters: 2(Integer) >> c.w.s.d.S.deleteByPrimaryKey
[default]2019-03-20 17:57:28,463 [http-nio-1188-exec-4 159] DEBUG >> <== Updates: 1 >> c.w.s.d.S.deleteByPrimaryKey
[default]2019-03-20 17:57:28,464 [http-nio-1188-exec-4 32] DEBUG >> 去數據庫查詢了數據! >> c.w.s.s.i.SysUserServiceImpl
[default]2019-03-20 17:57:28,467 [http-nio-1188-exec-4 159] DEBUG >> ==> Preparing: select id, `name`, pass, sex, age from sys_user where id = ? >> c.w.s.d.S.selectByPrimaryKey
[default]2019-03-20 17:57:28,468 [http-nio-1188-exec-4 159] DEBUG >> ==> Parameters: 2(Integer) >> c.w.s.d.S.selectByPrimaryKey
[default]2019-03-20 17:57:28,494 [http-nio-1188-exec-4 159] DEBUG >> <== Total: 0 >> c.w.s.d.S.selectByPrimaryKey
日誌輸出的內容符合我們預期。
結語
好了,spring boot 整合ehcache的內容就到此為止了,下篇再見,bye~~