总篇183篇 2022年第58篇?前言?缓存在现代计算机系统中无处不在,各式各样硬件和软件的组合构成和管理着缓存,一个编写良好的计算机程序倾向于展示出良好的局部性。在高性能服务架构设计中,缓存是一个不可或缺的环节。以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注解@EnableCache1、注解@EnableCache导入CacheConfigurationSelector。CacheConfigurationSelector向容器内注入了AutoProxyRegistrar和ProxyCacheAutoConfiguration这两个Bean2、AutoProxyRegistrar会确保容器中存在一个自动代理创建器(APC),缓存的代理对象最终是委托给自动代理创建器来完成。AutoProxyRegistrar在容器启动阶段对每个bean创建进行处理,如果bean中有方法标记了cache注解,为其创建代理对象, 包裹定义的CacheOperationSourceAdvisor bean3、ProxyCacheAutoConfiguration向容器定义如下基础设施bean。CacheOperationSourceAdvisor 用于管理CacheOperationSource和CacheInterceptor, CacheOperationSource 用于获取方法调用时最终应用的Cache注解的元数据, CacheInterceptor 包裹在目标bean外面用于操作Cache的AOP Advice3.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注解@Cacheable1、 @Cacheable可以作用在方法上,也可以标记在一个类上,当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的名称默认值说明value空字符串缓存名称,cacheName的别名cacheName空字符串缓存名称key空字符串缓存key,支持SpEL表达式,提供默认生成策略ttl10分钟过期时间,d/h/m/s四种时间单位选择,分别代表天/时/分/秒, ttl="10m"表示10分钟过期时间refreshTime空字符串自动刷新时间,d/h/m/s四种时间单位选择,分别代表天/时/分/秒maximumSize5000本地缓存容量cacheTypeREMOTE缓存类型,LOCAL/REMOTE/BOTH三种选择,分别代表本地缓存/集中式缓存/两级缓存condition空字符串指定符合条件的情况下才缓存,为空则认为全部无条件缓存,支持SpEL表达式2、key默认生成规则:命名空间、所属类名称、方法名称、方法参数以冒号相连。3、如果设置ttl为空:表示缓存永不过期。3.7缓存配置这里举个例子,具体的参数值,根据自己业务情况自行调整。auto:cache:local:type:?caffeineremote:type:?redishost:?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:xxallow-null-values:?true?#防止缓存穿透?4.?总结和展望本文主要是记录了商业资源组在使用spring-cache过程中遇到的问题点及注解式两级缓存服务框架设计思路,通过一步步拆解,将问题点逐个击破。该框架在实际项目中也经过了千万级别的验证,为我们的线上服务提供了良好的性能。构建一套完整的服务框架需要不断的迭代功能开发,后续要逐步支持的功能如下:增加熔断与降级增加缓存数据压缩增加缓存一致性增加缓存监控统计看板增加自定义缓存中间件增加缓存接口用于手工缓存操作参考文献
https://github.com/ben-manes/caffeine/wiki/Benchmarkshttps://docs.spring.io/spring-framework/docs/4.3.x/spring-framework-reference/html/expressions.html作者简介王云朋经销商技术部-商业资源团队2017年加入汽车之家经销商事业部,目前主要负责智能展厅核心功能开发工作阅读更多Unity3D 引擎的性能优化实践汽车之家电商系统架构演进与平台化架构实践面向推荐的汽车知识图谱构建