本文並不介紹memcached的安裝使用,也不長篇大論哪個緩存框架性能好。而是結合自己實際開發,來談談自己的使用。
一、配置文件application-cache.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:jee="http://www.springframework.org/schema/jee" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:cache="http://www.springframework.org/schema/cache" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.1.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.1.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache-3.1.xsd" default-lazy-init="true"> <!-- 緩存服務 --> <!-- 自定義key生成策略--> <!-- <cache:annotation-driven key-generator="stringKeyGenerator" /> <bean id="stringKeyGenerator" class="cn.roomy.supply.metadata.cache.StringKeyGenerator" /> --> <!--Spring 3.1 引入了基於註釋(annotation)的緩存(cache)技術 --> <cache:annotation-driven cache-manager="cacheManager" /> <bean id="cacheManager" class="cn.roomy.supply.metadata.cache.MemcacheCacheManager"> <!-- 是否事務環繞的,如果true,則如果事務回滾,緩存也回滾,默認false --> <property name="transactionAware" value="true" /> <property name="caches"> <set> <bean class="cn.roomy.supply.metadata.cache.MemcachedSpringCache"> <property name="name" value="r" /> <property name="memcachedClient" ref="memcachedClient4User" /> <!-- 1天 --> <property name="expiredDuration" value="86400" /> </bean> <bean class="cn.roomy.supply.metadata.cache.MemcachedSpringCache"> <property name="name" value="s" /> <property name="memcachedClient" ref="memcachedClient4User" /> <!-- 30天 --> <property name="expiredDuration" value="2592000" /> </bean> <bean class="cn.roomy.supply.metadata.cache.MemcachedSpringCache"> <property name="name" value="v" /> <property name="memcachedClient" ref="memcachedClient4User" /> <!-- 7天 --> <property name="expiredDuration" value="604800" /> </bean> </set> </property> </bean> <bean id="memcachedClient4User" class="net.spy.memcached.spring.MemcachedClientFactoryBean"> <property name="servers" value="${memcached.url}"/> <!-- 指定要使用的協議(BINARY,TEXT),默認是TEXT --> <property name="protocol" value="TEXT"/> <!-- 設置默認的轉碼器(默認以net.spy.memcached.transcoders.SerializingTranscoder) --> <property name="transcoder"> <bean class="net.spy.memcached.transcoders.SerializingTranscoder"> <property name="compressionThreshold" value="1024"/> </bean> </property> <!-- 以毫秒爲單位設置默認的操作超時時間 --> <property name="opTimeout" value="3000"/> <property name="timeoutExceptionThreshold" value="19980"/> <!-- 設置哈希算法 --> <property name="hashAlg"> <value type="net.spy.memcached.DefaultHashAlgorithm">KETAMA_HASH</value> </property> <!-- 設置定位器類型(ARRAY_MOD,CONSISTENT),默認是ARRAY_MOD --> <property name="locatorType" value="CONSISTENT"/> <!-- 設置故障模式(取消,重新分配,重試),默認是重新分配 --> <property name="failureMode" value="Redistribute"/> <!-- <property name="failureMode" value="Retry"/> --> <!-- 如果你想使用Nagle算法,設置爲true --> <property name="useNagleAlgorithm" value="false"/> </bean> </beans>
1、配置文件有一個關鍵的支持緩存的配置項:<cache:annotation-driven cache-manager="cacheManager" />,這個配置項缺省使用了一個名字叫 cacheManager 的緩存管理器,我們自定義了一個緩存管理器MemcacheCacheManager,它需要配置一個屬性 caches,即此緩存管理器管理的緩存集合。自定義MemcacheSpringCache配置了各個cache的有效時間。
2、引入第三方spy的客戶端MenCacheClientFactoryBean,設置其相關屬性。
二、MemcacheCacheManager與MemcacheSpringCache
/* * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.roomy.supply.metadata.cache; import java.util.Collection; import org.springframework.cache.Cache; import org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager; public class MemcacheCacheManager extends AbstractTransactionSupportingCacheManager { public MemcacheCacheManager() { } private Collection<? extends Cache> caches; /** * Specify the collection of Cache instances to use for this CacheManager. */ public void setCaches(Collection<? extends Cache> caches) { this.caches = caches; } @Override protected Collection<? extends Cache> loadCaches() { return this.caches; } }
這裏的重點在繼承了AbstractTransactionSupportingCacheManager抽象類,看類名大概也知道它的主要作用,對Spring事務的支持!即如果事務回滾了,Cache的數據也會移除掉。看其源碼可知,其繼承了AbstractCacheManager,而AbstractCacheManager實現了CacheManager接口。
package cn.roomy.supply.metadata.cache; import java.io.Serializable; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import net.spy.memcached.MemcachedClient; import org.apache.commons.lang3.time.StopWatch; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.cache.Cache; import org.springframework.cache.support.SimpleValueWrapper; import org.springframework.util.Assert; /** * 基於Spring Cache抽象體系的Memcached緩存實現 */ public class MemcachedSpringCache implements Cache, InitializingBean { private final static Logger logger = LoggerFactory.getLogger(MemcachedSpringCache.class); private String name; private MemcachedClient memcachedClient; /** * 默認最長緩存時間爲1小時 */ private static final int MAX_EXPIRED_DURATION = 60 * 60; /** Null值的最長緩存時間 */ private static final int NULL_VALUE_EXPIRATION = 60 * 60 * 24 * 7; /** 增量過期時間允許設置的最大值 */ private static final int DELTA_EXPIRATION_THRESHOLD = 60 * 60 * 24 * 30; /** * 緩存數據超時時間 */ private int expiredDuration = MAX_EXPIRED_DURATION; private static final Object NULL_HOLDER = new NullHolder(); private boolean allowNullValues = true; @Override public void afterPropertiesSet() throws Exception { Assert.notNull(memcachedClient, "memcachedClient must not be null!"); } @Override public String getName() { return this.name; } @Override public MemcachedClient getNativeCache() { return this.memcachedClient; } /** * 根據key得到一個ValueWrapper,然後調用其get方法獲取值 */ @Override public ValueWrapper get(Object key) { String cacheKey = getCacheKey(key); try { StopWatch sw = new StopWatch(); sw.start(); Object value = memcachedClient.get(cacheKey); sw.stop(); if (sw.getTime() > 50) { logger.info("讀取memcached用時{}, key={}", sw.getTime(), cacheKey); } return (value != null ? new SimpleValueWrapper(fromStoreValue(value)) : null); } catch (Exception e) { logger.error("讀取memcached緩存發生異常, key={}, server={}", cacheKey, memcachedClient.getNodeLocator().getPrimary(cacheKey).getSocketAddress(), e.getCause()); return null; } } /** * 根據key,和value的類型直接獲取value */ @SuppressWarnings("unchecked") @Override public <T> T get(Object key, Class<T> type) { ValueWrapper element = get(key); Object value = (element != null ? element.get() : null); if (value == null) return null; if (type != null && !type.isInstance(value)) { throw new IllegalStateException("緩存的值類型指定錯誤 [" + type.getName() + "]: " + value); } return (T) value; } /** * 存入到緩存的key,由緩存的區域+key對象值串接而成 * @param key key對象 * @return */ private String getCacheKey(Object key) { return this.name + key.toString(); } /** * 往緩存放數據 * 安全的Set方法,在3秒內返回結果, 否則取消操作. */ @Override public void put(Object key, Object value) { String cacheKey = getCacheKey(key); logger.debug("放入緩存的Key:{}, Value:{}, StoreValue:{}", cacheKey, value, toStoreValue(value)); int expiration = expiredDuration; if (value == null) { if (allowNullValues) { value = NULL_HOLDER; // 若允許緩存空值,則替換null爲佔坑對象;不允許直接緩存null,因爲無法序列化 } if (expiredDuration > NULL_VALUE_EXPIRATION) { expiration = NULL_VALUE_EXPIRATION; // 縮短空值的過期時間,最長緩存7天 } } else if (expiredDuration > DELTA_EXPIRATION_THRESHOLD) { expiration += (int) (System.currentTimeMillis() / 1000); // 修改爲UNIX時間戳類型的過期時間,使能夠設置超過30天的過期時間 // 注意:時間戳計算這裏有2038問題, // 2038-1-19 11:14:07 (GMT +8) 後,轉換成的 int 會溢出,導致出現負值 } Future<Boolean> future = memcachedClient.set(cacheKey, expiration, value); try { future.get(3, TimeUnit.SECONDS); } catch (Exception e) { future.cancel(false); logger.error("memcached寫入緩存發生異常, key={}, server={}", cacheKey, memcachedClient.getNodeLocator().getPrimary(cacheKey).getSocketAddress(), e); } } /** * 從緩存中移除key對應的緩存 * 安全的evict方法,在3秒內返回結果, 否則取消操作. */ @Override public void evict(Object key) { String cacheKey = getCacheKey(key); logger.debug("刪除緩存的Key:{}", cacheKey); Future<Boolean> future = memcachedClient.delete(cacheKey); try { future.get(3, TimeUnit.SECONDS); } catch (Exception e) { future.cancel(false); logger.error("memcached清除緩存出現異常, key={}, server={}", cacheKey, memcachedClient.getNodeLocator().getPrimary(cacheKey).getSocketAddress(), e); } } @Override public void clear() { try { memcachedClient.flush(); } catch (Exception e) { logger.error("memcached執行flush出現異常", e); } } protected Object fromStoreValue(Object storeValue) { if (this.allowNullValues && storeValue instanceof NullHolder) { return null; } return storeValue; } private static class NullHolder implements Serializable { private static final long serialVersionUID = -99681708140860560L; } protected Object toStoreValue(Object userValue) { if (this.allowNullValues && userValue == null) { return NULL_HOLDER; } return userValue; } public void setName(String name) { this.name = name; } public void setMemcachedClient(MemcachedClient memcachedClient) { this.memcachedClient = memcachedClient; } public void setExpiredDuration(int expiredDuration) { this.expiredDuration = expiredDuration; } public void setAllowNullValues(boolean allowNullValues) { this.allowNullValues = allowNullValues; } }
這類實現了Cache api接口,重寫了存入、取出、移除方法,並做了空值優化。所以之後的緩存操作將調用此自定義的方法。
三、自定義註釋
//把@Cacheable自定義成註釋@CacheableRelation @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Cacheable(value = "r", key="#distributId + '_' + #supplierId") public @interface CacheableRelation { }
//把@CacheEvict自定義成註釋@CacheEvictRelation @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @CacheEvict(value = "r", key="#relation.distributor.id + '_' + #relation.supplierId") public @interface CacheEvictRelation { }
//把@CachePut自定義成註釋@CachePutRelation @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @CachePut(value = "s", key="#relation.distributId + '_' + #relation.supplierId") public @interface CachePutRelation { }
關於@Cacheable、@CacheEvict、@CachePut等註釋的用法,這兒就不介紹了。重點在於value與key,這時你會發現,這個value在配置文件中有出現過,當時有設置其有效時間!而key的設定則可參照SpEL用法,當然其註釋的屬性中還有condition等條件判斷,篇幅有限,請自行查閱。
四、參考文章
開濤大神的博客文章:Spring Cache抽象詳解
IBM developerworks:註釋驅動的 Spring cache 緩存介紹