应用级缓存

缓存是让数据更接近用户,提高访问速度

由于磁盘到内存之间的 IO 瓶颈会影响数据读取速度,将数据放在内存中,有请求到来时先从缓存中读取数据,如果没有,再从磁盘上读取并同步到缓存。然而,内存的空间是远远小于硬盘空间的,不能讲全部的数据都放到内存中。经常读取的数据,频繁访问的数据,热点数据,I/O 瓶颈数据,计算昂贵的数据,符号 5 分钟法则(如果一个数据的访问周期在5分钟以内则存放在内存中,否则应该存放在硬盘中)和局部性原理(CPU访问存储器时,无论是存取指令还是存取数据,所访问的存储单元都趋于聚集在一个较小的连续区域中)的数据都可以进行缓存。如 CPU $\to$ L1/L2/L3$\to$ 内存 $\to$ 磁盘,提高缓存的命中率,可以提高访问速度,由于缓存空间有限,热点数据在不停变化,需要不断更新缓存以提高缓存命中率

缓存回收策略

  1. 基于时间
    1. TTL (Time to Live):存活期,从创建开始,不管数据有没有被访问,都将过期
    2. TTI (Time to Idle):空闲期,数据多久没被访问后移除缓存
  2. 基于空间/容量(空间:100MB,容量:1000条数据)
  3. 基于 Java 对象引用类型
    1. 软引用:当 JVM 堆内存不足时,会进行相应的 GC,这次 GC 不会回收软引用对象;如果 GC 后依旧空间不足, JVM 会进行第二次 GC,来回收不被强引用对象引用的软引用
    2. 弱引用:GC 时回收不被强引用对象引用的弱引用对象

回收算法

  1. FIFO (First In First Out):先进先出算法
  2. LRU (Least Recently Used): 最近最少被使用算法
  3. LFU (Least Frequently Used):最不常用算法

Java 缓存类型

堆缓存

使用 Java 堆内存来存储缓存对象,一般使用软引用/弱引用来存储,当缓存量很大时,会导致 GC 时间变长,可以使用Guava CacheEhcache 3.xMapDB实现

堆外缓存

将缓存数据存储在堆外内存,减少 GC 时间,可以支持更大的缓存空间(只受机器内存大小限制),但存取数据时需要序列化和反序列化,比堆缓存慢很多,可以使用Ehcache 3.xMapDB实现

磁盘缓存

缓存数据存储在磁盘上,JVM重启后数据还是存在,而堆缓存/堆外缓存都会丢失,可以使用Ehcache 3.xMapDB实现

分布式缓存

Redis

实现

堆缓存

Gauva Cache 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Cache<String, String> cache = CacheBuilder.newBuilder()
.concurrencyLevel(4) // 并发级别
.expireAfterWrite(2, TimeUnit.SECONDS) // 设置 TTL 时间
//.expireAfterAccess(2, TimeUnit.SECONDS) // 设置 TTI 时间
.maximumSize(10000) // 缓存容量,如果超出按照 LRU 进行缓存回收
.recordStats() // 开启统计命中率
.build();
cache.put("zjm", "boy");
String value = cache.getIfPresent("zjm");
System.out.println(value); // boy
Thread.sleep(3000L);
value = cache.getIfPresent("zjm");
System.out.println(value); // null
System.out.println(cache.stats()); // CacheStats{hitCount=1, missCount=1, loadSuccessCount=0, loadExceptionCount=0, totalLoadTime=0, evictionCount=1}
cache.put("name", "shaun");
cache.put("hello", "John");
cache.invalidate("name"); // 主动失效 key="name"
List<String> keys = Arrays.asList("name", "hello");
cache.invalidateAll(keys);
cache.incalidateAll(); // 主动全部失效

CacheBuilder的参数:maximumSize设置缓存容量(存入键值对的个数),expireAfterWrite定时回收(建议使用),expireAfterAccess空闲定时回收(可能产生脏数据),weakKeys/weakValues设置缓存为软引用类型,softValues设置缓存为软引用类型

EhCache 3.8 实现
1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
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(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.of(2, ChronoUnit.SECONDS)));
Cache<String, String> myCache = cacheManager.createCache("myCache", cacheConfig);
myCache.put("color", "red");
String color = myCache.get("color");
System.out.println(color);
}

heap(100, EntryUnit.ENTRIES):设置缓存的条目数量,当超出此数量按照 LRU 回收;head(100, MemoryUnit.MB):设置缓存的内存空间,同时要设置 withSizeOfMaxObjectGraph(2)统计对象大小时对对象图遍历深度和 withSizeOfMaxObjectSize(1, MemoryUnit.KB)可缓存的最大对象大小;使用withExpiry()设置基于时间的缓存回收策略;remove(K)/removeAll(Set keys)/clear()来主动回收

