摘要 在上一篇文章 万字长文聊缓存(上)中,我们主要如何围绕着Http做缓存优化,在后端服务器的应用层同样有很多地方可以做缓存,提高服务的效率;本篇我们就来继续聊聊应用级的缓存。 缓存的命中率 缓存的命中率是指从缓存中获取到数据的次数和总读取次数的比率,命中率越高证明缓存的效果越好。这是一个很重要的指标,应该通过监控这个指标来判断我们的缓存是否设置的合理。 缓存的回收策略 基于时间 存活期:在设置缓存的同时设置该缓存可以存活多久,不论在存活期内被访站长交易问了多少次,时间到了都会过期 空闲期:是指缓存的数据多久没有被访问就过期 基于空间 设置缓存的存储空间,比如:设置缓存的空间是 1G,当达到了1G之后就会按照一定的策略将部分数据移除 基于缓存数量 设置缓存的最大条目数,当达到了设置的最大条目数之后按照一定的策略将旧的数据移除 基于Java对象引用 弱引用:当垃圾回收器开始回收内存的时候,如果发现了弱引用,它将立即被回收。 软引用:当垃圾回收器发现内存已不足的情况下会回收软引用的对象,从而腾出一下空间,防止发生内存溢出。软引用适合用来做堆缓存 缓存的回收算法 FIFO 先进先出算法 LRU 最近最少使用算法 LFU 最不常用算法 Java缓存的类型 堆缓存 堆缓存是指把数据缓存在JVM的堆内存中,使用堆缓存的好处是没有序列化和反序列化的操作,是最快的缓存。如果缓存的数据量很大,为了避免造成OOM通常情况下使用的时软引用来存储缓存对象;堆缓存的缺点是缓存的空间有限,并且垃圾回收器暂停的时间会变长。 Gauva Cache实现堆缓存 Cache<string, string> cache = CacheBuilder.newBuilder() .build(); 通过CacheBuilder构建缓存对象 Gauva Cache的主要配置和方法 put : 向缓存中设置key-value V get(K key, Callable<!--? extends V--> loader) : 获取一个缓存值,如果缓存中没有,那么就调用loader获取一个然后放入到缓存 expireAfterWrite : 设置缓存的存活期,写入数据后指定时间之后失效 expireAfterAccess : 设置缓存的空闲期,在给定的时间内没有被访问就会被回收 maximumSize : 设置缓存的最大条目数 weakKeys/weakValues : 设置弱引用缓存 softValues : 设置软引用缓存 invalidate/invalidateAll: 主动失效指定key的缓存数据 recordStats : 启动记录统计信息,可以查看到命中率 removalListener : 当缓存被删除的时候会调用此监听器,可以用于查看为什么缓存会被删除 Caffeine实现堆缓存 Caffeine是使用Java8对Guava缓存的重写版本,高性能Java本地缓存组件,也是Spring推荐的堆缓存的实现,与spring的集成可以查看文档 由于是对Guava缓存的重写版本,所以很多的配置参数都是和Guava缓存一致: initialCapacity: 初始的缓存空间大小 maximumSize: 缓存的最大条数 maximumWeight: 缓存的最大权重 expireAfterAccess: 最后一次写入或访问后经过固定时间过期 expireAfterWrite: 最后一次写入后经过固定时间过期 expireAfter : 自定义过期策略 refreshAfterWrite: 创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存 weakKeys: 打开key的弱引用 weakValues:打开value的弱引用 softValues:打开value的软引用 recordStats:开启统计功能 Caffeine的官方文档: pom.xml中添加依赖 <dependency> <groupid>com.github.ben-manes.caffeine</groupid> <artifactid>caffeine</artifactid> <version>2.8.4</version></dependency> Caffeine Cache提供了三种缓存填充策略:手动、同步加载和异步加载。 手动加载:在每次get key的时候指定源码交易一个同步的函数,如果key不存在就调用这个函数生成一个值 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://"; } 同步加载:构造Cache时候,build方法传入一个CacheLoader实现类。实现load方法,通过key加载value。 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://"; } 异步加载:AsyncLoadingCache是继承自LoadingCache类的,异步加载使用Executor去调用方法并返回一个CompletableFuture 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实现堆缓存 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)设置缓存最大的空间10MB withExpiry(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://"); 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到磁盘上。 |
|