Spring Cache之Ehcache和Memcached

来源:互联网 发布:乐知英语官网下载 编辑:程序博客网 时间:2024/06/05 12:01

spring框架从3.1版本开始提供了缓存支持:在spring-context.jar里的org.springframework.cache包,以及spring-context-support.jar里的org.springframework.cache包;而且提供了基于ConcurrentHashMap、JCacheCache、EhCache、GuavaCache的实现。
这里我们先看下基于EhCache的使用,然后考虑集成Memcached;版本:spring3.2和spring4,EhCache2.7,spyMemcached2.8;
内容还涉及HashMap、LinkedHashMap、synchronizedMap、ConcurrentHashMap、ReentrantLock…… 
参考资料:spring framework 4.0.x referencewang chaoqun

一、EhCache配置

1. 添加相关jar,添加ehcache.xml

1234567891011
<?xml version="1.0" encoding="UTF-8"?><ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    xsi:noNamespaceSchemaLocation="ehcache.xsd" updateCheck="true"  monitoring="autodetect" dynamicConfig="true">  <diskStore path="java.io.tmpdir" />  <defaultCache maxElementsInMemory="10000" maxElementsOnDisk="100000"      eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true"      diskPersistent="false" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU" />  <cache name="notice_cache" maxElementsInMemory="10000" maxElementsOnDisk="100000"      eternal="true" overflowToDisk="true" diskSpoolBufferSizeMB="50" /></ehcache>

属性意义基本明确,这里我配置了一个名称是notice_cache的cache节点,其他的可以按此添加。

2. spring-cache.xml

配置cacheManager和cacheManagerFactory,并将ehcache.xml配入即可

123456789101112131415
<?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:cache="http://www.springframework.org/schema/cache"  xmlns:p="http://www.springframework.org/schema/p"  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd      http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache-3.2.xsd">  <cache:annotation-driven cache-manager="cacheManager" />    <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager"      p:cacheManager-ref="cacheManagerFactory" />        <bean id="cacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"      p:configLocation="classpath:ehcache.xml" p:shared="false" /></beans>

3. @Cacheable\@CacheEvict\@CachePut\@Caching…

注解加在相应方法上,支持spel,详细参见文档查阅spring 4.0.x reference