MapDB 3.0.7 实现
1
2
3
4
5
6
7
8
9
10
11
DB db = DBMaker.memoryDB().make();
ConcurrentMap map = db.hashMap("map").createOrOpen();
map.put("k", "v");
// 或者
HTreeMap hTreeMap = DBMaker.heapDB().concurrencyScale(16).make().hashMap("myCache")
.expireMaxSize(10000)
.expireAfterCreate(10, TimeUnit.SECONDS)
.expireAfterUpdate(10, TimeUnit.SECONDS)
.expireAfterGet(10, TimeUnit.SECONDS)
.create();
hTreeMap.put("major", "artist");

堆外缓存

EhCache 3.8 实现
1
2
3
4
5
6
7
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);
CacheConfigurationBuilder<String, String> cacheConfig = CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class, String.class,
ResourcePoolsBuilder.newResourcePoolsBuilder().offheap(100, MemoryUnit.MB))
.withDispatcherConcurrency(4)
.withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.of(10, ChronoUnit.SECONDS)));
Cache<String, String> cache = cacheManager.createCache("cache", cacheConfig);
cache.put("color", "red");
MapDB 3.0.7 实现
1
2
3
4
5
6
7
8
DB db = DBMaker.memoryDirectDB().make();
// 或者
HTreeMap myCache = DBMaker.memoryDirectDB().concurrencyScale(16).make().hashMap("myCache")
.expireStoreSize(64*1024*1024)
.expireAfterCreate(10, TimeUnit.SECONDS)
.expireAfterUpdate(10, TimeUnit.SECONDS)
.expireAfterGet(10, TimeUnit.SECONDS)
.create();

磁盘缓存

EhCache 3.8 实现

上面 EhCache 创建 cache 的写法很麻烦,下面通过配置文件生成 cache 的方式,更简明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="UTF-8"?>
<config
xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
xmlns='http://www.ehcache.org/v3'
xsi:schemaLocation="
http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.7.xsd">
<persistence directory="C:\Users\zjm\IdeaProjects\CacheDemo\ehcache.db"/>
<!-- cache 的别名 -->
<cache alias="productCache">
<!-- key-type: key的类型 value-type: 值的类型 -->
<key-type>java.lang.String</key-type>
<value-type>java.lang.String</value-type>
<!-- 用来控制元素的过期时间 -->
<expiry>
<ttl unit="seconds">200</ttl>
</expiry>
<resources>
<!-- <heap unit="entries">200</heap>-->
<!-- <offheap unit="MB">100</offheap>-->
<disk unit="MB" persistent="true">100</disk>
</resources>
</cache>
</config>

上面的配置文件中,是希望在磁盘上做缓存,<persistence>指定了持久化的路径,<cache>alias属性值是cache的别名,下面会通过 cacheManager获取

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test() {
final URL url = getClass().getResource("/ehcache.xml");
XmlConfiguration xmlConfig = new XmlConfiguration(url);
CacheManager cacheManager = CacheManagerBuilder.newCacheManager(xmlConfig);
cacheManager.init();
// 从配置文件中设置的别名获取 cache 的配置
Cache cache = cacheManager.getCache("productCache", String.class, String.class);
cache.put("aaa", "bbb");
System.out.println(cache.get("aaa"));
cacheManager.close();
}

注:在 JVM 停止时,一定要调用 cacheManager.close() 才能保证内存数据 dump 到磁盘

MapDB 3.0.7 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void fileMapTest() {
DB db = DBMaker.fileDB("file.db")
.fileMmapEnable() // 启动 mmap
.fileMmapEnableIfSupported() // 在支持的平台上启用 mmap
.fileMmapPreclearDisable() // 让 mmap 文件更快
.transactionEnable() // 开启事务
.closeOnJvmShutdown()
.concurrencyScale(16)
.make();
ConcurrentMap map = db.hashMap("map")
.expireMaxSize(10000)
.expireAfterCreate(10, TimeUnit.SECONDS)
.createOrOpen();
map.put("Butterfly", "beautify");
System.out.println(map.get("Butterfly"));
db.close();
}
mmap

mmap 是一种内存映射文件的方法。实现了用户态和内核态的数据直接交互,省去了空间不同数据不通的繁琐过程。优点:1. 减少了数据的拷贝次数,用内存读写取代I/O读写,提高了文件读取效率;2. 实现了用户空间和内核空间的高效交互方式

您的支持鼓励我继续创作!