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

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

 

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

 

Map<String, Object> cache = new HashMap<>();

 

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

@GetMapping("/{id}")
public Object get(@PathVariable("id") String id) {
  if (cache.containKey(id)) {
    return cache.get(id);
  }
  // 调取服务获取对象
  Object obj = service.get(id);
  // 塞进缓存中
  cache.put(id, obj);
  return obj;
}

 

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

 

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

 

if (cache.containKey(id)) {
  return cache.get(id);
}

 

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

Object obj = cache.get(id);
if (obj != null) {
  return obj;
}

 

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

 

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

 

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

 

@Component
public Cache<String, Object> extends HashMap<String, Object> {
  
  @Scheduled(cron="0 0/5 * * * ?")
  public void flushCache() {
    clear();
  }
    
}

 

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

 

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

 

增加一个注解@CachePut:

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CachePut {

  String key() default "";

}

 

再把切面实现下:

@Aspect
@Component
public class CachePutAspect {

  @Autowired
  private Cache cache;

  @Pointcut("@annotation(com.lin.cache.Cache)")
  public void cachePointCut() {
  }

  @SuppressWarnings("rawtypes")
  @Around("cachePointCut()")
  public Object pointCut(ProceedingJoinPoint pjp) throws Exception {
    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) {
        // 直接返回结果,不执行方法
        return result;
      }
      try {
        result = pjp.proceed();
        // 将结果缓存
        cacheMap.put(key, result);
      } catch (Throwable throwable) {
        throw new Exception("方法错误");
      }
    }
    return result;
  }

}

 

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

 

@CachePut(key = "getId")
@GetMapping("/{id}")
public Object get(@PathVariable("id") String id) {
  // 调取服务获取对象
  Object obj = service.get(id);
  return obj;
}

 

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

 

@Aspect
@Component
public class CachePutAspect {

  @Autowired
  private Cache cache;

  @Pointcut("@annotation(com.lin.cache.Cache)")
  public void cachePointCut() {
  }

  @SuppressWarnings("rawtypes")
  @Around("cachePointCut()")
  public Object pointCut(ProceedingJoinPoint pjp) throws Exception {
    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) {
      String key = generateKeyBySpEL(cacheImport.key(), pjp);
      result = cacheMap.get(key);
      // 避免获取结果时遇上clear操作
      if (result != null) {
        return result;
      }
      try {
        result = pjp.proceed();
        // 将结果缓存
        cacheMap.put(key, result);
      } catch (Throwable throwable) {
        throw new Exception("方法错误");
      }
    }
    return result;
  }

  // 使用SpringEL,将入参数据和表达式绑定起来,得到Key
  public String generateKeyBySpEL(String key, ProceedingJoinPoint pjp) {
    Expression expression = parserSpel.parseExpression(key);
    EvaluationContext context = new StandardEvaluationContext();
    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]);
    }
    return expression.getValue(context).toString();
  }
    
}

 

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

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

 

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

 

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值