手把手教你做一个缓存工具

日常开发中,某些数据接口即使优化到极致,都难免还会存在计算量巨大导致响应过慢,多数情况下会选择单独做一个统计表用于存放这些处理后的数据便于加快于读取,或者接入redis/memcache存计算数据,可以说单次响应本身是接受较慢一些的,实时性也并非特别高时,则可以考虑引入缓存机制,提升使用体验。说到用缓存,那就会有人提出用redis,但是项目组认为项目紧急,不希望浪费时间到新的工具研究上,或虽然熟悉,但维护工作有成本,为了有限的效果付出太多不划算。那么怎么办,莫得搞了,只能手把手给项目做一个缓存工具了!吃掉JVM!也和spring cache很类似的。

这样的缓存机制,无非就是key-value模型的体现,所以首先想到了map。

Map cache =newHashMap<>();

一个缓存工具就完成了,快吧。怎么用的话,就类似这样嘛:

@GetMapping("/{id}")

publicObjectget(@PathVariable("id")Stringid) {

if(cache.containKey(id)) {

returncache.get(id);

}

// 调取服务获取对象

Objectobj = service.get(id);

// 塞进缓存中

cache.put(id, obj);

returnobj;

}

挺好用的,既方便,效果又达成。

但是一想到是JVM的内存,那么对象是存放在堆中的,一旦发生GC,数据被清掉了呢,会不会执行下面这个操作的时候,我containKey方法判断它的确存在,但是到了return的时候,应该返回的对象没有了呢。

if(cache.containKey(id)) {

returncache.get(id);

}

这的确是个问题,需要防止它发生。有办法,换个思路写上面的代码:

Object obj = cache.get(id);

if(obj !=null) {

returnobj;

}

这样写总可以了吧,对象真的存在的时候我才给直接返回,不然还是老老实实执行查询对象的方法。

好是挺好,但是总不至于每次一个类,我就得它new一个Map吧,那得多费Map,而且Map可以存在很多的对象在里面,只要它的Key不重复。

考虑下使用Spring的组件来处理,其实就是抽Map成类,当然可以将Map放到一个类中做静态属性字段。Map的重复利用算是解决了。但是缓存的数据不是一直都不变的,那还需要给它来一个定时器,刷走缓存数据。

@Component

publicCacheextendsHashMap {

@Scheduled(cron="0 0/5 * * * ?")

publicvoidflushCache() {

clear();

}

}

其实这样做还是不够灵活,应该更能定制化地刷走缓存,有些数据是5分钟才变化,但有些数据一天都不变呢。这样的话,可以考虑用Java的定时器,对上面进行优化,定时删除指定的数据。

缓存的地方有了,定时刷新有了,但是仍然不好用,因为每个方法我都需要写代码去判断是否存在缓存。为了解决这个问题,很自然地想到了加注解,AOP做切面,交给切面去处理,很快,就做出来了。

增加一个注解@CachePut:

@Target({ ElementType.METHOD, ElementType.TYPE })

@Retention(RetentionPolicy.RUNTIME)

@Documented

public@interfaceCachePut {

Stringkey()default"";

}

再把切面实现下:

@Aspect

@Component

publicclassCachePutAspect{

@Autowired

privateCache cache;

@Pointcut("@annotation(com.lin.cache.Cache)")

publicvoidcachePointCut(){

}

@SuppressWarnings("rawtypes")

@Around("cachePointCut()")

publicObjectpointCut(ProceedingJoinPoint pjp)throwsException{

Object result =null;

String methodName = pjp.getSignature().getName();

Class classTarget = pjp.getTarget().getClass();

Class[] par = ((MethodSignature) pjp.getSignature()).getParameterTypes();

Method method = classTarget.getMethod(methodName, par);

CachePut cachePut = method.getAnnotation(CachePut.class);

if(cachePut !=null) {

result = cache.get(cacheImport.key());

// 避免获取结果时遇上clear操作

if(result !=null) {

// 直接返回结果,不执行方法

returnresult;

}

try{

result = pjp.proceed();

// 将结果缓存

cacheMap.put(key, result);

}catch(Throwable throwable) {

thrownewException("方法错误");

}

}

returnresult;

}

}

使用的时候就是这样的了。

@CachePut(key ="getId")

@GetMapping("/{id}")

publicObjectget(@PathVariable("id")Stringid) {

// 调取服务获取对象

Objectobj = service.get(id);

returnobj;

}

好像还是不对劲的,因为key都是固定是"getId",岂不前前后后只有一个key,不断被覆盖结果只有一个,但肯定不行,那怎么办。想起来之前用redis做缓存的时候,用的spring的缓存@CachePut(key = "info + #id"),从方法的入参那里拿到唯一的标识,将缓存结果区分开来,这里网上的资料比较少,啃源码一下子没看明白,怎么想都觉得这个操作很简单。直到偶尔翻翻,找到有人提供了一个可行的例子,才得以让这个缓存工具得到升华。

@Aspect

@Component

publicclassCachePutAspect {

@Autowired

privateCache cache;

@Pointcut("@annotation(com.lin.cache.Cache)")

publicvoidcachePointCut() {

}

@SuppressWarnings("rawtypes")

@Around("cachePointCut()")

publicObjectpointCut(ProceedingJoinPoint pjp) throws Exception {

Objectresult =null;

StringmethodName = pjp.getSignature().getName();

Class classTarget = pjp.getTarget().getClass();

Class[] par = ((MethodSignature) pjp.getSignature()).getParameterTypes();

Method method = classTarget.getMethod(methodName, par);

CachePut cachePut = method.getAnnotation(CachePut.class);

if(cachePut !=null) {

Stringkey = generateKeyBySpEL(cacheImport.key(), pjp);

result = cacheMap.get(key);

// 避免获取结果时遇上clear操作

if(result !=null) {

returnresult;

}

try{

result = pjp.proceed();

// 将结果缓存

cacheMap.put(key, result);

}catch(Throwable throwable) {

thrownewException("方法错误");

}

}

returnresult;

}

// 使用SpringEL,将入参数据和表达式绑定起来,得到Key

publicStringgenerateKeyBySpEL(Stringkey, ProceedingJoinPoint pjp) {

Expression expression = parserSpel.parseExpression(key);

EvaluationContext context =newStandardEvaluationContext();

MethodSignature methodSignature = (MethodSignature) pjp.getSignature();

Object[] args = pjp.getArgs();

String[] paramNames = parameterNameDiscoverer

.getParameterNames(methodSignature.getMethod());

for(int i =0; i < args.length ; i++) {

context.setVariable(paramNames[i], args[i]);

}

returnexpression.getValue(context).toString();

}

}

现在就可以这样用灵活的注解了:

@CachePut(key = "'info-'+ #id)

差不多了完成这个缓存工具了,当然除了将结果放进缓存的操作用注解处理,把缓存移除的操作也可以用注解完成。这里就不实现了。

enn。。好像和Spring Cache的比较像,将就吧。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,869评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,716评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,223评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,047评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,089评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,839评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,516评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,410评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,920评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,052评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,179评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,868评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,522评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,070评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,186评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,487评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,162评论 2 356