在上一篇文章 万字长文聊缓存(上)中,咱们主要如何围绕着Http作缓存优化,在后端服务器的应用层一样有不少地方能够作缓存,提升服务的效率;本篇咱们就来继续聊聊应用级的缓存。html
缓存的命中率是指从缓存中获取到数据的次数和总读取次数的比率,命中率越高证实缓存的效果越好。这是一个很重要的指标,应该经过监控这个指标来判断咱们的缓存是否设置的合理。git
设置缓存的存储空间,好比:设置缓存的空间是 1G,当达到了1G以后就会按照必定的策略将部分数据移除github
设置缓存的最大条目数,当达到了设置的最大条目数以后按照必定的策略将旧的数据移除web
堆缓存是指把数据缓存在JVM的堆内存中,使用堆缓存的好处是没有序列化和反序列化的操做,是最快的缓存。若是缓存的数据量很大,为了不形成OOM一般状况下使用的时软引用来存储缓存对象;堆缓存的缺点是缓存的空间有限,而且垃圾回收器暂停的时间会变长。redis
Cache<string, string> cache = CacheBuilder.newBuilder() .build();
经过CacheBuilder
构建缓存对象算法
Gauva Cache的主要配置和方法spring
put
: 向缓存中设置key-valueV get(K key, Callable<!--? extends V--> loader)
: 获取一个缓存值,若是缓存中没有,那么就调用loader获取一个而后放入到缓存expireAfterWrite
: 设置缓存的存活期,写入数据后指定时间以后失效expireAfterAccess
: 设置缓存的空闲期,在给定的时间内没有被访问就会被回收maximumSize
: 设置缓存的最大条目数weakKeys/weakValues
: 设置弱引用缓存softValues
: 设置软引用缓存invalidate/invalidateAll
: 主动失效指定key的缓存数据recordStats
: 启动记录统计信息,能够查看到命中率removalListener
: 当缓存被删除的时候会调用此监听器,能够用于查看为何缓存会被删除Caffeine是使用Java8对Guava缓存的重写版本,高性能Java本地缓存组件,也是Spring推荐的堆缓存的实现,与spring的集成能够查看文档https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache-store-configuration-caffeine。数据库
因为是对Guava缓存的重写版本,因此不少的配置参数都是和Guava缓存一致:后端
initialCapacity
: 初始的缓存空间大小maximumSize
: 缓存的最大条数maximumWeight
: 缓存的最大权重expireAfterAccess
: 最后一次写入或访问后通过固定时间过时expireAfterWrite
: 最后一次写入后通过固定时间过时expireAfter
: 自定义过时策略refreshAfterWrite
: 建立缓存或者最近一次更新缓存后通过固定的时间间隔,刷新缓存weakKeys
: 打开key的弱引用weakValues
:打开value的弱引用softValues
:打开value的软引用recordStats
:开启统计功能Caffeine的官方文档:https://github.com/ben-manes/caffeine/wiki缓存
<dependency> <groupid>com.github.ben-manes.caffeine</groupid> <artifactid>caffeine</artifactid> <version>2.8.4</version> </dependency>
public Object manual(String key) { Cache<string, object> cache = Caffeine.newBuilder() .expireAfterAccess(1, TimeUnit.SECONDS) //设置空闲期时长 .maximumSize(10) .build(); return cache.get(key, t -> setValue(key).apply(key)); } public Function<string, object> setValue(String key){ return t -> "https://silently9527.cn"; }
public Object sync(String key){ LoadingCache<string, object> cache = Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(1, TimeUnit.MINUTES) //设置存活期时长 .build(k -> setValue(key).apply(key)); return cache.get(key); } public Function<string, object> setValue(String key){ return t -> "https://silently9527.cn"; }
public CompletableFuture async(String key) { AsyncLoadingCache<string, object> cache = Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(1, TimeUnit.MINUTES) .buildAsync(k -> setAsyncValue().get()); return cache.get(key); } public CompletableFuture<object> setAsyncValue() { return CompletableFuture.supplyAsync(() -> "公众号:贝塔学JAVA"); }
public void removeListener() { Cache<string, object> cache = Caffeine.newBuilder() .removalListener((String key, Object value, RemovalCause cause) -> { System.out.println("remove lisitener"); System.out.println("remove Key:" + key); System.out.println("remove Value:" + value); }) .build(); cache.put("name", "silently9527"); cache.invalidate("name"); }
public void recordStats() { Cache<string, object> cache = Caffeine.newBuilder() .maximumSize(10000) .recordStats() .build(); cache.put("公众号", "贝塔学JAVA"); cache.get("公众号", (t) -> ""); cache.get("name", (t) -> "silently9527"); CacheStats stats = cache.stats(); System.out.println(stats); }
经过 Cache.stats()
获取到CacheStats
。CacheStats
提供如下统计方法:
hitRate()
: 返回缓存命中率evictionCount()
: 缓存回收数量averageLoadPenalty()
: 加载新值的平均时间EhCache 是老牌Java开源缓存框架,早在2003年就已经出现了,发展到如今已经很是成熟稳定,在Java应用领域应用也很是普遍,并且和主流的Java框架好比Srping能够很好集成。相比于 Guava Cache,EnCache 支持的功能更丰富,包括堆外缓存、磁盘缓存,固然使用起来要更重一些。使用 Ehcache 的Maven 依赖以下:
<dependency> <groupid>org.ehcache</groupid> <artifactid>ehcache</artifactid> <version>3.6.3</version> </dependency>
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true); ResourcePoolsBuilder resource = ResourcePoolsBuilder.heap(10); //设置最大缓存条目数 CacheConfiguration<string, string> cacheConfig = CacheConfigurationBuilder .newCacheConfigurationBuilder(String.class, String.class, resource) .withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofMinutes(10))) .build(); Cache<string, string> cache = cacheManager.createCache("userInfo", cacheConfig);
ResourcePoolsBuilder.heap(10)
设置缓存的最大条目数,这是简写方式,等价于ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, EntryUnit.ENTRIES);
ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, MemoryUnit.MB)
设置缓存最大的空间10MBwithExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofMinutes(10)))
设置缓存空闲时间withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(10)))
设置缓存存活时间remove/removeAll
主动失效缓存,与Guava Cache相似,调用方法后不会当即去清除回收,只有在get或者put的时候判断缓存是否过时withSizeOfMaxObjectSize(10,MemoryUnit.KB)
限制单个缓存对象的大小,超过这两个限制的对象则不被缓存堆外缓存即缓存数据在堆外内存中,空间大小只受本机内存大小限制,不受GC管理,使用堆外缓存能够减小GC暂停时间,可是堆外内存中的对象都须要序列化和反序列化,KEY和VALUE必须实现Serializable接口,所以速度会比堆内缓存慢。在Java中能够经过 -XX:MaxDirectMemorySize
参数设置堆外内存的上限
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true); // 堆外内存不能按照存储条目限制,只能按照内存大小进行限制,超过限制则回收缓存 ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder().offheap(10, MemoryUnit.MB); CacheConfiguration<string, string> cacheConfig = CacheConfigurationBuilder .newCacheConfigurationBuilder(String.class, String.class, resource) .withDispatcherConcurrency(4) .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(10))) .withSizeOfMaxObjectSize(10, MemoryUnit.KB) .build(); Cache<string, string> cache = cacheManager.createCache("userInfo2", cacheConfig); cache.put("website", "https://silently9527.cn"); System.out.println(cache.get("website"));
把缓存数据存放到磁盘上,在JVM重启时缓存的数据不会受到影响,而堆缓存和堆外缓存都会丢失;而且磁盘缓存有更大的存储空间;可是缓存在磁盘上的数据也须要支持序列化,速度会被比内存更慢,在使用时推荐使用更快的磁盘带来更大的吞吐率,好比使用闪存代替机械磁盘。
CacheManagerConfiguration<persistentcachemanager> persistentManagerConfig = CacheManagerBuilder .persistence(new File("/Users/huaan9527/Desktop", "ehcache-cache")); PersistentCacheManager persistentCacheManager = CacheManagerBuilder.newCacheManagerBuilder() .with(persistentManagerConfig).build(true); //disk 第三个参数设置为 true 表示将数据持久化到磁盘上 ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder().disk(100, MemoryUnit.MB, true); CacheConfiguration<string, string> config = CacheConfigurationBuilder .newCacheConfigurationBuilder(String.class, String.class, resource).build(); Cache<string, string> cache = persistentCacheManager.createCache("userInfo", CacheConfigurationBuilder.newCacheConfigurationBuilder(config)); cache.put("公众号", "贝塔学JAVA"); System.out.println(cache.get("公众号")); persistentCacheManager.close();
在JVM中止时,必定要记得调用persistentCacheManager.close()
,保证内存中的数据可以dump到磁盘上。
这是典型 heap + offheap + disk 组合的结构图,上层比下层速度快,下层比上层存储空间大,在ehcache中,空间大小设置
heap > offheap > disk
,不然会报错; ehcache 会将最热的数据保存在高一级的缓存。这种结构的代码以下:
CacheManagerConfiguration<persistentcachemanager> persistentManagerConfig = CacheManagerBuilder .persistence(new File("/Users/huaan9527/Desktop", "ehcache-cache")); PersistentCacheManager persistentCacheManager = CacheManagerBuilder.newCacheManagerBuilder() .with(persistentManagerConfig).build(true); ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder() .heap(10, MemoryUnit.MB) .offheap(100, MemoryUnit.MB) //第三个参数设置为true,支持持久化 .disk(500, MemoryUnit.MB, true); CacheConfiguration<string, string> config = CacheConfigurationBuilder .newCacheConfigurationBuilder(String.class, String.class, resource).build(); Cache<string, string> cache = persistentCacheManager.createCache("userInfo", CacheConfigurationBuilder.newCacheConfigurationBuilder(config)); //写入缓存 cache.put("name", "silently9527"); // 读取缓存 System.out.println(cache.get("name")); // 再程序关闭前,须要手动释放资源 persistentCacheManager.close();
前面提到的堆内缓存和堆外缓存若是在多个JVM实例的状况下会有两个问题:1.单机容量毕竟有限;2.多台JVM实例缓存的数据可能不一致;3.若是缓存数据同一时间都失效了,那么请求都会打到数据库上,数据库压力增大。这时候咱们就须要引入分布式缓存来解决,如今使用最多的分布式缓存是redis
当引入分布式缓存以后就能够把应用缓存的架构调整成上面的结构。
缓存使用的模式大概分为两类:Cache-Aside、Cache-As-SoR(SoR表示实际存储数据的系统,也就是数据源)
业务代码围绕着缓存来写,一般都是从缓存中来获取数据,若是缓存没有命中,则从数据库中查找,查询到以后就把数据放入到缓存;当数据被更新以后,也须要对应的去更新缓存中的数据。这种模式也是咱们一般使用最多的。
value = cache.get(key); //从缓存中读取数据 if(value == null) { value = loadFromDatabase(key); //从数据库中查询 cache.put(key, value); //放入到缓存中 }
wirteToDatabase(key, value); //写入到数据库 cache.put(key, value); //放入到缓存中 或者 能够删除掉缓存 cache.remove(key) ,再读取的时候再查一次
Spring的Cache扩展就是使用的Cache-Aside模式,Spring为了把业务代码和缓存的读取更新分离,对Cache-Aside模式使用AOP进行了封装,提供了多个注解来实现读写场景。官方参考文档:
@Cacheable
: 一般是放在查询方法上,实现的就是Cache-Aside
读的场景,先查缓存,若是不存在在查询数据库,最后把查询出来的结果放入到缓存。@CachePut
: 一般用在保存更新方法上面,实现的就是Cache-Aside
写的场景,更新完成数据库后把数据放入到缓存中。@CacheEvict
: 从缓存中删除指定key的缓存> 对于一些容许有一点点更新延迟基础数据能够考虑使用canal订阅binlog日志来完成缓存的增量更新。 > > Cache-Aside还有个问题,若是某个时刻热点数据缓存失效,那么会有不少请求同时打到后端数据库上,数据库的压力会瞬间增大
Cache-As-SoR模式也就会把Cache看作是数据源,全部的操做都是针对缓存,Cache在委托给真正的SoR去实现读或者写。业务代码中只会看到Cache的操做,这种模式又分为了三种
应用程序始终从缓存中请求数据,若是缓存中没有数据,则它负责使用提供的数据加载程序从数据库中检索数据,检索数据后,缓存会自行更新并将数据返回给调用的应用程序。Gauva Cache、Caffeine、EhCache都支持这种模式;
LoadingCache<key, graph> cache = Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(10, TimeUnit.MINUTES) .build(key -> createExpensiveGraph(key)); // Lookup and compute an entry if absent, or null if not computable Graph graph = cache.get(key); // Lookup and compute entries that are absent Map<key, graph> graphs = cache.getAll(keys);
在build Cache的时候指定一个CacheLoader
cache.get(key)
CacheLoader
去数据源中查询数据,以后在放入到缓存,返回给应用程序> CacheLoader
不要直接返回null,建议封装成本身定义的Null对像,在放入到缓存中,能够防止缓存击穿
为了防止由于某个热点数据失效致使后端数据库压力增大的状况,我能够在CacheLoader
中使用锁限制只容许一个请求去查询数据库,其余的请求都等待第一个请求查询完成后从缓存中获取,在上一篇 《万字长文聊缓存(上)》中咱们聊到了Nginx也有相似的配置参数
value = loadFromCache(key); if(value != null) { return value; } synchronized (lock) { value = loadFromCache(key); if(value != null) { return value; } return loadFromDatabase(key); }
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true); ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, MemoryUnit.MB); //设置最大缓存条目数 CacheConfiguration<string, string> cacheConfig = CacheConfigurationBuilder .newCacheConfigurationBuilder(String.class, String.class, resource) .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(10))) .withLoaderWriter(new CacheLoaderWriter<string, string>(){ @Override public String load(String key) throws Exception { //load from database return "silently9527"; } @Override public void write(String key, String value) throws Exception { } @Override public void delete(String key) throws Exception { } }) .build(); Cache<string, string> cache = cacheManager.createCache("userInfo", cacheConfig); System.out.println(cache.get("name"));
在EhCache中使用的是CacheLoaderWriter
来从数据库中加载数据;解决由于某个热点数据失效致使后端数据库压力增大的问题和上面的方式同样,也能够在load
中实现。
和Read Through模式相似,当数据进行更新时,先去更新SoR,成功以后在更新缓存。
Cache<string, string> cache = Caffeine.newBuilder() .maximumSize(100) .writer(new CacheWriter<string, string>() { @Override public void write(@NonNull String key, @NonNull String value) { //write data to database System.out.println(key); System.out.println(value); } @Override public void delete(@NonNull String key, @Nullable String value, @NonNull RemovalCause removalCause) { //delete from database } }) .build(); cache.put("name", "silently9527");
Caffeine经过使用CacheWriter
来实现Write Through,CacheWriter
能够同步的监听到缓存的建立、变动和删除操做,只有写成功了才会去更新缓存
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true); ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, MemoryUnit.MB); //设置最大缓存条目数 CacheConfiguration<string, string> cacheConfig = CacheConfigurationBuilder .newCacheConfigurationBuilder(String.class, String.class, resource) .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(10))) .withLoaderWriter(new CacheLoaderWriter<string, string>(){ @Override public String load(String key) throws Exception { return "silently9527"; } @Override public void write(String key, String value) throws Exception { //write data to database System.out.println(key); System.out.println(value); } @Override public void delete(String key) throws Exception { //delete from database } }) .build(); Cache<string, string> cache = cacheManager.createCache("userInfo", cacheConfig); System.out.println(cache.get("name")); cache.put("website","https://silently9527.cn");
EhCache仍是经过CacheLoaderWriter
来实现的,当咱们调用cache.put("xxx","xxx")
进行写缓存的时候,EhCache首先会将写的操做委托给CacheLoaderWriter
,有CacheLoaderWriter.write
去负责写数据源
这种模式一般先将数据写入缓存,再异步地写入数据库进行数据同步。这样的设计既能够减小对数据库的直接访问,下降压力,同时对数据库的屡次修改能够合并操做,极大地提高了系统的承载能力。可是这种模式也存在风险,如当缓存机器出现宕机时,数据有丢失的可能。
CacheLoaderWriter.write
方法中把数据发送到MQ中,实现异步的消费,这样能够保证数据的安全,可是要想实现合并操做就须要扩展功能更强大的CacheLoaderWriter
。//1 定义线程池 PooledExecutionServiceConfiguration testWriteBehind = PooledExecutionServiceConfigurationBuilder .newPooledExecutionServiceConfigurationBuilder() .pool("testWriteBehind", 5, 10) .build(); CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder() .using(testWriteBehind) .build(true); ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, MemoryUnit.MB); //设置最大缓存条目数 //2 设置回写模式配置 WriteBehindConfiguration testWriteBehindConfig = WriteBehindConfigurationBuilder .newUnBatchedWriteBehindConfiguration() .queueSize(10) .concurrencyLevel(2) .useThreadPool("testWriteBehind") .build(); CacheConfiguration<string, string> cacheConfig = CacheConfigurationBuilder .newCacheConfigurationBuilder(String.class, String.class, resource) .withLoaderWriter(new CacheLoaderWriter<string, string>() { @Override public String load(String key) throws Exception { return "silently9527"; } @Override public void write(String key, String value) throws Exception { //write data to database } @Override public void delete(String key) throws Exception { } }) .add(testWriteBehindConfig) .build(); Cache<string, string> cache = cacheManager.createCache("userInfo", cacheConfig);
首先使用PooledExecutionServiceConfigurationBuilder
定义了线程池配置;而后使用WriteBehindConfigurationBuilder
设置会写模式配置,其中newUnBatchedWriteBehindConfiguration
表示不进行批量写操做,由于是异步写,因此须要把写操做先放入到队列中,经过queueSize
设置队列大小,useThreadPool
指定使用哪一个线程池; concurrencyLevel
设置使用多少个并发线程和队列进行Write Behind
EhCache实现批量写的操做也很容易
newUnBatchedWriteBehindConfiguration()
替换成newBatchedWriteBehindConfiguration(10, TimeUnit.SECONDS, 20)
,这里设置的是数量达到20就进行批处理,若是10秒内没有达到20个也会进行处理CacheLoaderWriter
中实现wirteAll 和 deleteAll进行批处理> 若是须要把对相同的key的操做合并起来只记录最后一次数据,能够经过enableCoalescing()
来启用合并
文中或许会存在或多或少的不足、错误之处,有建议或者意见也很是欢迎你们在评论交流。