Spring Cache抽象詳解

jopen 10年前發布 | 31K 次閱讀 緩存組件 Spring Cache

緩存簡介

緩存,我的理解是:讓數據更接近于使用者;工作機制是:先從緩存中讀取數據,如果沒有再從慢速設備上讀取實際數據(數據也會存入緩存);緩存什么:那些經常讀取且不經常修改的數據/那些昂貴(CPU/IO)的且對于相同的請求有相同的計算結果的數據。如CPU--L1/L2--內存 --磁盤就是一個典型的例子,CPU需要數據時先從L1/L2中讀取,如果沒有到內存中找,如果還沒有會到磁盤上找。還有如用過Maven的朋友都應該知道,我們找依賴的時候,先從本機倉庫找,再從本地服務器倉庫找,最后到遠程倉庫服務器找;還有如京東的物流為什么那么快?他們在各個地都有分倉庫,如果該倉庫有貨物那么送貨的速度是非常快的。

 

緩存命中率

即從緩存中讀取數據的次數 與 總讀取次數的比率,命中率越高越好:

命中率 = 從緩存中讀取次數 / (總讀取次數[從緩存中讀取次數 + 從慢速設備上讀取的次數])

Miss率 = 沒有從緩存中讀取的次數 / (總讀取次數[從緩存中讀取次數 + 從慢速設備上讀取的次數])

 

這是一個非常重要的監控指標,如果做緩存一定要健康這個指標來看緩存是否工作良好;

 

緩存策略

Eviction policy

移除策略,即如果緩存滿了,從緩存中移除數據的策略;常見的有LFU、LRU、FIFO:

FIFO(First In First Out):先進先出算法,即先放入緩存的先被移除;

LRU(Least Recently Used):最久未使用算法,使用時間距離現在最久的那個被移除;

LFU(Least Frequently Used):最近最少使用算法,一定時間段內使用次數(頻率)最少的那個被移除;

 

TTL(Time To Live )

存活期,即從緩存中創建時間點開始直到它到期的一個時間段(不管在這個時間段內有沒有訪問都將過期)

 

TTI(Time To Idle)

空閑期,即一個數據多久沒被訪問將從緩存中移除的時間。

 

 

到此,基本了解了緩存的知識,在Java中,我們一般對調用方法進行緩存控制,比如我調用"findUserById(Long id)",那么我應該在調用這個方法之前先從緩存中查找有沒有,如果沒有再掉該方法如從數據庫加載用戶,然后添加到緩存中,下次調用時將會從緩存中獲取到數據。

 

自Spring 3.1起,提供了類似于@Transactional注解事務的注解Cache支持,且提供了Cache抽象;在此之前一般通過AOP實現;使用Spring Cache的好處:

提供基本的Cache抽象,方便切換各種底層Cache;

通過注解Cache可以實現類似于事務一樣,緩存邏輯透明的應用到我們的業務代碼上,且只需要更少的代碼就可以完成;

提供事務回滾時也自動回滾緩存;

支持比較復雜的緩存邏輯;

 

