内容简介:在spring 体系中,使用spring cache并结合redis来进行数据缓存是很常见的做法。不过,针对于具体的业务场景,可能会有不同的处理方法。像以下的1个业务场景,即有不同的处理方式。
在spring 体系中,使用spring cache并结合 redis 来进行数据缓存是很常见的做法。不过,针对于具体的业务场景,可能会有不同的处理方法。
像以下的1个业务场景,即有不同的处理方式。
前端访问后端的指定请求路径(GET类请求), 针对特定的条件下(对应cache condition),希望这个结果能够被缓存.同时,支持当资源修改之后,让此缓存失效掉。
此场景的典型方案就是使用spring cache的 @Cacheable 注解 和 @CacheEvict 注解,并结合实际场景进行混合处理。
在这个实际的场景当中,经历了 service层缓存,Controller层缓存,和Filter层缓存三个阶段,最终达到业务的需求,并且在性能上更接近于实际的需要。
本文就里面碰到的一些实际问题以及解决方式进行了简单描述,从复杂的框架层修改到简单的拦截处理,在思考思路上进行一个分享。
前提
本文中使用的序列化为jackson,即使用jackson将对象序列化为 json 字符串,再getBytes为 字节数组.
Service层缓存
service层cache即通过标准的spring cache 注解,在相应的方法上追加上注解信息,然后利用CacheInterceptor的拦截能力,拦截调用方法,当满足缓存条件时,将相应的结果对象序列化。在这个场景中需要处理的主要问题就是与业务之间的链接问题。
从理论上看,调用一个service,不管结果是从代码执行出来的还是从缓存出来的,那么如果结果内容是相同的,则并没有相应的区别。但如果内容不一样,则这个缓存本身就会有相应的问题。在这个场景中,结果对象中的对象体系由于之前对前端的特殊处理,导致按照正常的序列化情况,可能有些数据就不能正常的反序列化回来。如下的例子:
class VersionedObj {
@JsonIgnore
@Version
long version;
}
字段 version 在对于前端的响应中是不需要的,因此这里将其设定为version,但对于后端来说(用于JPA的版本化支持),这个字段又是需要的。在调用相应的方法时,期望的结果中是需要此字段值。如果使用标准的jackson objectMapper,这个字段的序列化即是一个问题。在实际处理中,可以使用 Jackson Mixin 概念来解决这个问题。
回头看一下应用场景,因为service方法可以在业务中的另1个service调用,也可以由controller来调用。但在应用场景中,期望仅通过controller来调用的方法走缓存,而是内部的调用则仍然调用原始的实现。即缓存的应用场景是基于前端访问的,在这里service的缓存控制仍然显得靠后了一点。
Controller层缓存
前移之后,需要处理2个问题。
1个是有一些公共的Controller是不可简单的在方法上加注解的。如类 RepositoryEntityController(spring data rest中)。这个类由spring 体系提供,但在这个类中的findById场景中,受缓存的影响最大,我们期望这个接口的结果是可缓存的,但不能在方法上加注解,因此cache切面不能正常工作。
此问题的处理方式是调整相应的 RepositoryRestHandlerAdapter 处理,在标准的spring mvc 中,调用controller均是通过类 ServletInvocableHandlerMethod#invokeAndHandle 来完成。因此可以通过 子类化 RepositoryRestHandlerAdapter,然后提供一个新的 ServletInvocableHandlerMethod 实例来完成相应的controller调用来处理。在 ServletInvocableHandlerMethod 子类中,可以手动来实现 cacheInterceptor的能力,即先 condition判定,cacheKey生成,原方法调用,写入redis 这几个步骤. 在具体实现上,相应的api尽量与原生cache 相一致。比如,写入redis这一步,仍然可以通过 获取到一个有效的 Cache 实例,然后再调用 cache#put 来完成相应数据的写入.
另1个问题则是反序列化问题,使用spring data (或hateos) 时,针对结果如类 ResponseEntity Resources, 其反序列化简直是噩梦。大量的final字段,以及不提供setter/getter。同时,官方实现也没有deserializer,整个过程真是麻烦。同时,在controller层的结果还并不是最终结果,可能还会在后续的处理流程中进行调整。这就导致在controller层作的缓存数据并不能直接落到前端,还要再处理一次。此问题的解决可以通过将前面写入redis的操作后移,通过注入一个 ResponseBodyAdvice 来完成此操作。在前一步中,记一个相应的标记。在advice中,如果判定需要作cache 处理,则在这里才将相应的数据(已经经过最终处理后)落到redis中。
controller层缓存的另1个问题则是当读取redis数据之后,将其反序列化为结果对象,但马上此数据即要重新被jackson序列化为 字符串返回给前端。在这个过程中,中间的一个反序列化和序列化是完全没有必要的。并且在前面的步骤中,为解决序列化的问题对框架层改动太多。
Filter层缓存
从应用场景来看,其实并不完全需要spring-cache的整个功能,而是仅仅需要一个类似nginx cache的一个模块。 同时,支持缓存evict的能力即可。
因此,相应的实现方案即通过一个filter,拦截调用请求,针对需要作缓存的请求,尝试拦截后端对响应的处理。如果需要缓存,则将响应的结果(即outputStream中的数据)写入缓存。下次,相同的请求即直接从缓存中读取数据,直接写回response即可。这样在下一次请求时,整个spring的servlet层直接全部跳过,从时序上来看,请求在 filter 层即已经完成整个处理,都不需要spring的mapping过程。
在具体实现过程中,condition的处理需要从请求中重新进行设计,因为在这一层中,只能拿到 路径,请求信息,请求头这些标准的数据信息,还不能到达对象体系的目的。不过对于业务来说,不同的路径,其在业务层的condition条件是已知的,可以通过 spring el 在这一层作一些脚本化的判断,也是可以工作的。
总结
整个过程,其实是对需求的不断调整和改进,如果一开始就套用技术,可能会初期满足业务需求,但总会有一定的限制和缺陷。不断地改进过程,对应用技术本身的了解就会越清晰。
以上所述就是小编给大家介绍的《spring cache 接口层缓存的演进过程》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Sovereign Individual
James Dale Davidson、William Rees-Mogg / Free Press / 1999-08-26 / USD 16.00
Two renowned investment advisors and authors of the bestseller The Great Reckoning bring to light both currents of disaster and the potential for prosperity and renewal in the face of radical changes ......一起来看看 《The Sovereign Individual》 这本书的介绍吧!