高并发服务设计——缓存(转载)

 

参考资料:

https://blog.csdn.net/foreverling/article/details/78012205

https://blog.csdn.net/xiaofei_hah0000/article/details/8993617

1 缓存回收策略

1.1 基于空间

即设置缓存的存储空间,如设置为10MB,当达到存储空间时,按照一定的策略移除数据。

1.2 基于容量

基于容量指缓存设置了最大大小,当缓存的条目超过最大大小,则按照一定的策略将旧数据移除。

1.3 基于时间

TTL(Time To Live):存活期,即缓存数据从缓存中创建时间开始直到它到期的一个时间段(不管在这个时间段内有没有访问都将过期)。

TTI(Time To Idle):空闲期,即缓存数据多久没被访问过将从缓存中移除的时间。

1.4 基于Java对象引用

软引用:如果一个对象是软引用,那么当JVM堆内存不足时,垃圾回收器可以回收这些对象。软引用适合用来做缓存,从而当JVM堆内存不足时,可以回收这些对象腾出一些空间供强引用对象使用,从而避免OOM。

弱引用:当垃圾回收器回收内存时,如果发现弱引用,则将立即回收它。相对于软引用有更短的生命周期。

注意:弱引用/软引用对象只有当没有其他强引用对象引用它时,垃圾回收时才回收该引用。 
即如果有一个对象(不是弱引用/软引用)引用了弱引用/软引用对象,那么垃圾回收是不会回收该引用对象。

1.5 回收算法

使用基于空间和基于容量的缓存会使用一定的策略移除旧数据,常见的如下:

  • FIFO(Fisrt In Fisrt Out):先进先出算法,即先进入缓存的先被移除。
  • LRU(Least Recently Used):最近最少使用算法,使用时间距离现在最久的数据被移除。
  • LFU(Least Frequently Used):最不常用算法,一定时间段内使用次数(频率)最少的数据被移除。

实际应用中基于LRU的缓存较多,如Guava Cache、EhCache支持LRU。

2 Java缓存类型

2.1 堆缓存

使用Java堆内存来存储对象。可以使用Guava Cache、Ehcache 3.x、MapDB实现。

  • 优点:使用堆缓存的好处是没有序列化/反序列化,是最快的缓存;
  • 缺点:很明显,当缓存的数据量很大时, GC暂停时间会变长,存储容量受限于堆空间大小;一般通过软引用/弱引用来存储缓存对象,即当堆内存不足时,可以强制回收这部分内存释放堆内存空间。一般使用堆缓存存储较热的数据。

2.2 堆外缓存

即缓存数据存储在堆外内存。可以使用Ehcache 3.x、MapDB实现。

  • 优点:可以减少GC暂停时间(堆对象转移到堆外,GC扫描和移动的对象变少了),可以支持更大的缓存空间(只受机器内存大小限制,不受堆空间的影响)。
  • 缺点:读取数据时需要序列化/反序列化,会比堆缓存慢很多。

2.3 磁盘缓存

即缓存数据的存储在磁盘上。当JVM重启时数据还是在的。而堆缓存/堆外缓存重启时数据会丢失,需要重新加载。可以使用Ehcache 3.x、MapDB实现。

2.4 分布式缓存

在多JVM实例的情况时,进程内缓存和磁盘缓存会存在两个问题:1.单机容量问题; 2.数据一致性问题(既然数据允许缓存,则表示允许一定时间内的不一致,因此可以设置缓存数据的过期时间来定期更新数据); 3.缓存不命中时,需要回源到DB/服务查询变多:每个实例在缓存不命中情况下都会回源到DB加载数据,因此,多实例后DB整体的访问量就变多了。解决办法可以使用如一致性哈希分片算法来解决。因此,这些情况可以考虑使用分布式缓存来解决。可以使用ehcache-clustered(配合Terracotta server)实现Java进程间分布式缓存。当然也可以使用如Redis实现分布式缓存。

两种模式如下:

  • 单机时:存储最热的数据到堆缓存,相对热的数据到堆外缓存,不热的数据存到磁盘缓存。
  • 集群时:存储最热的数据到堆缓存,相对热的数据到堆外缓存,全量数据存到分布式缓存。

3 Java缓存实现

3.1 堆缓存

3.1.1 Guava Cache实现

Guava Cache只提供堆缓存,小巧灵活,性能最好,如果只使用堆缓存,那么使用它就够了。

