前言
缓存在现代计算机系统中无处不在,各式各样硬件和软件的组合构成和管理着缓存,一个编写良好的计算机程序倾向于展示出良好的局部性。
在高性能服务架构设计中,缓存是一个不可或缺的环节。以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缓存配置
这里举个例子,具体的参数值,根据自己业务情况自行调整。
4. 总结和展望
本文主要是记录了商业资源组在使用spring-cache过程中遇到的问题点及注解式两级缓存服务框架设计思路,通过一步步拆解,将问题点逐个击破。该框架在实际项目中也经过了千万级别的验证,为我们的线上服务提供了良好的性能。
构建一套完整的服务框架需要不断的迭代功能开发,后续要逐步支持的功能如下:
增加熔断与降级
增加缓存数据压缩
增加缓存一致性
增加缓存监控统计看板
增加自定义缓存中间件
增加缓存接口用于手工缓存操作
参考文献
https://github.com/ben-manes/caffeine/wiki/Benchmarks
作者介绍:王云朋
- 经销商技术部-商业资源团队
- 2017年加入汽车之家经销商事业部,目前主要负责智能展厅核心功能开发工作