缓存的基本思想实际上是以空间换时间。咱们知道,IO的读写速度相对内存来讲是很是比较慢的,一般一个web应用的瓶颈就出如今磁盘IO的读写上。那么,若是咱们在内存中创建一个存储区,将数据缓存起来,当浏览器端由请求到达的时候,直接从内存中获取相应的数据,这样一来能够下降服务器的压力,二来,能够提升请求的响应速度,提高用户体验。java
通常来讲,web应用业务逻辑业务逻辑比较复杂,数据库繁多,要获取某个完整的数据,每每要屡次读取数据库,或者使用极其复杂效率较低的SQL查询语句。为了提升查询的性能,将查询后的数据放到内存中进行缓存,下次查询时,直接从内存缓存直接返回,提升响应效率。mysql
应用层缓存主要针对某个业务方法进行缓存,有些业务对象逻辑比较复杂,,可能涉及到屡次数据库读写或者其余消耗较高的操做,应用层缓存能够将复杂的业务逻辑解放出来,下降服务器压力。web
除了IO外,web应用的另外一大瓶颈就是页面模板的渲染。每次请求都须要从业务逻辑层获取相应的model,并将其渲染成对应的HTML。通常来讲,web应用读取数据的需求比更新数据的需求大不少,大多数状况下,某个请求返回的HTML是同样的,所以直接将HTML缓存起来也是缓存的一个主流作法。spring
代理服务器是浏览器和源服务器之间的中间服务器,浏览器先向这个中间服务器发起Web请求,通过处理后(好比权限验证,缓存匹配等),再将请求转发到源服务器。代理服务器缓存的运做原理跟浏览器的运做原理差很少,只是规模更大。能够把它理解为一个共享缓存,不仅为一个用户服务,通常为大量用户提供服务,所以在减小相应时间和带宽使用方面颇有效,同一个副本会被重用屡次。sql
CDN( Content delivery networks )缓存,也叫网关缓存、反向代理缓存。浏览器先向CDN网关发起Web请求,网关服务器后面对应着一台或多台负载均衡源服务器,会根据它们的负载请求,动态将请求转发到合适的源服务器上。虽然这种架构负载均衡源服务器之间的缓存无法共享,但却拥有更好的处扩展性。数据库
spring做为一个成熟的java web 框架,自身有一套完善的缓存机制,同时,spring还未其余缓存的实现提供了扩展。接下来,让咱们在一个简单的学生管理系统中尝试spring的数据库缓存、应用层缓存、页面缓存的实现。浏览器
本节课咱们来看看一个简单的学生管理系统,改系统使用了Spring+JPA+EhCache的架构对数据库进行了缓存。你们能够直接下载源码进行学习。缓存
测试程序使用了mysql做为数据库,安装好mysql后,创建一个空白的 数据库,例如cache
。服务器
建好数据库后,修改src/main/resources/application.properties
的数据库配置java-web
spring.datasource.url=jdbc:mysql://localhost/cache?useUnicode=true&characterEncoding=utf8 spring.datasource.username=root spring.datasource.password=
该系统利用maven做为构建工具,若是对maven没有了解的同窗能够自行了解一下,咱们会利用maven进行整个项目的构建以及运行。所以须要你们下载安装maven。
安装完成后,打开命令行,进入程序所在目录,输入如下命令:
打开浏览器,访问如下http://localhost:8111/blogs
便可看到最初的博客列表页面
com.tmy.App.java
若是你成功的将项目做为一个maven项目导入进eclipse,直接运行com.tmy.App.java
也能够将项目启动起来。
注意,若是但愿将项目导入进eclipse,须要为eclipse添加maven插件,不然会出现依赖的类找不到的问题。
如下是程序所提供的全部页面以及相关说明:
http://localhost:8111/blogs //没有加缓存的博客列表页面 http://localhost:8111/blogs/dao //添加了数据层缓存 http://localhost:8111/blogs/service?test=test //添加了服务层缓存 http://localhost:8111/blogs/service/update?test=test //更新服务层缓存 http://localhost:8111/blogs/service/evict?test=test //删除服务层缓存 http://localhost:8111/blogs/service/test?test=test //删除服务层缓存的同时更新缓存 http://localhost:8111/blogs/page //添加了页面缓存 http://localhost:8111/blogs/page/update //清空页面缓存 http://localhost:8111/blogs/page/delete //清空页面缓存
maven是目前主流java的构建工具之一,若是对maven没有了解的同窗能够自行了解一下,接下来咱们会利用maven进行整个项目的构建以及运行。
spring boot是spring的一个子项目,其目的是spring应用的初始搭建以及开发过程,若是你想本身搭建一个基于spring的应用,强烈建议学习一下在《java web 全栈开发》这门课程,教你如何从对spring零基础到搭建好一个完整的spring web应用。这里,咱们只需知道mvn spring-boot:run
命令能够将系统run起来便可。
Spring做为目前主流的java web框架,你们应该都很了解,这里不作过多介绍。
JPA全称Java Persistence API,JPA经过JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。本门课程主要讲基于spring的数据库缓存,对于JPA的内容不作过多的涉及。
EhCache 是一个纯Java的进程内缓存框架,具备快速、精干等特色。咱们的学生管理系统将利用EhCache对数据库层进行缓存。
上一节咱们讲到不少技术,这里咱们主要的依赖是指对EhCache的依赖,须要在Spring项目中引入EhCache,在pom.xml
中加入如下代码便可:
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-ehcache</artifactId> </dependency>
在src/main/resources
下添加文件ehcache.xml
:
<?xml version="1.0" encoding="UTF-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" name="CM1" updateCheck="false" maxBytesLocalHeap="16M"> <diskStore path="/data/app/cache/ehcache"/> <defaultCache eternal="false" overflowToDisk="false" maxElementsInMemory="10000" timeToIdleSeconds="3600" timeToLiveSeconds="36000" /> </ehcache>
encache能够对如下参数进行配置:
缓存名称
内存中最大缓存对象数
硬盘中最大缓存对象数,如果0表示无穷大
true表示对象永不过时,此时会忽略timeToIdleSeconds和timeToLiveSeconds属性,默认为false
true表示当内存缓存的对象数目达到了maxElementsInMemory界限后,会把溢出的对象写到硬盘缓存中。注意:若是缓存的对象要写入到硬盘中的话,则该对象必须实现了Serializable接口才行。
磁盘缓存区大小,默认为30MB。每一个Cache都应该有本身的一个缓存区。
是否缓存虚拟机重启期数据
磁盘失效线程运行时间间隔,默认为120秒
设定容许对象处于空闲状态的最长时间,以秒为单位。当对象自从最近一次被访问后,若是处于空闲状态的时间超过了timeToIdleSeconds属性值,这个对象就会过时,EHCache将把它从缓存中清空。只有当eternal属性为false,该属性才有效。若是该属性值为0,则表示对象能够无限期地处于空闲状态
设定对象容许存在于缓存中的最长时间,以秒为单位。当对象自从被存放到缓存中后,若是处于缓存中的时间超过了 timeToLiveSeconds属性值,这个对象就会过时,EHCache将把它从缓存中清除。只有当eternal属性为false,该属性才有效。若是该属性值为0,则表示对象能够无限期地存在于缓存中。timeToLiveSeconds必须大于timeToIdleSeconds属性,才有意义
当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。
首先,咱们要经过@EnableCaching
标注将Spring经过标注进行缓存管理的功能打开,以方便咱们以后经过标注添加数据库缓存。
而后,为CacheConfiguration添加@Configuration
标注,打开CacheConfiguration内@Bean
的功能。
生成一个CacheManager
的实例。
最后,在web app销毁的时候销毁cacheManager。
@Configuration @EnableCaching public class CacheConfiguration { private net.sf.ehcache.CacheManager cacheManager; @PreDestroy public void destroy() { cacheManager.shutdown(); } @Bean public CacheManager cacheManager() { cacheManager = net.sf.ehcache.CacheManager.create(); EhCacheCacheManager ehCacheManager = new EhCacheCacheManager(); ehCacheManager.setCacheManager(cacheManager); return ehCacheManager; } }
首先,咱们须要在EhCache中设置一块区域来存放缓存,在src/main/resources/ehcache.xml
中添加以下配置:
Hibernate提供了两级缓存,第一级是Session的缓存。因为Session对象的生命周期一般对应一个数据库事务或者一个应用事务,所以它的缓存是事务范围的缓存。第一级缓存是必需的,hibernate会默认提供好。
第二级缓存是一个可插拔的的缓存插件,它是由SessionFactory负责管理。因为SessionFactory对象的生命周期和应用程序的整个过程对应,所以第二级缓存是进程范围或者集群范围的缓存。这个缓存中存放的对象的松散数据第二级缓存是可选的,能够在每一个类或每一个集合的粒度上配置第二级缓存。
咱们能够经过为entry对象添加标注的方式打开二级缓存:
二级缓存一共有如下5种策略:
不使用缓存,默认的缓存策略
只读模式,在此模式下,若是对数据进行更新操做,会有异常
读写模式在更新缓存的时候会把缓存里面的数据换成一个锁,其它事务若是去取相应的缓存数据,发现被锁了,直接就去数据库查询
不严格的读写模式则不会的缓存数据加锁
事务模式指缓存支持事务,当事务回滚时,缓存也能回滚
而后,在src/main/resources/application.properties
中为cache指定一个factory:
第一次访问http://localhost:8111/blogs
时,waiting也就是服务器响应的时间为2.82秒,耗时较多。
注意:这里消耗2.82秒的缘由是:在Blog
对象中添加了对成员creator
添加了@ManyToOne
的标注,所以,当经过JPA获取blog对象后,JPA还会请求一次SQL查询,去user表中获取user信息,将user填充进来,而为了效果更加明显,系统在添加测试数据时为每一个blog都添加了不一样的user,致使sql请求大大增长,处理时间也大大增长
屡次访问http://localhost:8111/blogs
后,服务器响应时间大大减小,基本保持在700毫秒左右:
这是由于mysql实际上帮咱们作了缓存的工做,所以,屡次访问后,服务器响应时间会大大减小。若是你们有兴趣,能够自行搜索mysql缓存相关的内容。
那么,在屡次访问http://localhost:8111/blogs/dao
后,访问时间基本保持在100多毫秒,比没有缓存的页面效率高了5倍左右,比第一次访问效率高了20倍以上。
Spring 提供了一套标注来保住咱们快速的实现缓存系统:
@Cacheable
触发添加缓存的方法@CacheEvict
触发删除缓存的方法@CachePut
在不干涉方法执行的状况下更新缓存@Caching
组织多个缓存标注的标注@CacheConfig
在class的层次共享缓存的设置接下来咱们来看缓存的具体实现。
和数据层缓存同样,须要在内存中设置一块区域来存放service的缓存,在src/main/resources/ehcache.xml
中添加以下配置:
首先,在BlogWithCacheService
上添加@CacheConfig(cacheNames = "com.tmy.service.allBlogs")
标注,代表在BlogWithCacheService
中的方法的缓存都是放在com.tmy.service.allBlogs
区域中。
在须要缓存的方法上添加@Cacheable
标注:
@Cacheable(key = "#justTest") public List<BlogWithoutCache> findAll(String justTest){ return blogRepository.findAll(); }
当第一次调用该方法后,其返回值就会添加进缓存当中,当第二次调用时就能直接从缓存中获取对象了。为了测试缓存功能,咱们为findAll方法添加了一个参数,这里咱们将这个参数做为缓存的key。除了用参数以外,Spring还提供了其余解析方式来生成key:
#root.methodName
#root.method.name
#root.target
#root.targetClass
#root.args[0]
#root.caches[0].name
#arg
unless
参数或者@CachePut
标注中才能使用) #result
添加进缓存后,在update方法中添加@CachePut
标注能够更新相应的缓存,一样,咱们仍是使用传进来的参数来更新相应的缓存:
@CachePut(key = "#justTest") public List<BlogWithoutCache> updateAll(String justTest){ BlogWithoutCache blog = new BlogWithoutCache(); blog.setContent("这是不存在的博客"); blog.setTitle("谨慎使用这个方法"); return Lists.newArrayList(blog); }
在某些状况下,咱们还须要删除缓存,@CacheEvict
能够干这件事情:
若是你想在一个方法中同时对缓存作多种操做,Spring支持使用@Caching
来组织这些操做:
@Caching(evict = @CacheEvict(key="#justTest"), put = @CachePut(key="test")) public List<BlogWithoutCache> testForCaching(String justTest){ BlogWithoutCache blog = new BlogWithoutCache(); blog.setContent("这是不存在的博客"); blog.setTitle("谨慎使用这个方法"); return Lists.newArrayList(blog); }
在屡次访问http://localhost:8111/blogs/service?test=test
后,服务器的访问时间基本保持在100毫秒如下,根据上次实验能够发现,其效率甚至比加了数据层缓存后还要高。
更新缓存前,访问http://localhost:8111/blogs/service?test=test
页面,看下如下博客:
访问http://localhost:8111/blogs/service/update?test=test
更新缓存,再次访问http://localhost:8111/blogs/service?test=test
,将发现数据库没有变化,可是返回的博客列表发生了变化:
如今缓存对象已经被玩坏了,让咱们访问http://localhost:8111/blogs/service/evict?test=test
缓存的对象给删掉,再次访问http://localhost:8111/blogs/service/update?test=test
,咱们发现博客列表从新变为正确的列表,同时服务器响应时间变成和没有作缓存时一致:
一样,第一件事情让咱们添加一下缓存的空间:
ehcache为咱们提供了几个缓存页面的filter,使用这些filter实现缓存:
最基本的页面缓存filter实现,其知足大部分页面缓存的需求,该filter只缓存页面,不会修改herder的 ETag、Last-Modified、Expires属性
当response没有提交时写入缓存,不然不写缓存,该缓存可能致使空白页的错误,须要特别注意!
专门针对那些不独立存在,只是被include到其余页面的页面缓存
SimplePageCachingFilter的扩展,会填写herder的 ETag、Last-Modified、Expires属性,能够进一步减小浏览器的访问次数
以上filter会在filter初始化的时候经过FilterConfig
对缓存进行初始化,为了在SpringBoot中方便的经过注解去实例化这些Filter,咱们将CacheName
的获取作一个定制:
public class CustomPageCachingFilter extends SimpleCachingHeadersPageCachingFilter { private final String customCacheName; public CustomPageCachingFilter(String name){ this.customCacheName = name; } @Override protected String getCacheName() { return customCacheName; } }
这样,咱们就能很方便的注入cacheName了。
EhCache只提供了添加缓存的Filter,可是并无提供删除缓存的Filter,不要紧,让咱们来本身实现一个:
public class ClearPageCachingFilter implements Filter { private final CacheManager cacheManager; private final String customCacheName; public ClearPageCachingFilter(String name){ this.customCacheName = name; cacheManager = CacheManager.getInstance(); assert cacheManager != null; } @Override public void init(FilterConfig filterConfig) throws ServletException {} @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { Ehcache ehcache = cacheManager.getEhcache(customCacheName); ehcache.removeAll(); } @Override public void destroy() {} }
现实状况URL的设计是极其复杂的,咱们在这里就简单粗暴的将全部cache直接删除,若是缓存设计的比较好,最好能够经过ehcache.remove(key);
的方式对cache进行管理。
咱们目前使用标注的方式对Filter以及Filter mapping进行管理,目前咱们只缓存/blogs/page
这一个页面:
@Configuration @AutoConfigureAfter(CacheConfiguration.class) public class PageCacheConfiguration { @Bean public FilterRegistrationBean registerBlogsPageFilter(){ CustomPageCachingFilter customPageCachingFilter = new CustomPageCachingFilter("com.tmy.mapper.allBlogs"); FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); filterRegistrationBean.setFilter(customPageCachingFilter); filterRegistrationBean.setUrlPatterns(Lists.newArrayList("/blogs/page")); return filterRegistrationBean; } @Bean public FilterRegistrationBean registerClearBlogsPageFilter(){ ClearPageCachingFilter clearPageCachingFilter = new ClearPageCachingFilter("com.tmy.mapper.allBlogs"); FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); filterRegistrationBean.setFilter(clearPageCachingFilter); filterRegistrationBean.setUrlPatterns(Lists.newArrayList("/blogs/page/update", "/blogs/page/delete")); return filterRegistrationBean; } }
从以上配置能够看出,咱们为/blogs/page
注册了一个添加缓存的Filter,/blogs/page
请求将被缓存到内存当中。同时,为/blogs/page/update
以及/blogs/page/delete
注册了清空缓存的Filter,当访问这两个url时,将清空全部的缓存。
访问http://localhost:8111/blogs/page
,刷新,咱们能够看到,服务器的响应时间只须要4毫秒,是mysql缓存、数据层缓存、服务层缓存当中最好的。
咱们能够将以上几种缓存结合起来一块儿使用,http://localhost:8111/blogs/page
,该请求已经结合了以上三种缓存的实现。所以,当咱们访问http://localhost:8111/blogs/page/update
清空页面缓存时,再次访问http://localhost:8111/blogs/page
也只须要100多毫秒,此时页面缓存没有命中,可是service层缓存命中。
就实践看来,数据层缓存、服务层缓存、页面缓存一层比一层更加高效,可是因为其实现愈来愈复杂,须要考虑的状况也愈来愈多,所以,其设计也愈来愈复杂。
从服务层缓存的实现@CachePut
实现来看,在这一层须要咱们配置的东西愈来愈多,已经有很大可能出现数据不一致的现象。而页面缓存的复杂性相对服务层缓存又高了一个层级,所以在针对缓存进行设计的时候,不只仅考虑缓存所带来的性能提高,还要考虑到更新缓存所带来的性能损失。并且在实践当中,不是数据层缓存、服务层缓存、页面缓存越多越好,须要根据实际状况作出选择。