应用级缓存——《亿级流量》

栏目: 数据库 · 发布时间: 8年前

内容简介:应用级缓存——《亿级流量》

缓存简介

缓存,笔者的理解是让数据更接近于使用者,目的是让访问速度更多。工作机制是先从缓存中读取数据,如果没有,则再从慢速设备上读取实际数据并同步到缓存。那些经常读取的数据、频繁访问的数据、热点数据、IO瓶颈数据、计算昂贵的数据、符合五分钟法则和局部性原理的数据都可以进行缓存。如CPU→L1/L2/L3→内存→磁盘就是一个典型的例子,CPU需要数据时先从L1读取,如果没有找到,则查找L2/L3读取,如果没有,则到内存中查找,如果还没有,则会到磁盘中查找。还有比如用过Maven的朋友都应该知道,加载依赖的时候,先从本机仓库找,再从本地服务器仓库找,最后到远程仓库服务器找。还有如京东的物流为什么那么快?他们在各地都有分仓库,如果该仓库有货物,那么送货的速度是非常快的。

本文以 Java 应用缓存为示例进行讲解。

缓存命中率

缓存命中率是从缓存中读取数据的次数与总读取次数的比率,命中率越高越好。缓存命中率 = 从缓存中读取次数/〔总读取次数(从缓存中读取次数 + 从慢速设备上读取的次数)〕。这是一个非常重要的监控指标,如果做缓存,则应通过监控这个指标来看缓存是否工作良好。

缓存回收策略

基于空间

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

基于容量

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

基于时间

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

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

基于Java对象引用

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

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

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

回收算法

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

FIFO(First In First Out):先进先出算法,即先放入缓存的先被移除。

LRU(Least Recently Used):最近最少使用算法,使用时间距离现在最久的那个被移除。

LFU(Least Frequently Used):最不常用算法,一定时间段内使用次数(频率)最少的那个被移除。

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

Java缓存类型

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

堆外缓存:即缓存数据存储在堆外内存,可以减少GC暂停时间(堆对象转移到堆外,GC扫描和移动的对象变少了),可以支持更大的缓存空间(只受机器内存大小限制,不受堆空间的影响)。但是,读取数据时需要序列化/反序列化,因此,会比堆缓存慢很多。可以使用Ehcache 3.x、MapDB实现。

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

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

应用级缓存——《亿级流量》

两种模式如下。

单机时 :存储最热的数据到堆缓存,相对热的数据到堆外缓存,不热的数据存到磁盘缓存。

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

接下来,我们看看如何在Java中使用堆缓存、堆外缓存、磁盘缓存、分布式缓存,是不是感觉像L1、L2、L3级缓存架构。

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

EhCache3.x提供了堆缓存、堆外缓存、磁盘缓存、分布式缓存。但是,其代码注释比较少,API还不完善(比如,2.x支持LRU、LFU、FIFO,而3.x目前还没有API设置),功能还不完善(比如,集群情况个人测试其暂时不可以生产环境使用),如果需要较稳定的API和功能,则请考虑使用EhCache2.x(不支持堆外缓存)。

MapDB是一款嵌入式Java数据库引擎和集合框架。提供了Maps、Sets、Lists、Queues、Bitmaps的支持,还支持ACID事务,增量备份。支持堆缓存、堆外缓存、磁盘缓存。


堆缓存

Gauva Cache实现

Cache<String, String> myCache=

CacheBuilder.newBuilder()

.concurrencyLevel(4)

.expireAfterWrite(10, TimeUnit.SECONDS)

.maximumSize(10000)

.build();

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

1.缓存回收策略/基于容量

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

2.缓存回收策略/基于时间

expireAfterWrite:设置TTL,缓存数据在给定的时间内没有写(创建/覆盖)时,则被回收,即定期的会回收缓存数据。

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

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

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

softValues:设置软引用缓存。

4.缓存回收策略/主动失效

invalidate(Object key)/ invalidateAll(Iterable<?>keys)/invalidateAll():主动失效某些缓存数据。

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

5.并发级别

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

6.统计命中率

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

7.EhCache 3.x实现

本文使用最新的Ehcache3.1.2,目前Ehcache3.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也有几类参数:缓存回收策略、并发设置、统计命中率等。

8.缓存回收策略/基于容量

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

9.缓存回收策略/基于空间

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

10.缓存回收策略/基于时间

withExpiry(Expirations.timeToLiveExpiration(Duration.of(10,TimeUnit.SECONDS))):设置TTL,没有TTI。

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

11.缓存回收策略/主动失效

remove(K key)/ removeAll(Set<? extends K> keys)/clear():主动失效某些缓存数据。

什么时候触发失效呢?EhCache使用了类似于Guava Cache同样的机制。

12.并发级别

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

13.统计命中率

目前还没有开放API来统计。

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来读写缓存。其有几类参数:缓存回收策略、并发设置、统计命中率等。

1.缓存回收策略/基于容量

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

2.缓存回收策略/基于时间

expireAfterCreate/expireAfterUpdate:设置TTL,缓存数据在给定的时间内没有写(创建/覆盖)时,则被回收。即定期的会回收缓存数据。

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

3.缓存回收策略/主动失效

remove(Object key) /clear():主动失效某些缓存数据。

什么时候触发失效呢?MapDB默认使用类似于Guava Cache的机制。不过,也支持可以通过如下配置使用线程池定期进行缓存失效。

.expireExecutor(scheduledExecutorService)

.expireExecutorPeriod(3000)

4.并发级别

concurrencyScale:类似于Guava Cache配置。

5.统计命中率

暂无。

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


堆外缓存

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, MemoryUnit.KB);

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

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)

.create();

