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 緩存介紹



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