Cache<String, String> myCache=
        CacheBuilder.newBuilder()
        .concurrencyLevel(4)
        .expireAfterWrite(10, TimeUnit.SECONDS)
        .maximumSize(10000)
        .build();

然后可以通过put、getIfPresent 来读写缓存。CacheBuilder有几类参数:缓存回收策略、并发设置等。

3.1.1.1 缓存回收策略/基于容量

maximumSize:设置缓存的容量,当超出maximumSize时,按照LRU进行缓存回收。

3.1.1.2 缓存回收策略/基于时间

  • expireAfterWrite:设置TTL,缓存数据在给定的时间内没有写(创建/覆盖)时,则被回收,即定期的会回收缓存数据。
  • expireAfterAccess:设置TTI,缓存数据在给定的时间内没有读/写时,则被回收。每次访问时,都会更新它的TTI,从而如果该缓存是非常热的数据,则将一直不过期,可能会导致脏数据存在很长时间(因此,建议设置expireAfterWrite)。

3.1.1.3 缓存回收策略/基于Java对象引用

weakKeys/weakValues:设置弱引用缓存。 
softValues:设置软引用缓存。

3.1.1.4 缓存回收策略/主动失效

invalidate(Object key)/invalidateAll(Iterablekeys)/invalidateAll():主动失效某些缓存数据。

什么时候触发失效呢? Guava Cache不会在缓存数据失效时立即触发回收操作(如果要这么做,则需要有额外的线程来进行清理),是在PUT时会主动进行一次清理缓存,当然读者也可以根据实际业务通过自己设计线程来调用cleanUp方法进行清理。

3.1.1.5 并发级别

concurrencyLevel:Guava Cache重写了ConcurrentHashMap,concurrencyLevel用来设置Segment数量,concurrencyLevel越大并发能力越强。

3.1.1.6 统计命中率

recordStats:启动记录统计信息,比如命中率等

3.1.2 EhCache 3.x实现

CacheManager cacheManager = CacheManagerBuilder. newCacheManagerBuilder(). build(true);
CacheConfigurationBuilder<String, String> cacheConfig= CacheConfigurationBuilder.newCacheConfigurationBuilder(
       String.class,
       String.class,
       ResourcePoolsBuilder.newResourcePoolsBuilder()
               .heap(100, EntryUnit.ENTRIES))
       .withDispatcherConcurrency(4)
       .withExpiry(Expirations.timeToLiveExpiration(Duration.of(10,TimeUnit.SECONDS)));

Cache<String, String> myCache = cacheManager.createCache("myCache",cacheConfig)

CacheManager在JVM关闭时请调用CacheManager.close()方法。 可以通过PUT、GET来读写缓存。CacheConfigurationBuilder也有几类参数:缓存回收策略、并发设置、统计命中率等。

3.1.2.1 缓存回收策略/基于容量

heap(100, EntryUnit.ENTRIES):设置缓存的条目数量,当超出此数量时按照LRU进行缓存回收。

3.1.2.2 缓存回收策略/基于空间

heap(100, MemoryUnit.MB):设置缓存的内存空间,当超出此空间时按照LRU进行缓存回收。另外,应该设置withSizeOfMaxObjectGraph(2):统计对象大小时对象图遍历深度和withSizeOfMaxObjectSize(1, MemoryUnit.KB):可缓存的最大对象大小。

3.1.2.3 缓存回收策略/基于时间

withExpiry(Expirations.timeToLiveExpiration(Duration.of(10,TimeUnit.SECONDS))):设置TTL,没有TTI。 
withExpiry(Expirations.timeToIdleExpiration(Duration.of(10,TimeUnit.SECONDS))):同时设置TTL和TTI,且TTL和TTI值一样。

3.1.2.4 缓存回收策略/主动失效

remove(K key)/ removeAll(Set keys)/clear():主动失效某些缓存数据。 
什么时候触发失效呢?EhCache使用了类似于Guava Cache同样的机制。

3.1.2.5 并发级别

目前还没有提供API来设置,EhCache内部使用ConcurrentHashMap作为缓存存储,默认并发级别16。withDispatcherConcurrency是用来设置事件分发时的并发级别。

3.1.3 MapDB 3.x 实现

