zoukankan      html  css  js  c++  java
  • 高并发服务设计——缓存(转载)

     

    参考资料:

    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()
    }
  • 相关阅读:
    POJ 3258 (NOIP2015 D2T1跳石头)
    POJ 3122 二分
    POJ 3104 二分
    POJ 1995 快速幂
    409. Longest Palindrome
    389. Find the Difference
    381. Insert Delete GetRandom O(1)
    380. Insert Delete GetRandom O(1)
    355. Design Twitter
    347. Top K Frequent Elements (sort map)
  • 原文地址:https://www.cnblogs.com/guojia314/p/9668758.html
Copyright © 2011-2022 走看看