什么是缓存?
想必大家第一次听到“缓存”这个概念,还是在大学的计算机专业课上。学过操作系统原理、计算机组成原理的同学都知道,在计算机系统中,存储层级可按其作用分为高速缓冲存储器(Cache)、主存储器、辅助存储器三级。当然了,这里的“高速缓存”并非本文要讨论的缓存。今天我们要讨论的缓存是软件层面的。而高速缓存是位于CPU内部的物理器件,是主存与CPU之间的一级存储器,通常由静态存储芯片(SRAM)组成,容量很小但速度比主存快得多,接近于CPU的速度。其主要作用就是缓和CPU和内存之间速度不匹配的问题,使得整机处理速度得以提升。
尽管不同于硬件层面的高速缓存,但是缓存的思想本质上是一致的,都是将数据放在离使用者最近的位置以及访问速度较快的存储介质上以加快整个系统的处理速度。软件缓存可以认为是为了缓和客户端巨大的并发量和服务端数据库(通常是关系型数据库)处理速度不匹配的问题。缓存本质上是在内存中维护的一个hash数据结构(哈希表),通常是以<key,value>的形式存储。由于hash table查询时间复杂度为O(1),因此性能很高。能够有效地加速应用的读写速度,降低后端负载。
那么自行实现一个缓存需要考虑哪些基本问题呢?
1)数据结构
首先要考虑的是数据该如何存储,要选择合适的数据结构。在Java编程中,最简单的就是直接用Map集合来存储数据;复杂一点,像redis提供了多种数据结构:哈希,列表,集合,有序集合等,底层使用了双端链表,压缩列表,跳跃表等数据结构;
2)更新/清除策略
缓存中的数据都是有生命周期的,要在指定时间后被删除或更新,这样才能保证缓存空间在一个可控的范围。常用的更新/清除策略有LRU(Least Recently Used最近最少使用)、FIFO( First Input First Output先进先出)、LFU(Least Frequently Used最近最不常用)、SOFT(软引用)、WEAK(弱引用)等策略。
3)线程安全
redis是单线程处理模式,就不存在线程安全问题;而本地缓存往往是可以多个线程同时访问的,所以线程安全不容忽视;线程安全问题是不应该抛给使用者去保证的,因此需要缓存自身支持。
常用的缓存技术有哪些呢?
1)分布式缓存
Memcached
Memcached 是一个开源的、高性能的、分布式的、基于内存的对象缓存系统。它能够用来存储各种格式的数据,包括字符串、图像、视频、文件等。Memcached 把数据全部存在内存之中,断电后会丢失,因此数据不能超过内存大小。且支持的数据结构较为单一,一般用于简单的key-value形式的存储。
Redis
Redis是一个开源的基于内存的数据结构存储组件,可用作数据库(nosql)、缓存和消息队列。它支持诸如字符串、散列、列表、集合、带范围查询的有序集合、位图、hyperloglogs、支持半径查询和流的地理空间索引等多种数据结构。Redis具有内置的复制、Lua脚本、LRU清除、事务和不同级别的磁盘持久化特性,并通过Redis 哨兵机制和基于Redis集群的自动分区提供高可用性。
和Memcached 相比,Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,hash等多种数据结构的存储。Redis还支持定期把数据持久化到磁盘。
2)本地缓存
本地缓存,顾名思义就是在应用本身维护一个缓存结构,比如Java中的Map集合就比较适合做<key,value>缓存。本地应用缓存最大的优点是应用本身和Cache在同一个进程内部,请求缓存非常快速,没有额外的网络I/O开销。本地缓存适合于单应用中不需要集群、各节点无需互相通信的场景。因此,其缺点是缓存跟应用程序耦合,分布式场景下多个独立部署的应用程序无法直接共享缓存,各节点都需要维护自己的单独缓存,既是对物理内存的一种浪费,也会导致数据的不一致性。Guava cache就是一种本地缓存。
话不多说,进入正题,本文主要介绍Google的缓存组件Guava Cache的实战技巧!
1.引入依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.1-jre</version>
</dependency>
2.Demo1:简单使用,掌握如何创建使用Cache
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.util.concurrent.Callable;
public class CacheService1 {
public static void main(String[] args) throws Exception {
Cache<Object, Object> cache = CacheBuilder.newBuilder().build();
// 写入/覆盖一个缓存
cache.put("k1", "v1");
// 获取一个缓存,如果该缓存不存在则返回一个null值
Object value1 = cache.getIfPresent("k1");
System.out.println("value1:" + value1);
// 获取缓存,当缓存不存在时,则通Callable进行加载并返回,该操作是原子的
Object getValue1 = cache.get("k1", new Callable<Object>() {
@Override
public Object call() throws Exception {
//缓存加载逻辑
return null;
}
});
System.out.println("getValue1:" + getValue1);
Object getValue2 = cache.get("k2", new Callable<Object>() {
/**
* 加载缓存的逻辑
* @return
* @throws Exception
*/
@Override
public Object call() throws Exception {
return "v2";
}
});
System.out.println("getValue2:" + getValue2);
}
}
控制台输出:
value1:v1
getValue1:v1
getValue2:v2
上述程序演示了Guava Cache的读和写。Guava的缓存有许多配置选项,为了简化缓的创建,使用了Builder设计模式;Builder使用的是链式编程的思想,也就是每次调用方法后返回的是对象本身,这样可以简化配置过程。
获取缓存值时可以指定一个Callable实例动态执行缓存加载逻辑;也可以在创建Cache实例时直接使用LoadingCache。顾名思义,它能够通过CacheLoader自发的加载缓存。
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
public class CacheService2 {
public static void main(String[] args) throws Exception {
LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder()
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
// 缓存加载逻辑
return "value2";
}
});
loadingCache.put("k1", "value1");
String v1 = loadingCache.get("k1");
System.out.println(v1);
// 以不安全的方式获取缓存,当缓存不存在时,会通过CacheLoader自动加载
String v2 = loadingCache.getUnchecked("k2");
System.out.println(v2);
// 获取缓存,当缓存不存在时,会通过CacheLoader自动加载
String v3 = loadingCache.get("k3");
System.out.println(v3);
}
}
控制台输出:
value1
value2
value2
3.Demo2:理解Cache的过期处理机制
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class CacheService3 {
/**
* 通过builder模式创建一个Cache实例
*/
static Cache<Integer, String> cache = CacheBuilder.newBuilder()
//设置缓存在写入5秒钟后失效
.expireAfterWrite(5, TimeUnit.SECONDS)
//设置缓存的最大容量(基于容量的清除)
.maximumSize(1000)
//开启缓存统计
.recordStats()
.build();
public static void main(String[] args) throws Exception {
//单起一个线程监视缓存状态
new Thread() {
public void run() {
while (true) {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println(sdf.format(new Date()) + " cache size: " + cache.size());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
//写入缓存
cache.