對于Spring Cache抽象,主要從以下幾個方面學習:

  • Cache API及默認提供的實現

    </li>

  • Cache注解

    </li>

  • 實現復雜的Cache邏輯

    </li> </ul>

     

    Cache API及默認提供的實現

    Spring提供的核心Cache接口: 

    package org.springframework.cache;

    public interface Cache { String getName();  //緩存的名字 Object getNativeCache(); //得到底層使用的緩存,如Ehcache ValueWrapper get(Object key); //根據key得到一個ValueWrapper,然后調用其get方法獲取值 <T> T get(Object key, Class<T> type);//根據key,和value的類型直接獲取value void put(Object key, Object value);//往緩存放數據 void evict(Object key);//從緩存中移除key對應的緩存 void clear(); //清空緩存

    interface ValueWrapper { //緩存值的Wrapper
        Object get(); //得到真實的value
    

            } }</pre>

    提供了緩存操作的讀取/寫入/移除方法;

     

     

    默認提供了如下實現:

    ConcurrentMapCache:使用java.util.concurrent.ConcurrentHashMap實現的Cache;

    GuavaCache:對Guava com.google.common.cache.Cache進行的Wrapper,需要Google Guava 12.0或更高版本,@since spring 4;

    EhCacheCache:使用Ehcache實現

    JCacheCache:對javax.cache.Cache進行的wrapper,@since spring 3.2;spring4將此類更新到JCache 0.11版本;

     

    另外,因為我們在應用中并不是使用一個Cache,而是多個,因此Spring還提供了CacheManager抽象,用于緩存的管理: 

    package org.springframework.cache;
    import java.util.Collection;
    public interface CacheManager {
        Cache getCache(String name); //根據Cache名字獲取Cache 
        Collection<String> getCacheNames(); //得到所有Cache的名字
    }

     

    默認提供的實現: 

    ConcurrentMapCacheManager/ConcurrentMapCacheFactoryBean:管理ConcurrentMapCache;

    GuavaCacheManager;

    EhCacheCacheManager/EhCacheManagerFactoryBean;

    JCacheCacheManager/JCacheManagerFactoryBean;

     

    另外還提供了CompositeCacheManager用于組合CacheManager,即可以從多個CacheManager中輪詢得到相應的Cache,如

    <bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">
        <property name="cacheManagers">
            <list>
                <ref bean="ehcacheManager"/>
                <ref bean="jcacheManager"/>
            </list>
        </property>
        <property name="fallbackToNoOpCache" value="true"/>
    </bean>

    當我們調用cacheManager.getCache(cacheName) 時,會先從第一個cacheManager中查找有沒有cacheName的cache,如果沒有接著查找第二個,如果最后找不到,因為 fallbackToNoOpCache=true,那么將返回一個NOP的Cache否則返回null。

     

     

    除了GuavaCacheManager之外,其他Cache都支持Spring事務的,即如果事務回滾了,Cache的數據也會移除掉。

     

    Spring不進行Cache的緩存策略的維護,這些都是由底層Cache自己實現,Spring只是提供了一個Wrapper,提供一套對外一致的API。

     

    示例

    需要添加Ehcache依賴,具體依賴輕參考pom.xml

     

    SpringCacheTest.java

        @Test
        public void test() throws IOException {
            //創建底層Cache
            net.sf.ehcache.CacheManager ehcacheManager
                    = new net.sf.ehcache.CacheManager(new ClassPathResource("ehcache.xml").getInputStream());

            //創建Spring的CacheManager         EhCacheCacheManager cacheCacheManager = new EhCacheCacheManager();         //設置底層的CacheManager         cacheCacheManager.setCacheManager(ehcacheManager);

            Long id = 1L;         User user = new User(id, "zhang", "zhang@gmail.com");

            //根據緩存名字獲取Cache         Cache cache = cacheCacheManager.getCache("user");         //往緩存寫數據         cache.put(id, user);         //從緩存讀數據         Assert.assertNotNull(cache.get(id, User.class));     }</pre>

    此處直接使用Spring提供的API進行操作;我們也可以通過xml/注解方式配置到spring容器;

     

    xml風格的(spring-cache.xml):

        <bean id="ehcacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
            <property name="configLocation" value="classpath:ehcache.xml"/>
        </bean>

        <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">         <property name="cacheManager" ref="ehcacheManager"/>         <property name="transactionAware" value="true"/>     </bean></pre>

    spring提供EhCacheManagerFactoryBean來簡化ehcache cacheManager的創建,這樣注入configLocation,會自動根據路徑從classpath下找,比編碼方式簡單多了,然后就可以從 spring容器獲取cacheManager進行操作了。此處的transactionAware表示是否事務環繞的,如果true,則如果事務回滾,緩存也回滾,默認false。

     

     

    注解風格的(AppConfig.java):

        @Bean
        public CacheManager cacheManager() {

            try {             net.sf.ehcache.CacheManager ehcacheCacheManager                     = new net.sf.ehcache.CacheManager(new ClassPathResource("ehcache.xml").getInputStream());

                EhCacheCacheManager cacheCacheManager = new EhCacheCacheManager(ehcacheCacheManager);             return cacheCacheManager;         } catch (IOException e) {             throw new RuntimeException(e);         }     }</pre>

    和編程方式差不多就不多介紹了。

    另外,除了這些默認的Cache之外,我們可以寫自己的Cache實現;而且即使不用之后的Spring Cache注解,我們也盡量使用Spring Cache API進行Cache的操作,如果要替換底層Cache也是非常方便的。到此基本的Cache API就介紹完了,接下來我們來看看使用Spring Cache注解來簡化Cache的操作。

     

    Cache注解

    啟用Cache注解

    XML風格的(spring-cache.xml):

    <cache:annotation-driven cache-manager="cacheManager" proxy-target-class="true"/>

    另外還可以指定一個 key-generator,即默認的key生成策略,后邊討論;

     

     

    注解風格的(AppConfig.java):

    @Configuration
    @ComponentScan(basePackages = "com.sishuok.spring.service")
    @EnableCaching(proxyTargetClass = true)
    public class AppConfig implements CachingConfigurer {
        @Bean
        @Override
        public CacheManager cacheManager() {

            try {             net.sf.ehcache.CacheManager ehcacheCacheManager                     = new net.sf.ehcache.CacheManager(new ClassPathResource("ehcache.xml").getInputStream());

                EhCacheCacheManager cacheCacheManager = new EhCacheCacheManager(ehcacheCacheManager);             return cacheCacheManager;         } catch (IOException e) {             throw new RuntimeException(e);         }     }

        @Bean     @Override     public KeyGenerator keyGenerator() {         return new SimpleKeyGenerator();     } }</pre>

    1、使用@EnableCaching啟用Cache注解支持;

    2、實現CachingConfigurer,然后注入需要的cacheManager和keyGenerator;從spring4開始默認的keyGenerator是SimpleKeyGenerator;

     

    <h3>@CachePut </h3>

    應用到寫數據的方法上,如新增/修改方法,調用方法時會自動把相應的數據放入緩存: 

        @CachePut(value = "user", key = "#user.id")
        public User save(User user) {
            users.add(user);
            return user;
        }

    即調用該方法時,會把user.id作為key,返回值作為value放入緩存;

     

    @CachePut注解:

    public @interface CachePut {
        String[] value();              //緩存的名字,可以把數據寫到多個緩存
        String key() default "";       //緩存key,如果不指定將使用默認的KeyGenerator生成,后邊介紹
        String condition() default ""; //滿足緩存條件的數據才會放入緩存,condition在調用方法之前和之后都會判斷
        String unless() default "";    //用于否決緩存更新的,不像condition,該表達只在方法執行之后判斷,此時可以拿到返回值result進行判斷了
    }

     

    <h3>@CacheEvict </h3>

    即應用到移除數據的方法上,如刪除方法,調用方法時會從緩存中移除相應的數據:

        @CacheEvict(value = "user", key = "#user.id") //移除指定key的數據
        public User delete(User user) {
            users.remove(user);
            return user;
        }
        @CacheEvict(value = "user", allEntries = true) //移除所有數據
        public void deleteAll() {
            users.clear();
        }

     

    @CacheEvict注解:

    public @interface CacheEvict {
        String[] value();                        //請參考@CachePut
        String key() default "";                 //請參考@CachePut
        String condition() default "";           //請參考@CachePut
        boolean allEntries() default false;      //是否移除所有數據
        boolean beforeInvocation() default false;//是調用方法之前移除/還是調用之后移除

     

    <h3>@Cacheable</h3>

    應用到讀取數據的方法上,即可緩存的方法,如查找方法:先從緩存中讀取,如果沒有再調用方法獲取數據,然后把數據添加到緩存中:

       @Cacheable(value = "user", key = "#id")
        public User findById(final Long id) {
            System.out.println("cache miss, invoke find by id, id:" + id);
            for (User user : users) {
                if (user.getId().equals(id)) {
                    return user;
                }
            }
            return null;
        }

     

    @Cacheable注解:

    public @interface Cacheable {
        String[] value();             //請參考@CachePut
        String key() default "";      //請參考@CachePut
        String condition() default "";//請參考@CachePut
        String unless() default "";   //請參考@CachePut

     

    運行流程

    1、首先執行@CacheEvict(如果beforeInvocation=true且condition 通過),如果allEntries=true,則清空所有
    2、接著收集@Cacheable(如果condition 通過,且key對應的數據不在緩存),放入cachePutRequests(也就是說如果cachePutRequests為空,則數據在緩存中)
    3、如果cachePutRequests為空且沒有@CachePut操作,那么將查找@Cacheable的緩存,否則result=緩存數據(也就是說只要當沒有cache put請求時才會查找緩存)
    4、如果沒有找到緩存,那么調用實際的API,把結果放入result
    5、如果有@CachePut操作(如果condition 通過),那么放入cachePutRequests
    6、執行cachePutRequests,將數據寫入緩存(unless為空或者unless解析結果為false);
    7、執行@CacheEvict(如果beforeInvocation=false 且 condition 通過),如果allEntries=true,則清空所有

    流程中需要注意的就是2/3/4步:

    如果有@CachePut操作,即使有@Cacheable也不會從緩存中讀取;問題很明顯,如果要混合多個注解使用,不能組合使用 @CachePut和@Cacheable;官方說應該避免這樣使用(解釋是如果帶條件的注解相互排除的場景);不過個人感覺還是不要考慮這個好,讓用戶來決定如何使用,否則一會介紹的場景不能滿足。

    提供的SpEL上下文數據

    Spring Cache提供了一些供我們使用的SpEL上下文數據,下表直接摘自Spring官方文檔:

    </tr>

    名字 位置 描述 示例

    methodName

    </td>

    root對象

    </td>

    當前被調用的方法名

    </td>

    #root.methodName

    </td> </tr>

    method

    </td>

    root對象

    </td>

    當前被調用的方法

    </td>

    #root.method.name

    </td> </tr>

    target

    </td>

    root對象

    </td>

    當前被調用的目標對象

    </td>

    #root.target

    </td> </tr>

    targetClass

    </td>

    root對象

    </td>

    當前被調用的目標對象類

    </td>

    #root.targetClass

    </td> </tr>

    args

    </td>

    root對象

    </td>

    當前被調用的方法的參數列表

    </td>

    #root.args[0]

    </td> </tr>

    caches

    </td>

    root對象

    </td>

    當前方法調用使用的緩存列表(如@Cacheable(value={"cache1", "cache2"})),則有兩個cache

    </td>

    #root.caches[0].name

    </td> </tr>

    argument name

    </td>

    執行上下文

    </td>

    當前被調用的方法的參數,如findById(Long id),我們可以通過#id拿到參數

    </td>

    #user.id

    </td> </tr>

    result

    </td>

    執行上下文

    </td>

    方法執行后的返回值(僅當方法執行之后的判斷有效,如‘unless’,'cache evict'的beforeInvocation=false)

    </td>

    #result

    </td> </tr> </tbody> </table>

    通過這些數據我們可能實現比較復雜的緩存邏輯了,后邊再來介紹。

     

    Key生成器

    如果在Cache注解上沒有指定key的話@CachePut(value = "user"),會使用KeyGenerator進行生成一個key: 

    public interface KeyGenerator {
        Object generate(Object target, Method method, Object... params);
    }

    默認提供了DefaultKeyGenerator生成器(Spring 4之后使用SimpleKeyGenerator): 

    @Override
        public Object generate(Object target, Method method, Object... params) {
            if (params.length == 0) {
                return SimpleKey.EMPTY;
            }
            if (params.length == 1 && params[0] != null) {
                return params[0];
            }
            return new SimpleKey(params);
        }

    即如果只有一個參數,就使用參數作為key,否則使用SimpleKey作為key。

     

    我們也可以自定義自己的key生成器,然后通過xml風格的<cache:annotation-driven key-generator=""/>或注解風格的CachingConfigurer中指定keyGenerator。 

     

    條件緩存

    根據運行流程,如下@Cacheable將在執行方法之前( #result還拿不到返回值)判斷condition,如果返回true,則查緩存; 

    @Cacheable(value = "user", key = "#id", condition = "#id lt 10")
    public User conditionFindById(final Long id)

     

    根據運行流程,如下@CachePut將在執行完方法后(#result就能拿到返回值了)判斷condition,如果返回true,則放入緩存; 

    @CachePut(value = "user", key = "#id", condition = "#result.username ne 'zhang'")
    public User conditionSave(final User user)

      

    根據運行流程,如下@CachePut將在執行完方法后(#result就能拿到返回值了)判斷unless,如果返回false,則放入緩存;(即跟condition相反)

    @CachePut(value = "user", key = "#user.id", unless = "#result.username eq 'zhang'")
        public User conditionSave2(final User user)

     

    根據運行流程,如下@CacheEvict, beforeInvocation=false表示在方法執行之后調用(#result能拿到返回值了);且判斷condition,如果返回true,則移除緩存;

        @CacheEvict(value = "user", key = "#user.id", beforeInvocation = false, condition = "#result.username ne 'zhang'")
        public User conditionDelete(final User user)

     

    <h3>@Caching</h3>

    有時候我們可能組合多個Cache注解使用;比如用戶新增成功后,我們要添加id-->user;username--->user;email--->user的緩存;此時就需要@Caching組合多個注解標簽了。

     

    如用戶新增成功后,添加id-->user;username--->user;email--->user到緩存; 

        @Caching(
                put = {
                        @CachePut(value = "user", key = "#user.id"),
                        @CachePut(value = "user", key = "#user.username"),
                        @CachePut(value = "user", key = "#user.email")
                }
        )
        public User save(User user) {

      

    @Caching定義如下: 

    public @interface Caching {
        Cacheable[] cacheable() default {}; //聲明多個@Cacheable
        CachePut[] put() default {};        //聲明多個@CachePut
        CacheEvict[] evict() default {};    //聲明多個@CacheEvict
    }

     

    自定義緩存注解

    比如之前的那個@Caching組合,會讓方法上的注解顯得整個代碼比較亂,此時可以使用自定義注解把這些注解組合到一個注解中,如: 

    @Caching(
            put = {
                    @CachePut(value = "user", key = "#user.id"),
                    @CachePut(value = "user", key = "#user.username"),
                    @CachePut(value = "user", key = "#user.email")
            }
    )
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    public @interface UserSaveCache {
    }

     

    這樣我們在方法上使用如下代碼即可,整個代碼顯得比較干凈。 

    @UserSaveCache
    public User save(User user)

      

    示例

    新增/修改數據時往緩存中寫 

        @Caching(
                put = {
                        @CachePut(value = "user", key = "#user.id"),
                        @CachePut(value = "user", key = "#user.username"),
                        @CachePut(value = "user", key = "#user.email")
                }
        )
        public User save(User user)

     

        @Caching(
                put = {
                        @CachePut(value = "user", key = "#user.id"),
                        @CachePut(value = "user", key = "#user.username"),
                        @CachePut(value = "user", key = "#user.email")
                }
        )
        public User update(User user)

    刪除數據時從緩存中移除

        @Caching(
                evict = {
                        @CacheEvict(value = "user", key = "#user.id"),
                        @CacheEvict(value = "user", key = "#user.username"),
                        @CacheEvict(value = "user", key = "#user.email")
                }
        )
        public User delete(User user)
       @CacheEvict(value = "user", allEntries = true)
        public void deleteAll()

     

    查找時從緩存中讀

        @Caching(
                cacheable = {
                        @Cacheable(value = "user", key = "#id")
                }
        )
        public User findById(final Long id)

     

       @Caching(
                cacheable = {
                        @Cacheable(value = "user", key = "#username")
                }
        )
        public User findByUsername(final String username)

     

      @Caching(
                cacheable = {
                        @Cacheable(value = "user", key = "#email")
                }
        )
        public User findByEmail(final String email)

      

     

    問題及解決方案

    一、比如findByUsername時,不應該只放username-->user,應該連同id--->user和 email--->user一起放入;這樣下次如果按照id查找直接從緩存中就命中了;這需要根據之前的運行流程改造 CacheAspectSupport: 

    // We only attempt to get a cached result if there are no put requests
    if (cachePutRequests.isEmpty() && contexts.get(CachePutOperation.class).isEmpty()) {
        result = findCachedResult(contexts.get(CacheableOperation.class));
    }

    改為:

    Collection<CacheOperationContext> cacheOperationContexts = contexts.get(CacheableOperation.class);
    if (!cacheOperationContexts.isEmpty()) {
        result = findCachedResult(cacheOperationContexts);
    }

    然后就可以通過如下代碼完成想要的功能: 

        @Caching(
                cacheable = {
                        @Cacheable(value = "user", key = "#username")
                },
                put = {
                        @CachePut(value = "user", key = "#result.id", condition = "#result != null"),
                        @CachePut(value = "user", key = "#result.email", condition = "#result != null")
                }
        )
        public User findByUsername(final String username) {
            System.out.println("cache miss, invoke find by username, username:" + username);
            for (User user : users) {
                if (user.getUsername().equals(username)) {
                    return user;
                }
            }
            return null;
        }

      

     

    二、緩存注解會讓代碼看上去比較亂;應該使用自定義注解把緩存注解提取出去;

     

    三、往緩存放數據/移除數據是有條件的,而且條件可能很復雜,考慮使用SpEL表達式:

        @CacheEvict(value = "user", key = "#user.id", condition = "#root.target.canCache() and #root.caches[0].get(#user.id).get().username ne #user.username", beforeInvocation = true)
        public void conditionUpdate(User user)

     或更復雜的直接調用目標對象的方法進行操作(如只有修改了某個數據才從緩存中清除,比如菜單數據的緩存,只有修改了關鍵數據時才清空菜單對應的權限數據) 

        @Caching(
                evict = {
                        @CacheEvict(value = "user", key = "#user.id", condition = "#root.target.canEvict(#root.caches[0], #user.id, #user.username)", beforeInvocation = true)
                }
        )
        public void conditionUpdate(User user)
        public boolean canEvict(Cache userCache, Long id, String username) {
            User cacheUser = userCache.get(id, User.class);
            if (cacheUser == null) {
                return false;
            }
            return !cacheUser.getUsername().equals(username);
        }

    如上方式唯一不太好的就是緩存條件判斷方法也需要暴露出去;而且緩存代碼和業務代碼混合在一起,不優雅;因此把canEvict方法移到一個Helper靜態類中就可以解決這個問題了:

    @CacheEvict(value = "user", key = "#user.id", condition = "T(com.sishuok.spring.service.UserCacheHelper).canEvict(#root.caches[0], #user.id, #user.username)", beforeInvocation = true)
    public void conditionUpdate(User user)

     

    四、其實對于:id--->user;username---->user;email--->user;更好的方式可能是:id--->user;username--->id;email--->id;保證user只存一份;如:

    @CachePut(value="cacheName", key="#user.username", cacheValue="#user.username")
    public void save(User user)
    @Cacheable(value="cacheName", ley="#user.username", cacheValue="#caches[0].get(#caches[0].get(#username).get())")
    public User findByUsername(String username)

     

    五、使用Spring3.1注解 緩存 模糊匹配Evict的問題 

    緩存都是key-value風格的,模糊匹配本來就不應該是Cache要做的;而是通過自己的緩存代碼實現;

     

    六、spring cache的缺陷:例如有一個緩存存放 list<User>,現在你執行了一個 update(user)的方法,你一定不希望清除整個緩存而想替換掉update的元素

    這個在現有的抽象上沒有很好的方案,可以考慮通過condition在之前的Helper方法中解決;當然不是很優雅。

     

    也就是說Spring Cache注解還不是很完美,我認為可以這樣設計:

    @Cacheable(cacheName = "緩存名稱",key="緩存key/SpEL", value="緩存值/SpEL/不填默認返回值",  beforeCondition="方法執行之前的條件/SpEL", afterCondition="方法執行后的條件/SpEL", afterCache="緩存之后執行的邏輯/SpEL")

    value也是一個SpEL,這樣可以定制要緩存的數據;afterCache定制自己的緩存成功后的其他邏輯。

     

     

    當然Spring Cache注解對于大多數場景夠用了,如果場景復雜還是考慮使用AOP吧;如果自己實現請考慮使用Spring Cache API進行緩存抽象。

     

    歡迎加入spring群134755960進行交流。

     

    示例請參考github:

    https://github.com/zhangkaitao/spring4-showcase/tree/master/spring-cache

    來自:http://my.oschina.net/u/1435252/blog/191368

     本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
     轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
     本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!
sesese色