spring cache 頂 原

待緩存的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>

配置說明:

  1. <cache:annotation-driven />:用來添加spring的cache驅動,使得spring框架支持cache。這個配置項缺省使用了一個名字叫 cacheManager 的緩存管理器。所以下文中使用名爲cacheManager的緩存管理器。
  2. 使用org.springframework.cache.support.SimpleCacheManager實現混存管理器,並讓spring加載。
  3. 配置SimpleCacheManager屬性caches。該屬性是set類型。使用spring 已經實現的org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean來保存(該類的實現也是使用java.util.concurrent.ConcurrentHashMap類似於之前自定義的cache)。
  4. 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-------------

輸出分析:

  1. 首次getById的時候,取數據庫
  2. 並再次取,不會從數據庫取
  3. 輸出更新緩存-------------
  4. 改變account內容,不改變Id
  5. 更新緩存
  6. 再次getById直接從緩存中取
  7. 輸出更新後的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,異常導致清空緩存失敗

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章