深圳幻海软件技术有限公司 欢迎您!

注解式两级缓存服务框架设计与构建

2023-02-28

前言 缓存在现代计算机系统中无处不在,各式各样硬件和软件的组合构成和管理着缓存,一个编写良好的计算机程序倾向于展示出良好的局部性。在高性能服务架构设计中,缓存是一个不可或缺的环节。以Java体系为例,我们从传统的硬编码方式使用缓存到基于注解的spring-cache框架,确实大大提升了我们

前言 

缓存在现代计算机系统中无处不在,各式各样硬件和软件的组合构成和管理着缓存,一个编写良好的计算机程序倾向于展示出良好的局部性。

在高性能服务架构设计中,缓存是一个不可或缺的环节。以Java体系为例,我们从传统的硬编码方式使用缓存到基于注解的spring-cache框架,确实大大提升了我们的效率,代码也更加的简洁易维护。

但随着越来越多的项目使用spring-cache,场景越来越复杂,我们逐渐发现缓存配置代码重复、缓存策略不能在注解上直接配置、不支持多级缓存、不支持自动刷新缓存等问题逐渐突显。

基于这些在业务中遇到的问题点,我们构建了一套注解式两级缓存服务框架。在实际设计和构建过程中积累了一些经验,借此机会分享给大家,希望对业务中使用缓存尤其使用spring-cache场景的可以提供一些帮助。

 1. spring-cache简介 

Spring 3.1之后,引入了注解缓存技术,其本质上不是一个具体的缓存实现方案,而是一个对缓存使用的抽象,不仅能够使用SpEL(Spring Expression Language)来定义缓存的key和各种condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存集成。

事物都有两面性,优点如此优秀,那么缺点或不足是否也是如此的突出呢?

  • spring-cache问题点(我们认为)
  • 不支持缓存策略在注解上设置,每个方法的缓存策略需要单独硬编码方式配置
  • 不支持多级缓存
  • 不支持自动刷新缓存
  • 不支持缓存统计看板
  • 不支持熔断降级
  • 不支持数据压缩
  • 代码重复不易维护

基于以上我们认为的问题点,我们造了一个轮子,来试图解决这些问题,这个轮子就是“注解式两级缓存框架”。

2. 注解式两级缓存框架简介

  • 是一个注解式两级缓存框架:通过注解实现声明式的方法缓存,使用方式和spring-cache类似,提供了比spring-cache更加强大的注解。
  • 是一个全新的注解式两级缓存框架:一级缓存使用本地缓存(目前只支持Caffeine,后续可扩展),二级缓存使用集中式缓存(目前只支持Redis)。目前支持三种缓存策略:

只使用一级缓存

只使用二级缓存

同时用两级缓存

2.1全部特性

  • 支持TTL在注解上直接配置
  • 支持本地缓存容量在注解上直接配置
  • 支持condition在注解上直接配置,指定符合条件的情况下才缓存
  • 支持缓存Key的SpEL表达式、自定义生成策略(已提供默认生成策略)
  • 支持只使用一级缓存或者只使用二级缓存或者使用两级缓存
  • 支持value序列化策略配置,默认GenericJackson2JsonRedisSerializer
  • 支持异步加载缓存的方式
  • 支持自动刷新缓存
  • redis客户端选择
  • 支持熔断与降级 --- 延迟支持
  • 支持缓存数据压缩 --- 延迟支持
  • 支持缓存一致性 --- 延迟支持
  • 支持缓存监控统计看板 --- 延迟支持
  • 支持自定义缓存中间件 --- 延迟支持
  • 支持缓存接口用于手工缓存操作 --- 延迟支持

前菜我们品完了,接下来我们开始正餐,一步步介绍下设计思路,聊下如何站在spring-cache巨人肩膀上,试图解决上述问题点的。

 3. 注解式两级缓存框架架构设计

3.1注解@EnableCache

1、注解@EnableCache导入CacheConfigurationSelector。

CacheConfigurationSelector向容器内注入了AutoProxyRegistrar和ProxyCacheAutoConfiguration这两个Bean

2、AutoProxyRegistrar会确保容器中存在一个自动代理创建器(APC),缓存的代理对象最终是委托给自动代理创建器来完成。

AutoProxyRegistrar在容器启动阶段对每个bean创建进行处理,如果bean中有方法标记了cache注解,为其创建代理对象, 包裹定义的CacheOperationSourceAdvisor bean

3、ProxyCacheAutoConfiguration向容器定义如下基础设施bean。

CacheOperationSourceAdvisor 用于管理CacheOperationSource和CacheInterceptor, CacheOperationSource 用于获取方法调用时最终应用的Cache注解的元数据, CacheInterceptor 包裹在目标bean外面用于操作Cache的AOP Advice

3.2拦截器

由于AutoProxyRegistrar在容器启动阶段会对标有cache注解的bean创建代理对象,这时我们可以获取到具体方法和注解元数据, 我们针对两部分数据进行绑定提前缓存起来,这样目标方法调用时直接从缓存中获取元数据即可,避免了反射效率低下影响性能。

1、根据目标方法和目标类获取注解元数据,元数据包括缓存名称、缓存key、过期时间、自动刷新时间、本地缓存容量、缓存类型、缓存条件等。

2、根据缓存条件是否走注解缓存,缓存条件支持SpEL表达式,如果为false则直接执行目标方法,为ture走缓存逻辑。

3、生成key:支持SpEL表达式,可以自定义生成规则,默认规则:命名空间、所属类名称、方法名称、方法参数以冒号相连。

