在上篇《spring + ehcache + redis两级缓存实战篇(1)》中,最后遗留了两个问题给大家思考:
第一个是访问10次本地EhCache 强制访问一次Redis 使得激活数据或更新数据,这样会不会更好一些呢?
第二个是使用spring @Cacheable注解缓存方法时,将list参数的地址作为key存储,是否会有问题?
针对上面两个问题,我们主要来探讨如何解决。JUST DO IT! Go!
在上篇TODO处我们标记了问题可能要改进的地方。代码是最好的语言,就不做过多解释了。主要是Element是对value的一个封装,附加了一些状态信息。
EhRedisCache.Java
private int activeCount = 10;//默认十次 @Override public ValueWrapper get(Object key) { Element value = ehCache.get(key); LOG.info("Cache L1 (ehcache) :{}={}",key,value); if (value!=null) { //TODO 访问10次EhCache 强制访问一次redis 使得数据不失效 if(value.getHitCount() < activeCount){ return (value != null ? new SimpleValueWrapper(value.getObjectValue()) : null); }else{ value.resetAccessStatistics(); } } final String keyStr = key.toString(); Object objectValue = redisTemplate.execute(new RedisCallback<Object>() { public Object doInRedis(RedisConnection connection) throws DataAccessException { byte[] key = keyStr.getBytes(); byte[] value = connection.get(key); if (value == null) { return null; } //每次获得延迟时间 if (liveTime > 0) { connection.expire(key, liveTime); } return toObject(value); } },true); ehCache.put(new Element(key, objectValue));//取出来之后缓存到本地 LOG.info("Cache L2 (redis) :{}={}",key,objectValue); return (objectValue != null ? new SimpleValueWrapper(objectValue) : null); } 123456789101112131415161718192021222324252627282930313233343536 123456789101112131415161718192021222324252627282930313233343536getHitCount:缓存命中次数统计,resetAccessStatistics:统计次数重置为0。 前面讲过缓存的监控和Debug是困难的,难以排查是缓存的一个弱点。
思考:我们如今是两级缓存,是否也可以将Element做下扩展,使得用户可知缓存对象是来自L1或L2?L1和 L2中存活时间,缓存队列中KV的情况?被L1或L2的访问次数等等。最终做成一个可视化的缓存对象监控呢?
在上篇FIXME处我们标记了问题可能要改进的地方。通过spring注解的方式,在需要缓存的方法上注解@Cacheable即可,构建缓存时默认是根据接口输入参数作为key,返回值作为value。
当输入参数为基本数据类型+string,可不做特殊处理。当输入参数为Serializable Bean,可使用spEL(spring el)表达式获得对象中的成员变量。spEL表达式使用详见:扩展阅读。当输入参数为List类型时,将list参数的地址作为key存储,在分布式的情况下,相同的对象集合在不同节点上生成地址却不一样,会造成缓存命中率降低。由于EL表达式无法遍历List中T进行每个对象的处理,那么,我们如何对List中T的对象进行遍历处理?反射动态获取泛型?no,反射获取效率问题。 直接对T进行序列化?key无法灵活定义。 So,此处我新建一个ListKeyParam接口,通过实现该接口getKey来自定义key。
ListKeyParam.java
public interface ListKeyParam { Object getKey(); } 123456 123456User.java
public class User implements Serializable, ListKeyParam{ @Override public Object getKey() { return id+":"+userName; } //other codes… } 12345678910 12345678910BusinessCacheKeyGenerator.java
public Object generate(Object target, Method method, Object... params) { if (Void.class == method.getReturnType()) { LOG.error("无返回值的方法不可缓存 {}", method.getName()); return null; } Object result = null; StringBuilder sb = new StringBuilder(); sb.append(method.getDeclaringClass().getSimpleName()); sb.append("/").append(method.getName()).append("/"); if (params.length > 0) { for (Object param : params) { if (param == null) { sb.append(NULL_PARAM_KEY).append("/"); continue; } if (String.class.isAssignableFrom(param.getClass()) || Number.class.isAssignableFrom(param.getClass())) { sb.append(param).append("/"); continue; } if (Date.class.isAssignableFrom(param.getClass())) { Date date = (Date) param; sb.append(date.getTime()).append("/"); continue; } //list.toString是可行不妥的,不同机器,相同集合的地址不一样,降低缓存命中 // if (List.class.isAssignableFrom(param.getClass())) { // sb.append(param.toString()).append("/"); // continue; // } //TODO List参数类型处理 if(List.class.isAssignableFrom(param.getClass())){ @SuppressWarnings("unchecked") List<Object> objs = (List<Object>) param; for (Object object : objs) { if (object instanceof ListKeyParam) { sb.append(((ListKeyParam)object).getKey()); } } continue; } LOG.warn("缓存数据时存在不可识别的参数类型 {},method={}", param.getClass(), method); sb.append(param).append("/"); } } //TODO md5-->number 可将key缩短节省内存空间 result = super.generate(target, method,MD5Util.MD5(sb.toString())); LOG.debug("Cache key = {},method={}", result, method); return result; } 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152将key进行md5加密,可缩短节省内存空间。