Spring MVC整合Memcached基于注释的实践使用
本文并不介绍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 缓存介绍
本文出自 “学而思” 博客,请务必保留此出处http://linhongyu.blog.51cto.com/6373370/1677336