4、获取cache:根据cacheName和cacheType获取cache,对应cache有本地cache、远程cache、两级cache,根据key获取缓存结果, 缓存结果为空则执行目标方法并对结果缓存,反之直接返回缓存结果。

3.3获取cache组件

cache实现类有三种LocalCache、RemoteCache和TwoLevelCache,每个缓存实现类集成了具体的缓存中间件,LocalCache可以集成Caffeine、Guava、ehCache等, RemoteCache可以集成Redis、Memcache等,TwoLevelCache是LocalCache和RemoteCache组合实现。

1、CacheManagerContainer管理着所有的CacheManager,每个cacheType对应一个CacheManager的实现。

2、CacheManager提供了cache实现bean的创建,管理着多个cache,每个cache有对应的cacheName,每个应用里可以通过cacheName来对cache进行隔离,如果cacheName对应的cache不存在则会注册一个新的cache。

3、Cache接口提供了缓存的具体操作,例如放入,读取,清理等。

3.4两级缓存

两级缓存的产生是因为远程缓存有网络开销,大量的缓存读取会导致远程缓存网络成为整个系统的瓶颈,本地缓存是和应用程序在一个进程内,请求缓存速度快,没有过多的网络开销, 加入本地缓存目标是降低对远程缓存的读取次数,减轻网络开销,从而再次提升程序的响应速度与服务性能。

1、从本地缓存读出数据,如果存在则直接返回,进行后续具体业务逻辑。

2、本地缓存如果不存在则读取远程缓存,远程缓存如果存在则更新本地缓存,不存在则从数据源读取,然后依次更新远程缓存、本地缓存,然后进行后续具体业务逻辑。

3.5自动刷新缓存

防止某个缓存失效时,访问量突然大增,所有请求访问数据库,可能导致数据库挂掉;适用场景:key数量比较少,访问量大,加载开销较大的情况。

1、缓存读取时如果元数据自动刷新时间有值,会根据缓存key、目标方法、刷新时间创建一个给定初始延迟的间隔性的任务,任务自动执行间隔为自动刷新时间, 任务执行时会根据缓存key、目标方法重新加载缓存,保持缓存一直生效。

2、根据自动刷新时间会生成一个停止刷新时间,如果缓存key访问间隔时间超过了停止刷新时间或者缓存key过期,会删除该定时任务,释放资源,避免无效的刷新缓存。

3、两级缓存刷新缓存顺序为:先刷新远程缓存,然后根据Redis的pub/sub模式去监测和操作本地cache的删除动作,随后第一次请求会检查本地缓存--->再检查Redis缓存--->回源。

4、远程缓存自动刷新使用分布式锁,对同一key,全局只有一台机器自动刷新。

3.6注解@Cacheable

1、 @Cacheable可以作用在方法上,也可以标记在一个类上,当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的

名称

默认值

说明

value

空字符串

缓存名称,cacheName的别名

cacheName

空字符串

缓存名称

key

空字符串

缓存key,支持SpEL表达式,提供默认生成策略

ttl

10分钟

过期时间,d/h/m/s四种时间单位选择,分别代表天/时/分/秒, ttl="10m"表示10分钟过期时间

refreshTime

空字符串

自动刷新时间,d/h/m/s四种时间单位选择,分别代表天/时/分/秒

maximumSize

5000

本地缓存容量

cacheType

REMOTE

缓存类型,LOCAL/REMOTE/BOTH三种选择,分别代表本地缓存/集中式缓存/两级缓存

condition

空字符串

指定符合条件的情况下才缓存,为空则认为全部无条件缓存,支持SpEL表达式

2、key默认生成规则:命名空间、所属类名称、方法名称、方法参数以冒号相连。

3、如果设置ttl为空:表示缓存永不过期。

3.7缓存配置

这里举个例子,具体的参数值,根据自己业务情况自行调整。

auto:
  cache:
    local:
      type: caffeine
    remote:
      type: redis
      host: localhost #服务器地址
      port: 6379 #服务器连接端口
      timeout: 2000 #连接超时时间(毫秒)
      pool:
        min-idle: 2 #最小空闲连接数
        max-idle: 10 #最大空闲连接数
        max-active: 20 #连接池的最大数据库连接数
        max-wait: 200 #最大建立连接等待时间
      key-serializer: org.springframework.data.redis.serializer.StringRedisSerializer #key序列化策略
      value-serializer: org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer #value序列化策略
    namespace: testCache # 缓存的命名空间前缀,最终缓存格式为:testCache:xxx:xx
    allow-null-values: true #防止缓存穿透
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

 4. 总结和展望

本文主要是记录了商业资源组在使用spring-cache过程中遇到的问题点及注解式两级缓存服务框架设计思路,通过一步步拆解,将问题点逐个击破。该框架在实际项目中也经过了千万级别的验证,为我们的线上服务提供了良好的性能。

构建一套完整的服务框架需要不断的迭代功能开发,后续要逐步支持的功能如下:

增加熔断与降级

增加缓存数据压缩

增加缓存一致性

增加缓存监控统计看板

增加自定义缓存中间件

增加缓存接口用于手工缓存操作

参考文献

​​https://github.com/ben-manes/caffeine/wiki/Benchmarks​​

​​https://docs.spring.io/spring-framework/docs/4.3.x/spring-framework-reference/html/expressions.html​​

作者介绍:王云朋

  • 经销商技术部-商业资源团队
  • 2017年加入汽车之家经销商事业部,目前主要负责智能展厅核心功能开发工作