我读取 缓存的最好的比喻 来自Peter Chester,他在WordPress会议期间使用。他问观众,“3,485,250分为23,235”?最初的沉默之后,有些人把他们的计算器拿出来,最后有人大声喊道:“150.”切斯特先生又问了一个同样的问题,能够回应 这是缓存!
总而言之,这是一个非常简单的缓存情况,因为答案总是一样的。但隐喻是太棒了!实质上,缓存是关于经济。我们为等待响应的客户节省时间。我们节省资源,重新计算我们已经知道的答案。我们节省带宽。
我们该怎么做?通过保持一些响应“更接近”请求者并再次服务,而不必返回原始服务器并再次计算答案。
这不是一项直接的任务,它的实施需要严肃的策略。我们需要评估我们的应用程序的“最终目标”,我们的数据的性质,我们的应用程序的用户,现有的应用程序设计和缓存的实际目的。如果我们的策略没有很好的思考,那就是各种各样的危险。例子?
我们可能会暴露可能损害用户的敏感数据。我们可能会提供陈旧/无效的数据,这取决于应用程序可能是灾难性的。如果我们的缓存命中率不佳,那么我们甚至可以伤害我们的表现,这仅仅是缓存中可以提供的请求数量除以所提供的总请求数量。
你可能会问,为什么不把所有东西保存在缓存?再次,经济!缓存通常在内存中完成,这比数据库存储更昂贵。同样复制我们整个数据库可能是昂贵和非常复杂的。
那么,我们如何决定在数据库中保留什么以及分配多少空间?在什么顺序上,当缓存中的项目变满时,我们会将项目从序列中删除 那么它更像是一门艺术。我们做一个假设,监测是否和如何工作和相应的调整。这整个运动值得我们的麻烦吗?
我们可以乐观地认为,一些数据将比其他依靠诸如参考地点 和 帕累托原理这样的原则更受欢迎。是的,缓存是值得的。
我们来看一个Web应用程序的高级视图,以便介绍一些与缓存相关的术语,并在一些不同的场景下给出一些理由。考虑一个典型的多层应用程序。在基本级别上,我们有一个源服务器,它提供对某种存储库数据的访问,并执行计算。另一方面是客户端,通常是Web浏览器,用户可以从中查看和访问原始服务器的优秀应用程序和产品。在它们之间,许多其他组件可以作为调用者将数据从源服务器传递到浏览器,以供用户满意。
现在,这些层合作进行缓存的主要方式是通过彼此发信号通知其规则或偏好。这通过HTTP头完成。我不会详细介绍与缓存有关的所有标题,但是我们可以根据它们的工作方式对它们进行大致的分类。
时间
某些标头用于指示缓存资源被认为是新鲜的时间段。当资源可以用于提供请求时,资源是新鲜的,意味着它与原始服务器中的资源处于状态。不新鲜的内容称为陈旧。这些标头在缓存控制中提供,最常见的是:
max-age:这是缓存资源必须重新生效之前的时间段
s-maxage:与max-age相同,但用于中间缓存。结合起来,它们可以为我们的缓存策略提供灵活性
max-stale:客户愿意接受超出到期时间的响应
最新鲜:客户要求在指定时间段内新鲜的客户使用(由他们)
正如你所理解的,这个基于缓存的缓存,并不能保证我们从缓存中收到的内容不会过时。在资源被缓存之后立即发生,它将在源服务器中更新。但这种缓存有其用途。例如,有这样的策略几秒钟可以保护我们免受持续按f5的用户。
州
那么基于对象状态的缓存呢?有标题可以帮助:
ETag:源服务器与资源一起提供的值。在此资源的后续请求中,客户端提供ETag,并且服务器检查资源是否改变(基于计算的ETag),并相应地进行响应
最后修改:以类似的方式,服务器向资源提供最后的修改时间,客户端在下一个请求中使用它,并根据资源的最后修改日期作出响应,不会对其进行修改或者为新的最后修改时间
这些头确保确保缓存的内容与源服务器处于状态。但是,它们并没有真正使我们免于计算,起始服务器仍然需要评估资源的etag或检查其最后修改的时间。但是,当资源未被修改时,它可以节省带宽,因为它不再发送(用HTTP 304响应)。
所以我们还想要一些更好的东西,稍后我将在本教程中给出一个简单的例子,介绍如何保持缓存的新鲜。但现在让我们关闭一些其他标题,这些标题可以指定要缓存的内容。例如,如果内容可以被缓存,如果可以被转换,并且响应必须被重新验证,则由谁来缓存。
无存储:请求不以任何方式缓存资源,并且与敏感数据一起使用很重要
no-cache:请求每次都要重新验证任何缓存请求。不意味着必须重复所有内部计算,只需确保缓存的资源处于原始服务器的状态
private:指示资源不能被中间缓存缓存
public:允许资源由中间缓存缓存
content-length:指定要缓存的资源的大小
无变换:请求资源不会以任何方式进行转换(例如,为了性能原因而进行压缩)
必须重新验证:指示必须遵守*别和管理级(而不是例如在网络中断时提供过时的内容)
proxy-revalidate:与must-revalidate相同,但在中间缓存中使用
好的,现在我们了解Web缓存的工作原理,让我们看看如何在代码中实现这些。我创建了一个可以在我的github上找到的教程的小项目 。我们从我们的pom开始,这是相当简单的,因为我们将使用Spring Boot,它为我们提供了许多依赖。另外我们将使用EhCcache和其他依赖项,您可以在下面的pom中看到。
< project xmlns=“maven.apache/POM/4.0.0” xmlns:xsi=“w3/2001/XMLSchema-instance” xsi:schemaLocation=“http:/ /maven.apache/POM/4.0.0 maven.apache/maven-v4_0_0.xsd“ >
接下来,我们将为我们的二手手游购买应用程序设置配置。我们扩展了
AbstractAnnotationConfigDispatcherServletInitializer'类,它注册了一个ContextLoaderListener和一个DispatcherServlet。这简化了我们的工作,我们可以定义配置类(例如servlet)和servlet映射。
包 com.tasosmartidis.caching_demo.config;import org.springframework.web.servlet.support。AbstractAnnotationConfigDispatcherServletInitializer ;
WebAppInitializer类只有3种方法。getServletMappings()表示DispatcherServlet映射到的路径。我们把它映射到默认的servlet,它将处理进入我们应用程序的所有请求。getRootConfigClasses()处理由ContextLoaderListener创建的应用程序上下文的配置。最后,getServletConfigClasses()处理DispacherServlet的配置。
在我们的RootConfig类中,我们将创建通用目的bean,在这种情况下,EhCache的bean将为我们提供缓存支持。
包 com.tasosmartidis.caching_demo.config;import org.springframework.cache.CacheManager;import org.springframework.cache。注释 .EnableCaching;import org.springframework.cache.ehcache.EhCacheCacheManager;import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;import org.springframework.context。注释。import org.springframework.context。注释 ponentScan;import org.springframework.context。注释。import org.springframework.core.io.ClassPathResource;@Configuration @EnableCaching @ComponentScan(“com.tasosmartidis.caching_demo”)public class RootConfig { private static final String EHCACHE_CONFIGURATION=“ehcache.xml” ; @Bean
在WebConfig类中,我们将附加拦截器,我们将使用它们来记录来电的开始和结束。
package com .tasosmartidis .caching_demo .config ;import com .tasosmartidis .caching_demo .utils .LoggingInterceptor ;import org .springframework .context .annotation ponentScan ;进口 组织.springframework .context .annotation .Configuration ;进口 组织.springframework 名.web .servlet 的.config .annotation .DefaultServletHandlerConfigurer ;进口 组织.springframework 名.web .servlet 的.config .annotation .EnableWebMvc ;进口 组织.springframework 名.web .servlet 的.config .annotation .InterceptorRegistry ;进口 组织.springframework 名.web .servlet 的.config .annotation .WebMvcConfigurerAdapter ;
LoggingInterceptor类实现了HandleInterceptor接口,其实现如下所示:
包 com.tasosmartidis.caching_demo.utils;导入 org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;公共 类 LoggingInterceptor 实现 HandlerInterceptor { private static final Logger LOGGER=LoggerFactory.getLogger(LoggingInterceptor.class); @覆盖
现在我们在使用类和日志记录,这里是另一个用于在输入,启动和完成方法时记录的日志。
包 com.tasosmartidis.caching_demo.utils;导入 org.slf4j.Logger;public class LoggingUtils { public static void logMethodEntered (Logger logger) {
我们几乎准备好进入我们的演示项目。我们将要看到的缓存类型是基于时间的缓存,基于状态的缓存和缓存的无效。如前所述,我们使用EhCache进行缓存,以下是包含缓存配置的文件:
< ehcache xmlns:xsi=“w3/2001/XMLSchema-instance” xsi:noNamespaceSchemaLocation=“ehcache.xsd” updateCheck=“true” monitoring=“ autodetect ” dynamicConfig=“true” >
现在我们将创建一个端点来展示基于时间的缓存的工作原理。该服务将简单地返回“问候”,但端点的响应将被缓存指定的时间段。我们将使用日志记录来了解服务何时执行计算,而不是从缓存中提供服务。
包 com.tasosmartidis.caching_demo.web;import org.slf4j。;import org.springframework.cache。注释。import org.springframework.http.ResponseEntity;import org.springframework.web.bind。注释 import static com.tasosmartidis.caching_demo.utils.LoggingUtils.logMethodEntered;@RestController public class TimeBasedCachedService { 私人静态最终记录器记录仪=LoggerFactory.getLogger(TimeBasedCachedService。类); private static final String HELLO_INPUT_NAME_ENDPOINT=“/ hello / {name}” ; private static final String HELLO_WORLD_ENDPOINT=“/ helloworld” ; private static final String HELLO_SHORT_CACHE=“time-based-short-lived” ; private static final String HELLO_LONG_CACHE=“time-based-long-lived” ; private static final String NAME_CACHE_KEY=“#name” ; @RequestMapping(value=HELLO_WORLD_ENDPOINT,method=RequestMethod.GET)
现在我们将看看它的效果如何。我们将使用放心的呼叫端点,并确保我们的响应是按预期的,但记录的呼叫将给我们的主要输入。下面的课程会使事情更清楚:
包 com.tasosmartidis.caching_demo.web;import org.junit.Test;import org.junitnerWith;导入 org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;import static com.tasosmartidis.caching_demo.testutils.HttpMethodsUtils.doGetEnsure200AndReturnResponseAsString;import static com.tasosmartidis.caching_demo.utils.LoggingUtils。*;@RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.DEFINED_PORT) public class TimeBasedCachedServiceTest { private static final Logger LOGGER=LoggerFactory.getLogger(TimeBasedCachedServiceTest.class); private static final String HELLO_WORLD_RESOURCE=“helloworld” ; private static final String HELLO_INPUT_RESOURCE=“hello /” ; @Test
“
doGetEnsure200AndReturnResponseAsString”方法在一个实用程序类中,如下所示:
包com.tasosmartidis.caching_demo.testutils;
运行我们的
TimeBasedCachedServiceTest类将会提供类似的东西:
网页。TimeBasedCachedServiceTest:测试的方法的“Hello World”开始......
这是基于时间的,但是正如我们所讨论的,这样的缓存不能保证我们的缓存响应处于起始服务器的状态。所以我们来看看基于状态的服务。我们现在将会变得更加复杂,并且有数据保存和伪存储库来保存它们。然后,服务将允许我们更新和检索数据。我们有一个StubData POJO如下:
包 com.tasosmartidis.caching_demo.data;进口 lombok。*;import java.util.Date;@Getter @Setter @EqualsAndHashCode @Builder @ToString public class StubData { private String id; 私有字符串名称 私人日期lastModified; public void updateData () {
还有一个假仓库:
包com.tasosmartidis.caching_demo.data;import org.springframework.stereotypeponent;import java.util.Date;import java.util.HashMap;import java.util.Map;
没有什么奇怪的,只是一个地图内存与一些条目,我们可以通过我们的端点检索和更新。端点位于StateBasedCacheService类中:
包 com.tasosmartidis.caching_demo.web;导入 com.tasosmartidis.caching_demo。数据 .StubData;导入 com.tasosmartidis.caching_demo。数据 .StubDataDao;import com.tasosmartidis.caching_demo.utils.LoggingUtils;导入 org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory。注释。自动连线import org.springframework.http.CacheControl;import org.springframework.http.ResponseEntity;import org.springframework.web.bind。注释 .PathVariable;import org.springframework.web.bind。注释 .RequestMapping;import org.springframework.web.bind。注释 .RequestMethod;import org.springframework.web.bind。注释 .RestController;import javax.servlet.http.HttpServletRequest;import java.text.ParseException;import java.util.Date;import static com.tasosmartidis.caching_demo.utils.HttpUtils。*;@RestController public class StateBasedCachedService { 私人静态最终记录器记录仪=LoggerFactory.getLogger(StateBasedCachedService。类); private static final String STATE_BASED_BASE_ENDPOINT=“/ stubdata” ; private static final String STATE_BASED_UPDATE_RESOURCE_ENDPOINT=STATE_BASED_BASE_ENDPOINT + “/ {id}” ; private static final String LAST_MODIFIED_ENDPOINT=STATE_BASED_BASE_ENDPOINT + “/ lastmodified / {id}” ; private static final String ETAG_ENDPOINT=STATE_BASED_BASE_ENDPOINT + “/ etag / {id}” ; @Autowired
我们的端点与ETag和Last-Modified标题一起使用,因此我们创建一个实用程序方法来帮助我们进行必要的检查和操作。HttpUtils类显示如下:
包 com.tasosmartidis.caching_demo.utils;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import javax.servlet.http.HttpServletRequest;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.Date;公共 类 HttpUtils { private static final String IF_MODIFIED_SINCE_HEADER=“if-modified-since” ; private static final String HEADER_DATE_PATTERN=“EEE,dd MMM yyyy HH:mm:ss zzz” ; private static final String ETAG_HEADER=“etag” ; public static ResponseEntity make304NotModifiedResponse () { return ResponseEntity.status( HttpStatus.NOT_MODIFIED ).build();
现在我们来为这些端点创建我们的测试并查看日志:
包com.tasosmartidis.caching_demo.web;
我们测试的日志显示了我们的预期,如下图所示:
网页。StateBasedCachedServiceTest:测试的方法“最后一次修改”开始......
所以我们在不改变资源的情况下不再发送资源来节省一些带宽。但是,对于从存储库处理和检索资源来说,这并不算太多。而基于时间的缓存并没有确保我们的资源处于原始服务器的状态。那么什么
那么,我们可以提出一个解决方案来确保缓存被使用并得到保证。我们可以提出我们的缓存的无效解决方案。这绝对不是一个容易的任务,有一个长期的引用(起源未知):“计算机科学中只有两件难事:缓存无效和命名的东西。
让我们来看一个简单的无效示例,当资源被更新时:我们要么删除该资源的缓存版本,要么更新缓存中的版本。这保证我们永远不会提供陈旧的版本的请求。
以下课程将展示如何使用这种方式的服务:
包 com.tasosmartidis.caching_demo.web;导入 com.tasosmartidis.caching_demo。数据 .StubData;导入 com.tasosmartidis.caching_demo。数据 .StubDataDao;导入 org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory。注释。自动连线import org.springframework.cache。注释 .CacheEvict;import org.springframework.cache。注释 .CachePut;import org.springframework.cache。注释。import org.springframework.http.ResponseEntity;import org.springframework.web.bind。注释 .PathVariable;import org.springframework.web.bind。注释 .RequestMapping;import org.springframework.web.bind。注释 .RequestMethod;import org.springframework.web.bind。注释 .RestController;import static com.tasosmartidis.caching_demo.utils.LoggingUtils.logMethodEntered;@RestController 公共 类 CacheWithInvalidationService { 私人静态最终记录器记录仪=LoggerFactory.getLogger(CacheWithInvalidationService 类); private static final String STUB_DATA_UPDATE_CACHE_ENDPOINT=“/ update-cache / stubdata / {id}” ; private static final String STUB_DATA_EVICT_CACHE_ENDPOINT=“/ evict-cache / stubdata / {id}” ; private static final String INVALIDATION_CACHE_NAME=“resource-level-cache” ; private static final String STUB_DATA_ENDPOINT=“/ stubdata / {id}” ;
现在我们来创建一个测试类,并确保所有的工作都像我们想要的那样工作:
包 com.tasosmartidis.caching_demo.web;import org.junit.Ignore;import org.junit.Test;import org.junitnerWith;导入 org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;import static com.tasosmartidis.caching_demo.testutils.HttpMethodsUtils.doGetEnsure200AndReturnResponseAsString;import static com.tasosmartidis.caching_demo.testutils.HttpMethodsUtils.doPutEnsure200AndReturnResponseAsString;import static com.tasosmartidis.caching_demo.utils.LoggingUtils。*;@RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.DEFINED_PORT) public class CacheWithInvalidationServiceTest { private static final Logger LOGGER=LoggerFactory.getLogger(CacheWithInvalidationServiceTest.class); private static final String STUB_DATA_UPDATE_CACHE_ENDPOINT=“/ update-cache / stubdata / 1” ; private static final String STUB_DATA_EVICT_CACHE_ENDPOINT=“/ evict-cache / stubdata / 1” ; private static final String STUB_DATA_ENDPOINT=“/ stubdata / 1” ; @Test
运行测试课程给我们带来了绿色,日志告诉了以下故事:
W上。CacheWithInvalidationServiceTest:测试的方法“缓存驱逐”开始......
Java高级进阶:
1、具有1-5工作经验的,面对目前流行的技术不知从何下手,
需要突破技术瓶颈的可以加。2、在公司待久了,过得很安逸,
但跳槽时面试碰壁。需要在短时间内进修、跳槽拿高薪的可以加。
3、如果没有工作经验,但基础非常扎实,对java工作机制,
常用设计思想,常用java开发框架掌握熟练的,可以加。
4、觉得自己很牛B,一般需求都能搞定。
但是所学的知识点没有系统化,很难在技术领域继续突破的可以加。
5. 群号:高级架构群 469691824备注好信息!
6.阿里Java高级大牛直播讲解知识点,分享知识,
多年工作经验的梳理和总结,带着大家全面、
科学地建立自己的技术体系和技术认知!