spring + ehcache + redis两级缓存实战篇(2)

在上篇《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() {  
            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);

    }
 
  

getHitCount:缓存命中次数统计,resetAccessStatistics:统计次数重置为0。
前面讲过缓存的监控和Debug是困难的,难以排查是缓存的一个弱点。

思考:我们如今是两级缓存,是否也可以将Element做下扩展,使得用户可知缓存对象是来自L1或L2?L1和 L2中存活时间,缓存队列中KV的情况?被L1或L2的访问次数等等。最终做成一个可视化的缓存对象监控呢?


第二个问题

在上篇FIXME处我们标记了问题可能要改进的地方。通过spring注解的方式,在需要缓存的方法上注解@Cacheable即可,构建缓存时默认是根据接口输入参数作为key,返回值作为value。

  1. 当输入参数为基本数据类型+string,可不做特殊处理。
  2. 当输入参数为Serializable Bean,可使用spEL(spring el)表达式获得对象中的成员变量。spEL表达式使用详见:扩展阅读。
  3. 当输入参数为List类型时,将list参数的地址作为key存储,在分布式的情况下,相同的对象集合在不同节点上生成地址却不一样,会造成缓存命中率降低。

由于EL表达式无法遍历List中T进行每个对象的处理,那么,我们如何对List中T的对象进行遍历处理?反射动态获取泛型?no,反射获取效率问题。 直接对T进行序列化?key无法灵活定义。 So,此处我新建一个ListKeyParam接口,通过实现该接口getKey来自定义key。

ListKeyParam.java


public interface ListKeyParam {

    Object getKey();
}

User.java


public class User implements Serializable, ListKeyParam{

    @Override
    public Object getKey() {
        return id+":"+userName;
    }
   //other codes…
}

BusinessCacheKeyGenerator.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 objs = (List) 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;
    }
 
  

将key进行md5加密,可缩短节省内存空间。


扩展阅读

spEL表达式
http://docs.spring.io/spring/docs/4.2.x/spring-framework-reference/html/expressions.html


源码下载

http://download.csdn.net/detail/liaoyulin0609/9580598

你可能感兴趣的:(性能优化)