123456789
@Cacheable(value = "notice_cache", key = "'notice'+#id")public Notice get(String id) {  return noticeDao.get(id);}@Cacheable(value = "notice_cache")public List<Notice> search(String keywords, Date fromTime, Date toTime, Integer[] status, Page page) {  //...}

至此配置完了,run一下,报错:没有序列化,将vo实现Serializable接口

1
public class Notice implements Serializable {

标签:spring

这样ehcache集成完了,get方法对同一条记录只从数据库查询一次,cache是成功的,但search方法却一直读库,这里没有设置cache的key,设置的话如果是固定的,那么每次结果集都一样,不会更新;文档说不设key,将使用默认key生成器DefaultKeyGenerator:

123456789101112131415161718
public class DefaultKeyGenerator implements KeyGenerator {  public static final int NO_PARAM_KEY = 0;  public static final int NULL_PARAM_KEY = 53;  public Object generate(Object target, Method method, Object... params) {      if (params.length == 1) {          return (params[0] == null ? NULL_PARAM_KEY : params[0]);      }      if (params.length == 0) {          return NO_PARAM_KEY;      }      int hashCode = 17;      for (Object object : params) {          hashCode = 31 * hashCode + (object == null ? NULL_PARAM_KEY : object.hashCode());      }      return Integer.valueOf(hashCode);  }}

问题就在于object.hashCode(),看方法的参数string没问题,date没问题,Integer数组使用的就是Object类的hashCode是个内存地址,每次执行都变,要改用Arrays.hashCode(array)才不会变;
当然,分页类page也要重写hashCode;顺便说下,apache的commons-lang.jar提供了EqualsBuilder、HashCodeBuilder、ToStringBuilder可用于重写各方法。还要注意:分页列表不仅要缓存list,还要缓存分页信息,这样到前端才会分页,否则是不知道这个list是多少页的,故方法的返回值(上面search方法只返回list是不行的)可采用类似org.springframework.data.domain.Page内部包含结果集

4. 自定义key生成器

解决上面问题:重写生成器(继承DefaultKeyGenerator,需要注意的是对于param是list,set,map取hashcode,其泛型类也要重写hashCode方法)并配置:

12
<cache:annotation-driven cache-manager="cacheManager" key-generator="keyGenerator"/><bean id="keyGenerator" class="......CustomKeyGenerator" />
1234567891011121314151617181920212223242526272829
public class CustomKeyGenerator extends DefaultKeyGenerator {  public Object generate(Object target, Method method, Object... params) {      StringBuffer buffer = new StringBuffer();      buffer.append(target.getClass().getName()).append(".");      buffer.append(method.getName()).append(".");      if (params.length > 0) {          for (Object each : params) {              if (each != null) {                  if (each instanceof Boolean || each instanceof Character || each instanceof Void                          || each instanceof Short || each instanceof Byte || each instanceof Double                          || each instanceof Float || each instanceof Integer || each instanceof Long) {                      buffer.append(each);                  } else if (each instanceof Object[]) {                      buffer.append(Arrays.hashCode((Object[]) each)); // 后面会说到可替换Arrays.deepHashCode                  } else if (each instanceof HttpServletRequest || each instanceof HttpServletResponse) {                      continue;                  } else {                      buffer.append(each.hashCode()); // list,map,set其内的元素类型一直才好                  }              } else {                  buffer.append(NO_PARAM_KEY);              }          }      } else {          buffer.append(NO_PARAM_KEY);      }      return buffer.toString().hashCode();  }}

5. 添加、更新、删除

显然@Cacheable是缓存,@CacheEvict是擦除,@CachePut相当于擦除后再缓存,对于key是确定的很好,比如getById(id),update(obj),其key可以用id,obj.id;update时也可以用@CachePut,要注意update方法要返回更新后的obj,void不行。

问题又出现了:不明确的key如何更新?例如search,当新添加一条记录后,就不能使用@CacheEvict(value=“notice_cache”, key=“?”),因为取不到key,也不能模糊匹配;这种情况下只能使用@CacheEvict(value = “notice_cache”, allEntries = true),将notice_cache所有的缓存擦除,多少有点粗糙,而memcached甚至没有某个cache的removeAll,这就要自己写个MemcachedCache

通常注解使用在service方法上,还有一个注意事项:因其使用aop的动态代理,对于内部调用无效,例如publish方法没加注解,内部调用update方法(加了@CachePut)更新状态值,但cache不会更新;controller方法(不加注解)调用service方法(加了注解)是可以;当然controller方法也可以加,需要单独处理,因为参数若有request、response之类,每次请求都变,就要在keyGenerator里做过滤了。

二、集成Memcached

背景:现在的项目使用memcached做缓存,基本上是编码式的,在需要的时候,生成key,将value转为json再set到缓存,因此打算使用注解式更优雅的处理,就需要实现spring cache的相关接口和自定义一些方法

1. spring集成Memcached,使用spyMemcached

1234567891011121314151617
<bean id="memcachedClient" class="net.spy.memcached.spring.MemcachedClientFactoryBean">  <property name="servers" value="${memcached.servers}" />  <property name="protocol" value="BINARY" />  <property name="transcoder">      <bean class="net.spy.memcached.transcoders.SerializingTranscoder">          <property name="compressionThreshold" value="${memcached.transcoder.compressionThreshold}" />      </bean>  </property>  <property name="opTimeout" value="${memcached.opTimeout}" />  <property name="timeoutExceptionThreshold" value="1998" />    <property name="hashAlg">            <value type="net.spy.memcached.DefaultHashAlgorithm">${memcached.hashAlg}</value>  </property>  <property name="locatorType" value="${memcached.locatorType}" />  <property name="failureMode" value="${memcached.failureMode}" />  <property name="useNagleAlgorithm" value="${memcached.useNagleAlgorithm}" /></bean>

properties:

12345678910
####memcached configmemcached.servers=ip:portmemcached.protocol=BINARYmemcached.transcoder.compressionThreshold=1024memcached.opTimeout=1000memcached.timeoutExceptionThreshold=1998memcached.hashAlg=KETAMA_HASHmemcached.locatorType=CONSISTENTmemcached.failureMode=Redistributememcached.useNagleAlgorithm=false

标签:技术

2. 实现MemcachedCacheManager和MemcachedCache

参考ehcache的源码(org.springframework.cache.ehcache包里):EhCacheCache和EhCacheCacheManager,manager用来获取cache,重写了getCache和loadCaches方法,这样配置在ehcache.xml里的cache name都会实例化成每个EhCacheCache,当执行到@Cacheable的方法上,就会调用getCache(name)获取cache,再根据key取得value;

MemcachedCacheManager

1234567891011121314151617181920212223242526272829303132
public class MemcachedCacheManager extends AbstractCacheManager {  // 注入memcachedClient(后面会有配置)  private MemcachedClient client;  public MemcachedCacheManager() {}  public MemcachedCacheManager(MemcachedClient client) {      this.client = client;  }  public void setClient(MemcachedClient client) {      this.client = client;  }    // AbstractCacheManager.afterPropertiesSet不允许loadCaches返回空,所以覆盖掉  public void afterPropertiesSet() {  }  protected Collection<? extends Cache> loadCaches() {      return null;  }  // 根据名称获取cache,对应注解里的value如notice_cache,没有就创建并加入cache管理  public Cache getCache(String name) {      Cache cache = super.getCache(name);      if (cache == null) {          cache = new MemcachedCache(name, client);          super.addCache(cache);      }      return cache;  }}

这样应用启动时实例化manager,在执行加缓存注解的的方法时,会调用getCache(获取或新建cache),根据缓存的key从cache中取值(没有就读库,然后将结果加入cache,下次相同的key就能取到缓存的值了)
要写MemcachedCache实现org.springframework.cache.Cache接口,先来分析EhCacheCache

1234567891011121314151617181920212223242526272829303132333435363738394041
public class EhCacheCache implements Cache {  // 使用Ehcache的cache,来做get,put,evict...,集成memcached就要使用memcachedClient  private final Ehcache cache;  /**  * Create an {@link EhCacheCache} instance.  * @param ehcache backing Ehcache instance  */  public EhCacheCache(Ehcache ehcache) {      Assert.notNull(ehcache, "Ehcache must not be null");      Status status = ehcache.getStatus();      Assert.isTrue(Status.STATUS_ALIVE.equals(status),              "An 'alive' Ehcache is required - current cache is " + status.toString());      this.cache = ehcache;  }  // 也就是ehcache.xml里配置的  public String getName() {      return this.cache.getName();  }  // 底层使用的cache,要改用memcachedClient  public Ehcache getNativeCache() {      return this.cache;  }  // 从cache取值,改用memcachedClient取值  public ValueWrapper get(Object key) {      Element element = this.cache.get(key);      return (element != null ? new SimpleValueWrapper(element.getObjectValue()) : null);  }  // 改用memcachedClient存值  public void put(Object key, Object value) {      this.cache.put(new Element(key, value));  }  // 擦除delete  public void evict(Object key) {      this.cache.remove(key);  }  // 清空cache,这个是例如@CacheEvict(value = "notice_cache", allEntries = true)时调用的  public void clear() {      this.cache.removeAll();  }}

好了,来写memcachedCache,问题来了
1.clear方法,spy的client没有removeAll,clear之类的方法,有个flush是全部清空,服务器N多个cache都会擦掉
2.@CacheEvict(value = “notice_cache”, allEntries = true)就是用的clear,“添加个notice都要清掉其他非notice_cache缓存”就很可怕,能不能根据cache名称清除呢?
3.上面两个实际是一个问题,memcached是key-value存储,所以要对key进行分组,采用一个集合保存key,然后将实际的key-value存入;如果想模糊匹配也是可行的,需要在此基础上做修改:key就得用字符串而不是字符串的hashCode了,或者自定义注解

常用的集合数据类型如list,map,set它也支持,考虑到key的字符限制和单个value不超过1MB,使用一个set存储一个cache里所有的key能达到2万以上(看key的字节数),使用压缩存储的更多,同时使用LRU(如LinkedHashMap,将过期的或长期不用的移除),基本满足使用
标签:memcached

MemcachedCache

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
public class MemcachedCache implements Cache {  private static final String PRESENT = new String();  // 单个cache存储的key最大数量  private static final int maxElements = 10000;  // 默认过期时间10天  private static final int expire = 10 * 24 * 60 * 60;  private String name;  private MemcachedClient client;  // 存储key的集合,使用LinkedHashMap实现  private KeySet keys;  public MemcachedCache() {}  public MemcachedCache(String name, MemcachedClient client) {      this.name = name;      this.client = client;      this.keys = new KeySet(maxElements);  }  public String getName() {      return this.name;  }  public Object getNativeCache() {      return this.client;  }  // ckey是key+cacheName作为前缀,也是最终存入缓存的key  public ValueWrapper get(Object key) {      String ckey = toStringWithCacheName(key);      if (keys.containsKey(ckey)) {          Object value = client.get(ckey);          return value != null ? new SimpleValueWrapper(value) : null;      } else {          return null;      }  }  // 将ckey加入key集合并将ckey-value存入缓存  public void put(Object key, Object value) {      String ckey = toStringWithCacheName(key);      keys.put(ckey, PRESENT);      client.set(ckey, expire, value);  }  // 从keys集合清除ckey,并从缓存清除  public void evict(Object key) {      String ckey = toStringWithCacheName(key);      keys.remove(ckey);      client.delete(ckey);  }  private String toStringWithCacheName(Object obj) {      return name + "." + String.valueOf(obj);  }  // 遍历清除  public void clear() {      for (String ckey : keys.keySet()) {          client.delete(ckey);      }      keys.clear();  }  public MemcachedClient getClient() {      return this.client;  }  public void setClient(MemcachedClient client) {      this.client = client;  }    public KeySet getKeys() {      return this.keys;  }}

这里keys也可以使用cacheName作为key存入缓存,就需要在put,evict,clear方法里使用client.replace(name, expire, keys);保持更新

KeySet继承LinkedHashMap,为了使用removeEldestEntry,满了移除最旧元素,保持initSize:

1234567891011121314151617181920
/** * 用于存储keys,容量到达上限移除最旧的,缓存也移除 */class KeySet extends LinkedHashMap<String, String> {  private static final long serialVersionUID = 1L;  private int maxSize;  public KeySet(int initSize) {      super(initSize, 0.75F, true);      this.maxSize = initSize;  }  public boolean removeEldestEntry(Map.Entry<String, String> eldest) {      boolean overflow = size() > this.maxSize;      if (overflow) {          MemcachedCache.this.client.delete(eldest.getKey());      }      return overflow;  }}

3. 线程安全

因为要存储keys,所以考虑使用哪种集合:HashSet\HashMap都不是线程安全的,例如Java HashMap的死循环;
安全的如Collections.synchronizedMap和ConcurrentHashMap(不允许value为null);
两者的区别是锁不同:synchronizedMap使用对象锁,相当于在方法上声明synchronized;ConcurrentHashMap比较复杂,在segment上加锁,将范围控制的很小,因而并发性能就高;
这里使用LinkedHashMap,ConcurrentHashMap不好包装,synchronizedMap效率低,不如加个ReentrantLock,或者使用读写锁ReentrantReadWriteLock(但这篇文章介绍了读写锁可能存在问题:小心LinkedHashMap的get()方法):

123456
lock.lock();try {  client.set(...);} finally {  lock.unlock();}

下面是HashMap占用cpu 100% bug的代码:

1234567891011121314151617181920212223
public class MapTest {  public static void main(String[] args) throws InterruptedException {      Map<String, String> temp = new HashMap<>(2);      final Map<String, String> map = temp;      //     final Map<String, String> map = new LinkedHashMap<>(temp);      //     final Map<String, String> map = new ConcurrentHashMap<>(temp);      //     final Map<String, String> map = Collections.synchronizedMap(temp);      Thread t = new Thread(new Runnable() {          public void run() {              for (int i = 0; i < 10000; i++) {                  new Thread(new Runnable() {                      public void run() {                          map.put(UUID.randomUUID().toString(), "");                      }                  }).start();              }          }      });      t.start();      t.join();  }}

4. Spring 4.0.x Cache

以上3.2.x使用正常,4.0版本改动了key生成器,源码也很简单:SimpleKeyGenerator和SimpleKey,toString方法将参数转为字符串,嫌长就改用hashCode

1234567891011121314151617181920212223242526272829303132333435363738
public class SimpleKeyGenerator implements KeyGenerator {  @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);  }}public final class SimpleKey implements Serializable {  public static final SimpleKey EMPTY = new SimpleKey();  private final Object[] params;  /**  * Create a new {@link SimpleKey} instance.  * @param elements the elements of the key  */  public SimpleKey(Object... elements) {      Assert.notNull(elements, "Elements must not be null");      this.params = new Object[elements.length];      System.arraycopy(elements, 0, this.params, 0, elements.length);  }  public boolean equals(Object obj) {      return (this == obj || (obj instanceof SimpleKey && Arrays.equals(this.params, ((SimpleKey) obj).params)));  }  public int hashCode() {      return Arrays.hashCode(this.params);  }  public String toString() {      return "SimpleKey [" + StringUtils.arrayToCommaDelimitedString(this.params) + "]";  }}

事实上,对于复杂的,类似Object数组(下面会有),那么无论是上面自定义keyGenerator还是spring4.0的都会有问题(hashCode和toString,不一致就取不到cache,会每次都读库),测试简单数组如下:

123456789101112131415
Integer[] array = new Integer[] { 1, 2, 3 };System.out.println(array); // [Ljava.lang.Integer;@5f4fcc96System.out.println(ObjectUtils.nullSafeToString(array)); // {1, 2, 3} System.out.println(Arrays.toString(array)); // [1, 2, 3]System.out.println(Arrays.deepToString(array)); // [1, 2, 3]System.out.println(array.hashCode()); // 1599065238System.out.println(Arrays.hashCode(array)); // 30817 System.out.println(Arrays.deepHashCode(array)); // 30817System.out.println(Arrays.toString(array).hashCode()); // -412129978// StringUtils是spring的:org.springframework.util.StringUtilsSystem.out.println(StringUtils.arrayToCommaDelimitedString(array)); // 1,2,3System.out.println(StringUtils.arrayToCommaDelimitedString(array).hashCode()); //46612798

这个测试Arrays.hashCode(array)和SimpleKey.toString()多次运行是一致的,也就是自定义keyGenerator和SimpleKeyGenerator正确

复杂的Object[] mixed = new Object[] { 1, "11", array, list };

1234567891011121314
List<Integer> list = Lists.newArrayList(array);Object[] mixed = new Object[] { 1, "11", array, list };System.out.println(mixed); // [Ljava.lang.Object;@549f9afbSystem.out.println(ObjectUtils.nullSafeToString(mixed)); // {1, 11, [Ljava.lang.Integer;@5f4fcc96, [1, 2, 3]}System.out.println(Arrays.toString(mixed)); // [1, 11, [Ljava.lang.Integer;@5f4fcc96, [1, 2, 3]]System.out.println(Arrays.deepToString(mixed)); // [1, 11, [1, 2, 3], [1, 2, 3]]System.out.println(mixed.hashCode()); // 1419746043System.out.println(Arrays.hashCode(mixed)); // -1966094197System.out.println(Arrays.deepHashCode(mixed)); // 3446304System.out.println(Arrays.toString(mixed).hashCode()); // -691776533System.out.println(StringUtils.arrayToCommaDelimitedString(mixed)); // 1,11,[Ljava.lang.Integer;@5f4fcc96,[1, 2, 3]System.out.println(StringUtils.arrayToCommaDelimitedString(mixed).hashCode()); // 572153479

结论:多次运行发现只有Arrays.deepToString和Arrays.deepHashCode是一致的,也就是对每个元素,如果是数组再递归;同理,如果是集合list,set,map之类,最好使用泛型,类型一致,不要混合

三、总结

spring cache用好要注意很多:
1、搞清各注解意义和使用时机,逻辑正确,更新一致
2、缓存key的使用很重要,自定义key要考虑参数重写hashCode和toString
3、返回结果如分页结果集,不仅要有list还要有page
4、可虑清楚并测试加了Cacheable确实生效?
5、效益最大化:使用多注解多缓存的情景,一次方法执行缓存多个信息(要更新时也得多个更新,才能保持一致)

0 0