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<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);

    }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

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();
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

User.java


public class User implements Serializable, ListKeyParam{

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

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

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<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;
    }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值