HTreeMap myCache =
       DBMaker.heapDB().concurrencyScale(16).make().hashMap("myCache")
       .expireMaxSize(10000)
       .expireAfterCreate(10, TimeUnit.SECONDS)
       .expireAfterUpdate(10,TimeUnit.SECONDS)
       .expireAfterGet(10, TimeUnit.SECONDS)
       .create();

 

然后可以通过PUT、GET来读写缓存。其有几类参数:缓存回收策略、并发设置、统计命中率等。

3.1.3.1 缓存回收策略/基于容量

expireMaxSize:设置缓存的容量,当超出expireMaxSize时,按照LRU进行缓存回收。

3.1.3.2 缓存回收策略/基于时间

  • expireAfterCreate/expireAfterUpdate:设置TTL,缓存数据在给定的时间内没有写(创建/覆盖)时,则被回收。即定期的会回收缓存数据。
  • expireAfterGet:设置TTI, 缓存数据在给定的时间内没有读/写时,则被回收。每次访问时都会更新它的TTI,从而如果该缓存是非常热的数据,则将一直不过期,可能会导致脏数据存在很长的时间(因此,建议要设置expireAfterCreate/expireAfterUpdate)。

3.1.3.3 缓存回收策略/主动失效

  • remove(Object key) /clear():主动失效某些缓存数据。 
    什么时候触发失效呢? 
    MapDB默认使用类似于Guava Cache的机制。不过,也支持可以通过如下配置使用线程池定期进行缓存失效。
  • expireExecutor(scheduledExecutorService)
  • expireExecutorPeriod(3000)

3.1.3.4 并发级别

concurrencyScale:类似于Guava Cache配置。

还可以使用DBMaker.memoryDB()创建堆缓存,它将数据序列化并存储到1MB大小的byte[]数组中,从而减少垃圾回收的影响。

3.2 堆外缓存

3.2.1 EhCache 3.x实现

CacheConfigurationBuilder<String, String> cacheConfig= CacheConfigurationBuilder.newCacheConfigurationBuilder(
       String.class,
       String.class,
       ResourcePoolsBuilder.newResourcePoolsBuilder()
               .offheap(100, MemoryUnit.MB))
       .withDispatcherConcurrency(4)
       .withExpiry(Expirations.timeToLiveExpiration(Duration.of(10,TimeUnit.SECONDS)))
       .withSizeOfMaxObjectGraph(3)
       .withSizeOfMaxObjectSize(1, MemoryUni

堆外缓存不支持基于容量的缓存过期策略。

3.2.2 MapDB 3.x实现

HTreeMap myCache =
       DBMaker.memoryDirectDB().concurrencyScale(16).make().hashMap("myCache")
       .expireStoreSize(64 * 1024 * 1024) //指定堆外缓存大小64MB
       .expireMaxSize(10000)
       .expireAfterCreate(10, TimeUnit.SECONDS)
       .expireAfterUpdate(10, TimeUnit.SECONDS)
       .expireAfterGet(10, TimeUnit.SECONDS)

Ehcache 3.x还是使用CacheLoaderWriter来实现,通过write(String key, String value)、writeAll(Iterable> entries)和delete(String key)、deleteAll(Iterable keys)分别来支持单个写、批量写和单个删除、批量删除操作。

操作流程如下:当我们调用myCache.put(“e”,”123”)或者myCache.putAll(map)时,写缓存。首先,Cache会将写操作立即委托给CacheLoaderWriter#write和#writeAll,然后由CacheLoaderWriter负责立即去写SoR。当写SoR成功后,再写入Cache。

4.2.3 Write-Behind

Write-Behind,也叫Write-Back,称之为回写模式,不同于Write-Through是同步写SoR和Cache,Write-Behind是异步写。异步之后可以实现批量写、合并写、延时和限流。

4.2.3.1 异步写

略,可用EhCache实现

4.2.3.2 批量写

略,可用EhCache实现

4.2.4 Copy Pattern

有两种Copy Pattern, Copy-On-Read和Copy-On-Write。在Guava-Cache和EhCache中堆缓存都是基于引用的,这样如果哟人拿到缓存数据并修改了它,则可能发生不可预测的问题。Guava Cache没有提供支持,EhCache 3.x提供了支持。

public interface Copier<T> {
    T copyForRead(T obj);    //Copy-On-Read,比如myCache.get()
    T copyForWrite(T obj);   //Copy-On-Write,比如myCache.put()
}
原文地址:https://www.cnblogs.com/guojia314/p/9668758.html