[关闭]
@yanglfyangl 2018-05-15T07:56:49.000000Z 字数 3488 阅读 665

关于缓存使用

注:以下情况使用缓存是错的

  • 在可能有错误的情况下使用缓存
  • 随意定义缓存过期时间
  • 函数体内有必须要调用的逻辑的情况下,使用了缓存。

铁律

  • 缓存使用时必须基于性能测试,如果没有性能问题的情况下,不要轻易使用接口缓存。(未来大家熟悉了逻辑后再说)
  • 缓存的过期时间必须经过“业务需求”层面的同意。随意设置过期时间是不允许的。
  • 对于大接口(代码行数比较多,逻辑比较复杂的),不能轻易使用缓存。将其有效拆分后合理加入缓存。

使用时需要注意事项示例

1. 可能出错的情况下使用缓存

  1. @Cached(name="topic-", key="#req.ID", expire = 3600)
  2. public Response<List<TopicsMetaData>> listTopicsByKeyWord(Request<Map<String, Object>> req) {
  3. Response<List<TopicsMetaData>> res = Response.create();
  4. res.setSid(req.getSid());
  5. try {
  6. List<TopicsMetaData> dto = topicServiceImpl.listTopicsByKeyWord(req.getData());
  7. res.success(dto);
  8. } catch (Exception e) {
  9. logger.error(e.toString());
  10. res.setErrorMsg("请联系管理员");
  11. res.setSuccess(false);
  12. }
  13. return res;
  14. }

这样使用缓存很容易产生这样的问题

  • 如果listTopicsByKeyWord出现异常,我们返回的错误值被缓存下来,而且1个小时内根本不会过期。这时候系统是处于严重问题之中的。

这种情况下如何解决呢?

  1. @Cached(name="topic-", key="#Key", expire = 600)
  2. private List<> listTopicsByKeyWord(String Key){
  3. return topicServiceImpl.listTopicsByKeyWord(req.getData());
  4. }
  5. public Response<List<TopicsMetaData>> listTopicsByKeyWord(Request<Map<String, Object>> req) {
  6. Response<List<TopicsMetaData>> res = Response.create();
  7. res.setSid(req.getSid());
  8. try {
  9. List<TopicsMetaData> dto = topicServiceImpl.listTopicsByKeyWord(req.getData());
  10. res.success(dto);
  11. } catch (Exception e) {
  12. logger.error(e.toString());
  13. res.setErrorMsg("请联系管理员");
  14. res.setSuccess(false);
  15. }
  16. return res;
  17. }

这样,确保你的缓存注解所在的函数能够尽可能的缓存可以缓存的数据。

2. 对于空值需要额外考虑(避免缓存穿透)

  1. @Cached(name="topic-", key="#Key", expire = 600)
  2. List<T> getList(String Key){
  3. return ServiceA.getList(key)
  4. }

上面的代码的一个问题是,如果系统因为一时的“小”问题,可能会导致未来的600秒缓存里的数据全是null值。
为了避免这个问题,可以将代码写成如下模式

模式A.

  1. @Cached(name="topic-", key="#Key", expire = 600cacheNullValue = false)
  2. @CachePenetrationProtect
  3. List<T> getList(String Key){
  4. return ServiceA.getList(key)
  5. }

@CachePenetrationProtect 注解保证当缓存未命中的时候,一个JVM里面只有一个线程去执行方法,其它线程等待结果。

模式B.
当然,如果缓存本身的过期时间比较短,也可以采用如下模式

  1. @Cached(name="topic-", key="#Key", expire = 30cacheNullValue = true)
  2. List<T> getList(String Key){
  3. return ServiceA.getList(key)
  4. }

这样,用空值来进行一段时间的保证,一旦“异常“修复了,系统可以很快的恢复过来。

3. 函数中有其它业务时,一定要避免缓存注解导致业务代码不被执行

  1. @Cached(name="topic-", key="#topicID", expire = 3600)
  2. TopicInfo readTopic(String topicID){
  3. TopicInfo info = ServiceA.getTopicInfo(key);
  4. ServiceB.MarkTopicBeenReaded(info.ID);
  5. ServiceC.IncreaseTopicValue(info.ID);
  6. }

这个代码最严重的问题是,在同一个topic被读的时候,MarkTopicBeenReaded和IncreaseTopicValue只有很有限的机会被调用。这样的方法必须合理的拆解

  1. @Cached(name="topic-", key="#topicID", expire = 3600)
  2. TopicInfo getTopic(String topicID){
  3. return ServiceA.getTopicInfo(key);
  4. TopicInfo readTopic(String topicID){
  5. TopicInfo info = getTopic(topicID);
  6. ServiceB.MarkTopicBeenReaded(info.ID);
  7. ServiceC.IncreaseTopicValue(info.ID);
  8. }

4. 可以使用自动刷新,但一定要弄清楚场景

  1. @Cached(name="topic-", key="#topicID", expire = 3600)
  2. @CacheRefresh(timeUnit = TimeUnit.MINUTES, refresh = 60)
  3. TopicInfo getTopic(String topicID){
  4. return ServiceA.getTopicInfo(key);

上面的代码很可能性能很高,但这时候的数据一致性可能出现很多问题,最好是:能避免先避免(后期性能要求更高,同时大家对于业务更熟悉后,再根据情况考虑)。

如果调用外部接口不符合我们的要求呢?

比如如下接口,接口内已经封装了返回数据和状态码的情况

  1. @Cached(name="topic-", key="#var1.data.id", expire = 3600)
  2. Response<CircleDetailsVo> getCircleDetails(Request<CircleDto> var1);

这种情况下,使用缓存就会有我们上面提到的风险。

  • 返回值如果是异常值的话,异常值可能被缓存了,不符合要求。
  • 缓存穿透保护很难处理。

对这样的接口,原则上

  • 业务端不轻易加缓存。
  • 如果加缓存优先走”业务缓存模式“,而不是jetcache注解模式。比如我们目前有的:User, Circle, Crowd三个缓存方法类。

如果最后我们还需要jetcache进行一定的优化来让我们调用引擎更快,则处理方式是:

  1. //接口
  2. Response<CircleDetailsVo> getCircleDetails(Request<CircleDto> var1);
  3. //调用引擎的实现。
  4. @Cached(name="topic-", key="#data.id", expire = 3600)
  5. CircleDetailsVo getCircleDetails(CircleDto data){
  6. Response<CircleDetailsVo> res = getCircleDetails(...);
  7. if(res.code == '200' && res.data !=null && ...) {
  8. return res.data;
  9. } else {
  10. //可能有两种可能性
  11. //1. 通过异常返回数据。
  12. throw new RespondException("", res);
  13. //2. 如果返回空值是合理的,则
  14. return null;
  15. }
  16. }

当然,到底是使用异常返回还是使用空值返回,需要根据业务情况case by case.

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注