高并发服务设计—缓存
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只提供堆缓存,小巧灵活,性能最好,如果只使用堆缓存,那么使用它就够了。 CachemyCache= 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实现 CacheManagercacheManager=CacheManagerBuilder.newCacheManagerBuilder().build(true); CacheConfigurationBuildercacheConfig=CacheConfigurationBuilder.newCacheConfigurationBuilder( String.class, String.class, ResourcePoolsBuilder.newResourcePoolsBuilder() .heap(100,EntryUnit.ENTRIES)) .withDispatcherConcurrency(4) .withExpiry(Expirations.timeToLiveExpiration(Duration.of(10,TimeUnit.SECONDS))); CachemyCache=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 实现 HTreeMapmyCache= DBMaker.heapDB().concurrencyScale(16).make().hashMap("myCache") .expireMaxSize(10000) .expireAfterCreate(10,TimeUnit.SECONDS) .expireAfterUpdate(10,TimeUnit.SECONDS) .expireAfterGet(10,TimeUnit.SECONDS) .create();1234567 然后可以通过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实现 CacheConfigurationBuildercacheConfig=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,MemoryUnit.KB); 堆外缓存不支持基于容量的缓存过期策略。 3.2.2 MapDB 3.x实现 HTreeMapmyCache= 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) .create(); 在使用堆外缓存时,请记得添加JVM启动参数,如-XX:MaxDirectMemorySize=10G。 3.3 磁盘缓存 3.3.1 EhCache 3.x实现 CacheManagercacheManager=CacheManagerBuilder.newCacheManagerBuilder() //默认线程池 .using(PooledExecutionServiceConfigurationBuilder.newPooledExecutionServiceConfigurationBuilder().defaultPool("default",1,10).build()) //磁盘文件存储位置 .with(newCacheManagerPersistenceConfiguration(newFile("D:\\bak"))) .build(true); CacheConfigurationBuildercacheConfig=CacheConfigurationBuilder.newCacheConfigurationBuilder( String.class, String.class, ResourcePoolsBuilder.newResourcePoolsBuilder() .disk(100,MemoryUnit.MB,true))//磁盘缓存 .withDiskStoreThreadPool("default",5)//使用"default"线程池进行dump文件到磁盘 .withExpiry(Expirations.timeToLiveExpiration(Duration.of(50,TimeUnit.SECONDS))) .withSizeOfMaxObjectGraph(3) .withSizeOfMaxObjectSize(1,MemoryUnit.KB); 在JVM停止时,记得调用cacheManager.close(),从而保证内存数据能dump到磁盘。 3.3.2 MapDB 3.x实现 DBdb=DBMaker .fileDB("D:\\bak\\a.data")//数据存哪里 .fileMmapEnable()//启用mmap .fileMmapEnableIfSupported()//在支持的平台上启用mmap .fileMmapPreclearDisable()//让mmap文件更快 .cleanerHackEnable()//一些BUG处理 .transactionEnable()//启用事务 .closeOnJvmShutdown() .concurrencyScale(16) .make(); HTreeMapmyCache=db.hashMap("myCache") .expireMaxSize(10000) .expireAfterCreate(10,TimeUnit.SECONDS) .expireAfterUpdate(10,TimeUnit.SECONDS) .expireAfterGet(10,TimeUnit.SECONDS) .createOrOpen(); 因为开启了事务,MapDB则开启了WAL。另外,操作完缓存后记得调用db.commit方法提交事务。 myCache.put("key"+counterWriter,"value"+counterWriter); db.commit(); 3.4 分布式缓存 3.4.1 Ehcache 3.1 + Terracotta Server 不建议使用。 3.4.2 Redis 性能非常好,有主从模式、集群模式。 3.5 多级缓存 如先查找堆缓存,如果没有查找磁盘缓存,则使用MapDB可以通过如下配置实现。 HTreeMapdiskCache=db.hashMap("myCache") .expireStoreSize(8*1024*1024*1024) .expireMaxSize(10000) .expireAfterCreate(10,TimeUnit.SECONDS) .expireAfterUpdate(10,TimeUnit.SECONDS) .expireAfterGet(10,TimeUnit.SECONDS) .createOrOpen(); HTreeMapheapCache=db.hashMap("myCache") .expireMaxSize(100) .expireAfterCreate(10,TimeUnit.SECONDS) .expireAfterUpdate(10,TimeUnit.SECONDS) .expireAfterGet(10,TimeUnit.SECONDS) .expireOverflow(diskCache)//当缓存溢出时存储到disk .createOrOpen(); 4 缓存使用模式 主要分两大类:Cache-Aside和Cache-As-SoR(Read-through、Write-through、Write-behind) SoR(system-of-record):记录系统,或者可以叫做数据源,即实际存储原始数据的系统。 Cache:缓存,是SoR的快照数据,Cache的访问速度比SoR要快,放入Cache的目的是提升访问速度,减少回源到SoR的次数。 回源:即回到数据源头获取数据,Cache没有命中时,需要从SoR读取数据,这叫做回源。 4.1 Cache-Aside Cache-Aside 即业务代码围绕着Cache写,是由业务代码直接维护缓存,示例代码如下所示。 4.1.1 读场景 先从缓存获取数据,如果没有命中,则回源到SoR并将源数据放入缓存供下次读取使用。 //1、先从缓存中获取数据 value=myCache.getIfPresent(key); if(value==null){ //2.1、如果缓存没有命中,则回源到SoR获取源数据 value=loadFromSoR(key); //2.2、将数据放入缓存,下次即可从缓存中获取数据 myCache.put(key,value); } 4.1.2 写场景 先将数据写入SoR,写入成功后立即将数据同步写入缓存。 //1、先将数据写入SoR writeToSoR(key,value); //2、执行成功后立即同步写入缓存 myCache.put(key,value); 或者先将数据写入SoR,写入成功后将缓存数据过期,下次读取时再加载缓存。 //1、先将数据写入SoR writeToSoR(key,value); //2、失效缓存,然后下次读时再加载缓存 myCache.invalidate(key); Cache-Aside适合使用AOP模式去实现 4.2 Cache-As-SoR Cache-As-SoR即把Cache看作为SoR,所有操作都是对Cache进行,然后Cache再委托给SoR进行真实的读/写。即业务代码中只看到Cache的操作,看不到关于SoR相关的代码。有三种实现:read-through、write-through、write-behind。 4.2.1 Read-Through Read-Through,业务代码首先调用Cache,如果Cache不命中由Cache回源到SoR,而不是业务代码(即由Cache读SoR)。使用Read-Through模式,需要配置一个CacheLoader组件用来回源到SoR加载源数据。Guava Cache和Ehcache 3.x都支持该模式。 4.2.1.1 Guava Cache实现 LoadingCachegetCache= CacheBuilder.newBuilder() .softValues() .maximumSize(5000).expireAfterWrite(2,TimeUnit.MINUTES) .build(newCacheLoader(){ @Override publicResultload(finalIntegersortId)throwsException{ returncategoryService.get(sortId); } }); 在build Cache时,传入一个CacheLoader用来加载缓存,操作流程如下: 应用业务代码直接调用getCache.get(sortId)。 首先查询Cache,如果缓存中有,则直接返回缓存数据。 如果缓存没有命中,则委托给CacheLoader,CacheLoader会回源到SoR查询源数据(返回值必须不为null,可以包装为Null对象),然后写入缓存。 使用CacheLoader后有几个好处: 应用业务代码更简洁了,不需要像Cache-Aside模式那样缓存查询代码和SoR代码交织在一起。如果缓存使用逻辑散落在多处,则使用这种方式很简单的消除了重复代码。 解决Dog-pile effect,即当某个缓存失效时,又有大量相同的请求没命中缓存,从而同时请求到后端,导致后端压力太大,此时限制一个请求去拿即可。 4.2.1.2 Ehcache 3.x实现 CacheManagercacheManager=CacheManagerBuilder.newCacheManagerBuilder().build(true); org.ehcache.CachemyCache=cacheManager.createCache("myCache", CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class,String.class, ResourcePoolsBuilder.newResourcePoolsBuilder().heap(100,MemoryUnit.MB)) .withDispatcherConcurrency(4) .withExpiry(Expirations.timeToLiveExpiration(Duration.of(10,TimeUnit.SECONDS))) .withLoaderWriter(newDefaultCacheLoaderWriter(){ @Override publicStringload(Stringkey)throwsException{ returnreadDB(key); } @Override publicMaploadAll(Iterablekeys)throwsBulkCacheLoadingException,Exception{ returnnull; } })); Ehcache 3.1没有自己去解决Dog-pile effect。 4.2.2 Write-Through Write-Through,称之为穿透写模式/直写模式,业务代码首先调用Cache写(新增/修改)数据,然后由Cache负责写缓存和写SoR,而不是业务代码。 使用Write-Through模式需要配置一个CacheWriter组件用来回写SoR。Guava Cache没有提供支持。Ehcache 3.x支持该模式。 Ehcache需要配置一个CacheLoaderWriter,CacheLoaderWriter知道如何去写SoR。当Cache需要写(新增/修改)数据时,首先调用CacheLoaderWriter来同步(立即)到SoR,成功后会更新缓存。 CacheManagercacheManager=CacheManagerBuilder.newCacheManagerBuilder().build(true); CachemyCache=cacheManager.createCache("myCache", CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class,String.class, ResourcePoolsBuilder.newResourcePoolsBuilder().heap(100,MemoryUnit.MB)) .withDispatcherConcurrency(4) .withExpiry(Expirations.timeToLiveExpiration(Duration.of(10,TimeUnit.SECONDS))) .withLoaderWriter(newDefaultCacheLoaderWriter(){ @Override publicvoidwrite(Stringkey,Stringvalue)throwsException{ //write } @Override publicvoidwriteAll(Iterableentries)throwsBulkCacheWritingException,Exception{ for(Objectentry:entries){ //batchwrite } } @Override publicvoiddelete(Stringkey)throwsException{ //delete } @Override publicvoiddeleteAll(Iterablekeys)throwsBulkCacheWritingException,Exception{ for(Objectkey:keys){ //batchdelete } } }).build()); 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提供了支持。 publicinterfaceCopier{ TcopyForRead(Tobj);//Copy-On-Read,比如myCache.get() TcopyForWrite(Tobj);//Copy-On-Write,比如myCache.put() } 本文转载自http://blog.csdn.net/foreverling/article/details/78012205