最近在做的一个系统涉及到基础数据的频繁调用,大量的网络开销和数据读写给系统带来了极大的性能压力,我们决定引入缓存机制来缓解系统压力。


什么是缓存

提起缓存机制,大概10个程序员总有5种不同的解释吧(姑且认为只有一半的程序员是通过复制粘贴来学习知识的),我也不能免俗的来说说我的理解。

 

在回答这个问题之前,我们首先要搞清楚为什么要用缓存?

历史唯物主义揭示了社会发展的基本动力是社会基础矛盾。

运用到软件领域同样适用,一种新技术的出现必然是伴随着特定的矛盾产生的,而缓存的出现正是因为介质提供的实际处理响应速度和软件需求之间的矛盾,最终缓存机制的提出大大的缓解了这个矛盾,同时也印证了一句计算机领域的名言:

Any problem in computer science can be solved by anther layer of indirection.

 Picture1.png

 

缓存示意图

结合上图我们可以看出缓存从某种意义上来说是一种代理,通过自身某一方面的优势弥补实际响应的局限性,理论上来说还是时间和空间的取舍权衡。

下面列举几种常见的缓存

1, 数据库缓存

通过将查询语句缓存到内存中来减少文件系统的读写次数和程序响应时间

2, 应用缓存

将应用常用数据缓存到内存中来减少数据库访问,通过缓存减少了连接创建销毁的时间

3, 用户端缓存

通过一些用户端技术如浏览器和本地cookie等将用户常用数据进行缓存,减少网络连接的创建销毁,同时避免了网络传输的消耗


Spring中的缓存

 

Spring3.1版本开始就引入了基于注解的缓存支持,到现在已经发展的相当稳定了。Spring主要提供的是基于JSR107的抽象,对于缓存的具体实现可以是EhCache也可以是Redis。下面简单搬运一下几种注解的定义:

@Cacheable  缓存的入口,首先检查缓存如果没有命中则执行方法并将方法结果缓存

@CacheEvict  缓存回收,清空对应的缓存数据

@CachePut   缓存更新,执行方法并将方法执行结果更新到缓存中

@Caching    组合多个缓存操作

@CacheConfig 类级别的公共配置

 

原文链接:

https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#cache


实际系统中的应用

在了解了缓存的一些基础知识和框架的支持情况后,我们开始付诸实施,我们使用Redis作为缓存的具体实现。

项目基于spring boot <version>2.0.0.RC1</version>maven的主要配置信息如下:

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.RC1</version>
</parent>
<dependencies>
           <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-data-redis</artifactId>
             <version>1.5.7.RELEASE</version>
      </dependency>
</dependencies>


 

首先明确缓存的位置,缓存的参与方可能在下面四层

a)      客户端

b)      接口层

c)      服务层

d)      数据层

 

在选择位置的时候出现的分歧是离客户端更近一些还是离缓存所有方更近,具体到我们系统中就是缓存放在a还是b,各有优劣。

放在客户端可以降低网络消耗,放在服务端可以明确管理职责,最终我们选择了放在b牺牲一部分的性能消耗来保证数据的完整性和一致性。

 

下面通过两个场景来说明缓存的维护

1,   缓存创建(接口层@Cacheable

       无标题.png

2,   缓存更新(服务层@CacheEvict, @Caching

      无标题.png

注:考虑配置数据的修改频率较低,并且配置数据的缓存结构比较复杂,每次数据修改和新增会删除相应的缓存,再由接口层调用来重新加载缓存

 

接下来就是实现了,

首先需要开启缓存功能,在主程序上加上@EnableCaching注解即可

然后是相关注解的代码:


   @Cacheable(value="icare_region",key="('c_').concat(#companyId)")
   public List<Region> loadRegionByCompIdRest(@RequestParam("companyId") Integer companyId){
          List<Region> regions = regionService.selectRegionsByCompId(companyId);
          return regions;
      }
 

   @CacheEvict(cacheNames="icare_region", key="('c_').concat(#region.companyId)")
   public void saveRegion(Region region) {
        regionMapper.insert(region);
   }
 

   @Caching(evict = { @CacheEvict(cacheNames="icare_region", key="('r_').concat(#region.regionId)"), @CacheEvict(cacheNames="icare_region", key="('c_').concat(#region.companyId)") })
   public void updateRegion(Region region) {
        Region existRegion = regionMapper.selectByPrimaryKey(region.getRegionId());
        region.setStatus(existRegion.getStatus());
        region.setCreateTime(existRegion.getCreateTime());
        region.setUpdateTime(new Date());
        regionMapper.updateByPrimaryKey(region);
   }


 

最后就是测试了

在如何确定程序按照我们的意图走到了缓存而非原来的数据库调用的时候,我们使用了druidsql监控功能,如图直接观察sql的执行次数就可以:

      无标题.png


问题和扩展

先说个碰到的具体问题,我们在使用Redis的时候选择从网上拷贝了一个RedisConfig的文件来扩展KeyGeneratorRedisTemplateCacheManager。但是当我们再引入了spring bootdev-tool的时候,上面的缓存实现会报错提示ClassCast Exception

最终在官网找到答案:在老版本的CacheManager中没有考虑序列化和反序列化的ClassLoader问题,导致序列化和反序列化的ClassLoader不一致;最新的修复就是指定了CacheManager使用的ClassLoader。而网上现在流传的都是老版本的CacheManager,反而把最新版本的修复覆盖掉了

问题链接:https://github.com/spring-projects/spring-boot/issues/11822

 

此外,我们现在实现的这种缓存还有诸多限制,也是我们要扩展的方向

1, 无法设置失效时间

Redis是支持设置失效时间的,但是spring 抽象中没有提供相关支持。

2, 无法统计命中率等指标

无法统计命中率就没有办法判定缓存的失效和替换,当然这些都是在缓存变大的情况下需要考虑的