在上篇《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) {
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。
- 当输入参数为基本数据类型+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();
}
User.java
public class User implements Serializable, ListKeyParam{
@Override
public Object getKey() {
return id+":"+userName;
}
}
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;
}
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("/");
}
}
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加密,可缩短节省内存空间。