title: J2Cache二级缓存'没有自动更新' tags:
- J2Cache
- Spring
- 二级缓存
- Cache
- Cacheable categories: 工作日志 date: 2017-06-25 18:18:55
在给组内小伙伴们做完二级缓存的普及后,组内小伙伴使用二级缓存开发了公告功能。
功能如下:
复制代码
@Override @Cacheable(value = "notice") public ListgetNoticeInfo(TbNoticeSo notice) { return remindMapper.getNoticeInfo(notice); } 复制代码
问题出现了,发现在即使过去了3600s之后也不会自动清除缓存。
为了调试方便,将ehcache配置修改
复制代码
2s之后将会过期,发现了一个诡异的现象,开发在调试的时候通过调用缓存接口发现2s之后缓存会清除掉,而实际情况缺始终无法自动更新缓存,开发小伙伴百思不得其解。
那么问题出在哪里呢?
首先我们缓存的含义在上文中已经清除的描述了,目的很纯粹就是保存计算好的数据方便下次取用。
那么存储的地方必然会有一个key来进行映射。
那么key的默认值是什么呢?
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Cacheable { /** * Name of the caches in which the update takes place. *May be used to determine the target cache (or caches), matching the * qualifier value (or the bean name(s)) of (a) specific bean definition. */ String[] value(); /** * Spring Expression Language (SpEL) attribute for computing the key dynamically. *
Default is "", meaning all method parameters are considered as a key. */ String key() default ""; /** * Spring Expression Language (SpEL) attribute used for conditioning the method caching. *
Default is "", meaning the method is always cached. */ String condition() default ""; /** * Spring Expression Language (SpEL) attribute used to veto method caching. *
Unlike {
@link #condition()}, this expression is evaluated after the method * has been called and can therefore refer to the { @code result}. Default is "", * meaning that caching is never vetoed. * @since 3.2 */ String unless() default ""; }复制代码
很明显当key不填写的时候是将所有的参数作为key传输的。
具体代码如下
/** * Computes the key for the given caching operation. * @return generated key (null if none can be generated) */ protected Object generateKey() { if (StringUtils.hasText(this.operation.getKey())) { EvaluationContext evaluationContext = createEvaluationContext(ExpressionEvaluator.NO_RESULT); return evaluator.key(this.operation.getKey(), this.method, evaluationContext); } return keyGenerator.generate(this.target, this.method, this.args); } private EvaluationContext createEvaluationContext(Object result) { return evaluator.createEvaluationContext(this.caches, this.method, this.args, this.target, this.targetClass, result); } protected CollectiongetCaches() { return this.caches; } }复制代码
key表达式支持spel表达式 那么当用户不填写key的时候呀默认执行keyGenerator来生成
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); } }复制代码
此时很明显此刻的值只和参数有关(那么参数的hashCode将会影响到生成的key)
pojo还是需要复写hashcode方法 和equals方法
可想而知如果没有复写hashcode方法的话那么 即使是equals相同的参数也无法获得相同的hashcode值,那么对真实意义来说只有完全内存地址一样的对象才可以认为相等。
一旦有人不使用任何key的话并且是没有复写hashcode的pojo作为参数那么很有可能发生内存泄漏。
为何会发生内存泄露呢?我们已经设置了过期时间了啊?
我们作为开发人员考虑一下实现过期功能。考虑如下几种方案
protected byte[] getKeyName(Object key) { if (key instanceof Number) return ("I:" + key).getBytes(); else if (key instanceof String || key instanceof StringBuilder || key instanceof StringBuffer) return ("S:" + key).getBytes(); return ("O:" + key).getBytes(); }复制代码
全局设置一个线程监听所有需要过期的key,一旦过期就会删除复制代码
-
设置key的同时如果有过期时间那么同时起一个延迟线程将对应时间之后自动清除
-
不用自动过期,使用时候check是否需要过期?
对于redis来说其采用第一种方案,全局会自动删除过期的key 第二种方案几乎很难实现,可能造成线程数的暴涨其次就是可能出现取消过期等等。
ehcache使用第三种方案。具体调用堆栈如下当使用者调用get的时候会去check isExpired 如果过期了那么调用cacheExpiredListener同时将Key remove
那么我们来看看为何该缓存注解方法的参数。
/** * Created by PPbear on 2017/6/12. */ public class TbNoticeSo extends So { private static final long serialVersionUID = -3432391375970229933L; }复制代码
该实体并没有复写hashCode方法equals方法toString方法,那么查看父类复制代码
public class So implements Serializable, Pagable, Sortable { private static final long serialVersionUID = 436513842210828356L; @Override public String toString() { return "So{" + "currentPage=" + currentPage + ", pageSize=" + pageSize + ", pkId='" + pkId + '\'' + ", idOwnOrg='" + idOwnOrg + '\'' + ", sorts=" + sorts + '}'; } }复制代码
父类也并没有复写hashCode方法和equals方法。延伸阅读 [覆盖equals时总是覆盖hashCode][equals_hashCode] 复制代码
- 那么每次页面请求过来构造参数的自然是各不相同的,那么ehcache将会不停的缓存不同的key,直到oom或者达到缓存上限。 如上所述,如果确实如此为何开发修改了数据库之后拿到的还是之前的值?即并没有每次查询?那么解释读取顺序
- 我们在读取缓存顺序如下-> L1 -> L2 -> DB 一级缓存 此处为ehcache 二级缓存此处为redis 将对象存储到redis中涉及到序列化 那么我们将r该对象存储到redis中的key是啥呢? 先看一下结果
public void put(Object key, Object value) throws CacheException { if (key == null) return; if (value == null) evict(key); else { try { redisCacheProxy.hset(region2, getKeyName(key), SerializationUtils.serialize(value)); } catch (Exception e) { throw new CacheException(e); } } } protected byte[] getKeyName(Object key) { if (key instanceof Number) return ("I:" + key).getBytes(); else if (key instanceof String || key instanceof StringBuilder || key instanceof StringBuffer) return ("S:" + key).getBytes(); return ("O:" + key).getBytes(); }复制代码
明显此处直接使用toString那么对应参数的toString在父类中确实已经被复写了,所以此处的对象中只要toString相同那么都可以获取得到二级缓存
因此此处将会可以获得二级缓存的值。因此不会查询数据库。所以一旦查询过数据库的值将会无法自动更新。
当开发重复使用调试功能来调用缓存时由于实例对象没有发生更改,那么自然在调用到key的时候如方案3所述检测到过期发起过期通知 使得二级缓存也remove掉对应的key,
自然可以发生"调试时缓存就会自动过期"的灵异事件了。
解决方案较多。
我的方案如下
修改对应缓存注解如下
@Override @Cacheable(value = "notice", key = "'notice'") public ListgetNoticeInfo(TbNoticeSo notice) { return remindMapper.getNoticeInfo(notice); }复制代码
一切恢复正常了。