待緩存的POJO
/**
* Created by hgf on 16/6/22.
*/
public class Account {
private int id;
private String username;
public Account() {
}
public Account(int id, String username) {
this.id = id;
this.username = username;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", username='" + username + '\'' +
'}';
}
}
自己實現的簡單cache
cache的功能:
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* Created by hgf on 16/6/22.
*/
public class MyCache<T> {
private ConcurrentMap<String, T> cache = new ConcurrentHashMap<String, T>();
public T getValue(String key) {
return cache.get(key);
}
public void putOrUpdate(String key, T value) {
cache.put(key, value);
}
public void evictCache(String key) {
cache.remove(key);
}
public void evictAllInCache() {
cache.clear();
}
}
在CacheService中使用cahce:
/**
* Created by hgf on 16/6/22.
*/
public class MyCacheService {
private MyCache<Account> cache;
public MyCacheService() {
this.cache = new MyCache<Account>();
}
public Account getAccount(String cacheId){
Account account = cache.getValue(cacheId);
if(account!=null){
return account;
}
//get from DB or file
return null;
}
}
使用基於註解的spring cahce
使用基於註解的spring cache實現上面的cache
/**
* Created by hgf on 16/6/22.
*/
@Service
public class AccountService {
//假定此處是從數據庫獲取數據
public Account getFromDB(int id){
System.out.println("get "+ id + " from DB");
return new Account(id,"someone");
}
//如果命中cache,是不會執行getFromDB方法的
@Cacheable(value = "accountCache")
public String getById(int id){
return getFromDB(id);
}
}
spring xml配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">
<cache:annotation-driven />
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default"></bean>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="accountCache"></bean>
</set>
</property>
</bean>
<context:component-scan base-package="com.meituan"></context:component-scan>
</beans>
配置說明:
<cache:annotation-driven />
:用來添加spring的cache驅動,使得spring框架支持cache。這個配置項缺省使用了一個名字叫 cacheManager 的緩存管理器。所以下文中使用名爲cacheManager的緩存管理器。- 使用
org.springframework.cache.support.SimpleCacheManager
實現混存管理器,並讓spring加載。 - 配置
SimpleCacheManager
屬性caches
。該屬性是set類型。使用spring 已經實現的org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean
來保存(該類的實現也是使用java.util.concurrent.ConcurrentHashMap
類似於之前自定義的cache)。 p:name="accountCache"
設置緩存的名稱,在註解中可以指定緩存名稱,不知定就緩存在默認的緩存中。
默認的緩存管理器的名字只能是cacheManager,改爲其他的名稱不會被spring加載成功,導致找不到cacheManager緩存管理器錯誤。
啓動測試程序:
public class App
{
public static void main( String[] args ){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:application.xml");
AccountService accountService = applicationContext.getBean(AccountService.class);
for(int i = 0; i < 10; i++){
accountService.getById(2);
}
}
}
輸出:
get 2 from DB
只輸出一行,其餘的均命中cache,直接從cahce中讀。
修改緩存
更新緩存
想更改緩存中的數據,需要更新緩存
在AccountService中添加方法:
//修改
@CachePut(value = "accountCache", key = "#account.getId()")
public Account updateCache(Account account){
System.out.println("do put account");
return account;
}
使用@CachePut
註解,可以確保方法被執行,同時方法的返回值也被記錄到緩存中。
參數key
表示緩存(Cache)的健(key)與給定參數中的key的值相等纔會將返回結果緩存到cache中。
假設此處key設置爲key="1"
, 那麼只有account=1的時候纔會將返回結果緩存。
測試方法:
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:application.xml");
AccountService accountService = applicationContext.getBean(AccountService.class);
Account account = accountService.getById(2);
accountService.getById(2);
System.out.println("更新緩存-------------");
account.setUsername("xxxx");
accountService.updateCache(account);
account = accountService.getById(2);
System.out.println(account);
System.out.println("2-------------");
}
輸出:
get 2 from DB
更新緩存-------------
do put account
Account{id=2, username='xxxx'}
2-------------
輸出分析:
- 首次getById的時候,取數據庫
- 並再次取,不會從數據庫取
- 輸出更新緩存-------------
- 改變account內容,不改變Id
- 更新緩存
- 再次getById直接從緩存中取
- 輸出更新後的account內容
清理
例如需要強制刷刪除某個緩存,或者清空緩存。
@CacheEvict(value = "accountCache",key = "#id")
public void removeOneCache(int id){
}
@CacheEvict(value = "accountCache",key = "#account.getId()")
public void removeOneCache(Account account){
}
@CacheEvict(value = "accountCache",allEntries = true)
public void clearAll(){
}
只需要使用@CacheEvict
註解,就能將緩存中能與註解參數key作爲健值的項清除🆑。使用allEntries
表示刪除所有數據項。
總結:在
@Cacheable
中,key參數作用爲設置緩存的健;在@CacheEvict
,@CachePut
註解中,參數key作爲匹配緩存中數據的健的條件。
多參數緩存
使用註解中的key參數,將多個參數拼接結果作爲key。 例如:
@Cacheable(value = "accountCache",key = "#name.concat(#id)")
public Account getMultiKeyAccount(int id, String name){
System.out.println("get from DB");
return new Account(id,name);
}
不管是一個參數緩存還是在多參數緩存中,緩存中的key總是只能有一個,並且唯一,所有在多參數緩存中,只需要設定好多個參數組合成緩存系統中key的方式,就沒有什麼問題。
spring cache 原理
spring cache使用基於動態代理的AOP實現緩存機制。當客戶端通過spring 獲取某個對象foo,實際上獲取的不是該對象,而是一個代理對象proxy_foo。當客戶端調用foo的get方法時,實際上是在代理類中執行方法,並且能實現執行方法前後做一些操作。
在函數運行前,獲取參數(作爲key),運行後,獲取執行結果(作爲value);然後將它們存儲在緩存中;下次訪問時,先依據參數從緩存中查找,命中🎯直接返回,就不會執行函數了。
spring cache 中的坑
使用的限制
spring cache是基於動態生成的 proxy 代理機制來對方法的調用進行切面,這裏關鍵點是對象的引用問題,如果對象的方法是內部調用(即 this 引用)而不是外部引用,則會導致 proxy 失效,那麼我們的切面就失效,也就是說上面定義的各種註釋包括 @Cacheable
、@CachePut
和 @CacheEvict
都會失效。
例如:
@Cacheable(value = "accountCache")
public Account getById(int id){
return getFromDB(id);
}
public Account getById2(int id){
return this.getById(id);
}
如果直接使用getById2
,那麼是不能緩存返回值的。只有在getById2
上也添加@Cacheable(value = "accountCache")
註解,才能將返回的值添加到緩存。
總結:方法有內部調用帶有Cache相關注解的方法失效 可以考慮基於AspectJ實現AOP解決該問題。因爲動態代理機制是運行時代理,運行時客戶端實際上調用的是代理類的方法,當出現內部調用時,直接調用原函數,而不是代理後的函數;而AspectJ代理是編譯期的代理,客戶端調用的是已經編譯好的方法,cache也會在方法中實現,即使使用內部引用,也會對方法的整體調用,調用修改後的方法。
@CacheEvict 的可靠性問題
我們看到,@CacheEvict
註釋有一個屬性 beforeInvocation
,缺省爲 false,即缺省情況下,都是在實際的方法執行完成後,纔對緩存進行清空操作。期間如果執行方法出現異常,則會導致緩存清空不被執行。
例如,修改AccountService中clearAll方法:
@CacheEvict(value = "accountCache",allEntries = true)
public void clearAll(){
throw new RuntimeException();
}
測試方法
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:application.xml");
AccountService accountService = applicationContext.getBean(AccountService.class);
accountService.getById(1);
accountService.getById(1);
System.out.println("異常清理-------------");
try {
accountService.clearAll();
}
catch (Exception e){
//e.printStackTrace();
}
System.out.println("檢查--------------------");
accountService.getById(1);
}
輸出一行
get 1 from DB
只輸出一行,說明最後一次也命中cache,異常導致清空緩存失敗。