在使用堆外缓存时,请记得添加JVM启动参数,如-XX:MaxDirectMemorySize=10G。


磁盘缓存

EhCache 3.x实现

CacheManager cacheManager = CacheManagerBuilder. newCacheManagerBuilder()

//默认线程池

.using(PooledExecutionServiceConfigurationBuilder.newPooledExecutionServiceConfigurationBuilder().defaultPool("default",1, 10).build())

//磁盘文件存储位置

.with(new CacheManagerPersistenceConfiguration(newFile("D:\\bak")))

.build(true);

CacheConfigurationBuilder<String, String> cacheConfig= 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到磁盘。

MapDB 3.x实现

DB db = DBMaker

.fileDB("D:\\bak\\a.data")//数据存哪里

.fileMmapEnable() //启用mmap

.fileMmapEnableIfSupported() //在支持的平台上启用mmap

.fileMmapPreclearDisable() //让mmap文件更快

.cleanerHackEnable() //一些BUG处理

.transactionEnable() //启用事务

.closeOnJvmShutdown()

.concurrencyScale(16)

.make();

HTreeMap myCache = 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();


分布式缓存

本文使用Ehcache 3.1+ Terracottaserver 实现,Ehcache 3.1引入了一个下载套件,其包含了Terracotta Server。

应用级缓存——《亿级流量》

调用start-tc-server脚本启动tc server。

1.架构

应用级缓存——《亿级流量》

Terracotta Server配置

<?xml version="1.0"encoding="UTF-8"?>

<tc-configxmlns="http://www.terracotta.org/config"

xmlns:ohr="http://www.terracotta.org/config/offheap-resource">

<servers>

<server host="192.168.147.50" name="s1">

<tsa-port>9510</tsa-port>

<tsa-group-port>9530</tsa-group-port>

</server>

<server host="192.168.147.52" name="s2">

<tsa-port>9510</tsa-port>

<tsa-group-port>9530</tsa-group-port>

</server>

<client-reconnect-window>30</client-reconnect-window>

<restartable enabled="true"/>

</servers>

<services>

<service id="resources">

<ohr:offheap-resources>

<ohr:resource name="cache"unit="MB">64</ohr:resource>

</ohr:offheap-resources>

</service>

</services>

</tc-config>

配置了两个tc server,其中一主一备。在两台服务器中分别调用如下脚本启动两台tc server。

./start-tc-server.sh -f tc-config.xml  -n s1

./start-tc-server.sh -f tc-config.xml  -n s2

2.EhCache代码片段

CacheManagerBuilder<PersistentCacheManager> clusteredCacheManagerBuilder=

CacheManagerBuilder.newCacheManagerBuilder()

.with(ClusteringServiceConfigurationBuilder.cluster(URI.create("terracotta://192.168.147.50:9510")).readOperationTimeout(500,TimeUnit.MILLISECONDS).autoCreate());

final PersistentCacheManager cacheManager =clusteredCacheManagerBuilder. build(true);

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

CacheConfigurationBuilder.newCacheConfigurationBuilder(

String.class,

String.class,

ResourcePoolsBuilder.newResourcePoolsBuilder().with(ClusteredResourcePoolBuilder.clusteredDedicated("cache",32, MemoryUnit.MB)))

.withDispatcherConcurrency(4).withExpiry(Expirations.timeToLiveExpiration(Duration.of(10,TimeUnit.SECONDS))));

可以看到一个问题,此处只指定了IP为192.168.147.50这台机器的tc-server,那么当50这台机器挂了,目前是不能自动连接到52机器的。不知道未来是否会支持。或者考虑使用其主打产品BigMemory(付费)。

对于分布式缓存个人还是喜欢使用Redis之类的,性能也非常好,有主从模式、集群模式。目前不建议使用Ehcache3.1+ Terracottaserver 组合。


多级缓存

如先查找堆缓存,如果没有查找磁盘缓存,则使用MapDB可以通过如下配置实现。

HTreeMap diskCache = db.hashMap("myCache")

.expireStoreSize(8 * 1024 * 1024 * 1024)

.expireMaxSize(10000)

.expireAfterCreate(10, TimeUnit.SECONDS)

.expireAfterUpdate(10, TimeUnit.SECONDS)

.expireAfterGet(10, TimeUnit.SECONDS)

.createOrOpen();

HTreeMap heapCache = db.hashMap("myCache")

.expireMaxSize(100)

.expireAfterCreate(10, TimeUnit.SECONDS)

.expireAfterUpdate(10, TimeUnit.SECONDS)

.expireAfterGet(10, TimeUnit.SECONDS)

.expireOverflow(diskCache) //当缓存溢出时存储到disk

.createOrOpen();

对于更复杂的多级缓存请参考“第11章 多级缓存”。

聊聊高并发系统之HTTP缓存

应用多级缓存模式支撑海量读服务

应用级缓存示例

应用级缓存——《亿级流量》

新书预售地址,请长按二维码预定。

========================

gitchat谢工邀请我做一场chat很久了,一直没有什么好的题材,没有去做这件事情。上周末想到一个关于写书的主题,分享给大家,主题是“如何开始写一本属于自己的技术书”,希望能对那些想写书的朋友有用。

应用级缓存——《亿级流量》


以上所述就是小编给大家介绍的《应用级缓存——《亿级流量》》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web Applications (Hacking Exposed)

Web Applications (Hacking Exposed)

Joel Scambray、Mike Shema / McGraw-Hill Osborne Media / 2002-06-19 / USD 49.99

Get in-depth coverage of Web application platforms and their vulnerabilities, presented the same popular format as the international bestseller, Hacking Exposed. Covering hacking scenarios across diff......一起来看看 《Web Applications (Hacking Exposed)》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具