缓存在现代计算机系统中无处不在,各式各样硬件和软件的组合构成和管理着缓存,一个编写良好的计算机程序倾向于展示出良好的局部性。,
,在高性能服务架构设计中,缓存是一个不可或缺的环节。以Java体系为例,我们从传统的硬编码方式使用缓存到基于注解的spring-cache框架,确实大大提升了我们的效率,代码也更加的简洁易维护。,但随着越来越多的项目使用spring-cache,场景越来越复杂,我们逐渐发现缓存配置代码重复、缓存策略不能在注解上直接配置、不支持多级缓存、不支持自动刷新缓存等问题逐渐突显。,基于这些在业务中遇到的问题点,我们构建了一套注解式两级缓存服务框架。在实际设计和构建过程中积累了一些经验,借此机会分享给大家,希望对业务中使用缓存尤其使用spring-cache场景的可以提供一些帮助。,Spring 3.1之后,引入了注解缓存技术,其本质上不是一个具体的缓存实现方案,而是一个对缓存使用的抽象,不仅能够使用SpEL(Spring Expression Language)来定义缓存的key和各种condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存集成。,事物都有两面性,优点如此优秀,那么缺点或不足是否也是如此的突出呢?,基于以上我们认为的问题点,我们造了一个轮子,来试图解决这些问题,这个轮子就是“注解式两级缓存框架”。,只使用一级缓存,只使用二级缓存,同时用两级缓存,前菜我们品完了,接下来我们开始正餐,一步步介绍下设计思路,聊下如何站在spring-cache巨人肩膀上,试图解决上述问题点的。,
,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,由于AutoProxyRegistrar在容器启动阶段会对标有cache注解的bean创建代理对象,这时我们可以获取到具体方法和注解元数据, 我们针对两部分数据进行绑定提前缓存起来,这样目标方法调用时直接从缓存中获取元数据即可,避免了反射效率低下影响性能。,
,1、根据目标方法和目标类获取注解元数据,元数据包括缓存名称、缓存key、过期时间、自动刷新时间、本地缓存容量、缓存类型、缓存条件等。,2、根据缓存条件是否走注解缓存,缓存条件支持SpEL表达式,如果为false则直接执行目标方法,为ture走缓存逻辑。,3、生成key:支持SpEL表达式,可以自定义生成规则,默认规则:命名空间、所属类名称、方法名称、方法参数以冒号相连。,4、获取cache:根据cacheName和cacheType获取cache,对应cache有本地cache、远程cache、两级cache,根据key获取缓存结果, 缓存结果为空则执行目标方法并对结果缓存,反之直接返回缓存结果。,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接口提供了缓存的具体操作,例如放入,读取,清理等。,两级缓存的产生是因为远程缓存有网络开销,大量的缓存读取会导致远程缓存网络成为整个系统的瓶颈,本地缓存是和应用程序在一个进程内,请求缓存速度快,没有过多的网络开销, 加入本地缓存目标是降低对远程缓存的读取次数,减轻网络开销,从而再次提升程序的响应速度与服务性能。,
,1、从本地缓存读出数据,如果存在则直接返回,进行后续具体业务逻辑。,2、本地缓存如果不存在则读取远程缓存,远程缓存如果存在则更新本地缓存,不存在则从数据源读取,然后依次更新远程缓存、本地缓存,然后进行后续具体业务逻辑。,防止某个缓存失效时,访问量突然大增,所有请求访问数据库,可能导致数据库挂掉;适用场景:key数量比较少,访问量大,加载开销较大的情况。,
,1、缓存读取时如果元数据自动刷新时间有值,会根据缓存key、目标方法、刷新时间创建一个给定初始延迟的间隔性的任务,任务自动执行间隔为自动刷新时间, 任务执行时会根据缓存key、目标方法重新加载缓存,保持缓存一直生效。,2、根据自动刷新时间会生成一个停止刷新时间,如果缓存key访问间隔时间超过了停止刷新时间或者缓存key过期,会删除该定时任务,释放资源,避免无效的刷新缓存。,3、两级缓存刷新缓存顺序为:先刷新远程缓存,然后根据Redis的pub/sub模式去监测和操作本地cache的删除动作,随后第一次请求会检查本地缓存--->再检查Redis缓存--->回源。,4、远程缓存自动刷新使用分布式锁,对同一key,全局只有一台机器自动刷新。,1、 @Cacheable可以作用在方法上,也可以标记在一个类上,当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的,2、key默认生成规则:命名空间、所属类名称、方法名称、方法参数以冒号相连。,3、如果设置ttl为空:表示缓存永不过期。,这里举个例子,具体的参数值,根据自己业务情况自行调整。,本文主要是记录了商业资源组在使用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,
,作者介绍:王云朋
© 版权声明
文章版权归作者所有,未经允许请勿转载。