前言
缓存是计算机系统中一种常见的数据存储技术。它用于临时存储经常访问的数据,以提高系统的性能和响应速度。
在计算机系统中,数据通常存储在较慢的主存(RAM)中。而缓存则位于主存和处理器之间,作为一个更快、更小的存储器。当处理器需要访问数据时,它首先会查找缓存。如果数据存在于缓存中,就可以快速获取并提供给处理器,这样就避免了读取主存的延迟时间。
缓存利用了局部性原理(Locality Principle),即数据访问模式具有时间局部性和空间局部性。
- 时间局部性指的是近期访问的数据可能在不久的将来再次被访问
- 空间局部性指的是与当前访问的数据相邻的数据可能很快被访问。
本地缓存是指将数据暂存到本地计算机的内存中,以便在后续访问中能够更快地获取。本地缓存通常由应用程序使用,可以提高应用程序的性能和响应速度。
Map
在Java中,实现本地缓存通常使用key/value形式的数据结构,可以选择使用Map集合来作为存储容器。常见的Map实现类有HashMap、Hashtable和ConcurrentHashMap。
- 如果不考虑高并发情况下的数据安全问题,可以选择HashMap。它是非线程安全的,但在单线程或低并发环境下性能较好。
- 如果需要考虑高并发情况下的数据安全问题,可以选择Hashtable或ConcurrentHashMap。Hashtable是线程安全的,但性能相对较差。而ConcurrentHashMap则既能保证线程安全,又具备较好的性能。
在选择时,更推荐使用ConcurrentHashMap。它通过使用分段锁(Segment)的方式,将数据分成多个段,每个段由独立的锁控制。这样可以提供更好的并发性能,不同的线程可以同时访问不同的段,从而减少了锁竞争的概率。
代码语言:javascript复制public class ConcurrentHashMapTest {
private static ConcurrentHashMap<Integer, Tuple2<Long, String>> cache = new ConcurrentHashMap<>(3);
private static Map<Integer, String> dbData = new HashMap<>(3);
static {
// 初始化数据库数据
dbData.put(1, "上海");
dbData.put(2, "北京");
dbData.put(3, "深圳");
}
private static int expirationTime = 3; // 缓存过期时间(单位:秒)
private static int mill = 1000; // 时间单位换算,从毫秒转为秒
@Test
@SneakyThrows
public void test() {
System.out.println("the result is " getCityByCode(1)); // 获取城市码为1的城市名
Thread.sleep(1000); // 等待1秒
System.out.println("the result is " getCityByCode(1)); // 再次获取城市码为1的城市名(此时缓存未过期,直接从缓存中获取)
Thread.sleep(3000); // 等待3秒(超过缓存过期时间)
System.out.println("the result is " getCityByCode(1)); // 再次获取城市码为1的城市名(此时缓存已过期,需要重新从数据库获取)
Thread.sleep(1000); // 等待1秒
System.out.println("the result is " getCityByCode(2)); // 获取城市码为2的城市名
}
private String getCityByCode(int code) {
if (!cache.containsKey(code)) { // 如果缓存中不包含该城市码的数据
return getCityFromDb(code); // 从数据库中获取城市名
}
Tuple2<Long, String> tuple2 = cache.get(code);
if (isOverTime(tuple2._1)) { // 如果缓存已超过过期时间
System.out.println("cache is over time");
return getCityFromDb(code); // 从数据库中获取城市名
} else {
return tuple2._2; // 否则直接从缓存中获取城市名
}
}
private String getCityFromDb(Integer code) {
String city = dbData.get(code); // 从数据库中获取城市名
System.out.println("query city " city " from db");
cache.put(code, new Tuple2<>(System.currentTimeMillis(), city)); // 将查询结果放入缓存中
return city;
}
private boolean isOverTime(Long time) {
if ((System.currentTimeMillis() - time) / mill > expirationTime) { // 判断是否超过缓存过期时间
return true; // 已超过过期时间
}
return false; // 未超过过期时间
}
}
GuavaCache
Guava Cache是一个功能强大且易于使用的缓存库,它提供了简单高效的缓存解决方案。
- Guava Cache(也称为Guava缓存)是Google开源的一个Java库,用于实现本地缓存。它是Guava项目的一部分,是Google对Java集合框架的扩展和增强。
- Guava Cache提供了一个简单而强大的缓存实现,旨在提高应用程序的性能和响应速度。它支持线程安全,并提供了一些高级特性,例如自动加载缓存、大小限制、过期策略和统计信息收集等。
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>27.0.1-jre</version>
</dependency>
代码语言:javascript复制import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.util.concurrent.TimeUnit;
public class GuavaCacheExample {
private static Cache<Integer, String> cache;
public static void main(String[] args) {
// 创建一个GuavaCache实例
cache = CacheBuilder.newBuilder()
.expireAfterWrite(3, TimeUnit.SECONDS) // 设置缓存过期时间为3秒
.maximumSize(100) // 设置最大缓存大小为100
.build();
// 添加数据到缓存
cache.put(1, "shanghai");
cache.put(2, "beijing");
cache.put(3, "shenzhen");
// 从缓存中获取数据
System.out.println("the result is " getCityByCode(1));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("the result is " getCityByCode(1));
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("the result is " getCityByCode(1));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("the result is " getCityByCode(2));
}
private static String getCityByCode(int code) {
String city = cache.getIfPresent(code); // 从缓存中获取城市名
if (city == null) {
city = getCityFromDb(code);
cache.put(code, city); // 将查询结果放入缓存
}
return city;
}
private static String getCityFromDb(int code) {
// 模拟从数据库中获取城市名的操作
switch (code) {
case 1:
return "shanghai";
case 2:
return "beijing";
case 3:
return "shenzhen";
default:
return null;
}
}
}
Caffeine
Caffeine 是基于 JAVA 8 的高性能本地缓存库。并且在 spring5 (springboot 2.x) 后,spring 官方放弃了 Guava,而使用了性能更优秀的 Caffeine 作为默认缓存组件。
Caffeine是在Guava Cache的基础上做一层封装,性能有明显提高,二者同属于内存级本地缓存。使用Caffeine后无需使用Guava Cache,从并发的角度来讲,Caffeine明显优于Guava,原因是使用了Java 8最新的StampedLock锁技术。
本地缓存与分布式缓存对应,缓存进程和应用进程同属于一个JVM,数据的读、写在一个进程内完成。本地缓存没有网络开销,访问速度很快。
Caffeine提供灵活的结构来创建缓存,并且有以下特性:
- 自动加载条目到缓存中,可选异步方式
- 可以基于大小剔除
- 可以设置过期时间,时间可以从上次访问或上次写入开始计算
- 异步刷新
- keys自动包装在弱引用中
- values自动包装在弱引用或软引用中
- 条目剔除通知
- 缓存访问统计
简单使用
代码语言:javascript复制<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.6.2</version>
</dependency>
入门案例
代码语言:javascript复制 // 构建cache对象
Cache<String, String> cache = Caffeine.newBuilder().build();
// 存数据
cache.put("k1", "v1");
// 取数据
String v1 = cache.getIfPresent("k1");
System.out.println("k1 = " v1);
// 取数据,包含两个参数:
// 参数一:缓存的key
// 参数二:Lambda表达式,表达式参数就是缓存的key,方法体是查询数据库的逻辑
// 优先根据key查询JVM缓存,如果未命中,则执行参数二的Lambda表达式
String defaultkey = cache.get("k2", key -> {
// 根据key去数据库查询数据
return "v2";
});
System.out.println("k2 = " defaultkey);
配置案例
代码语言:javascript复制public static LoadingCache<Long, User> loadingCache = Caffeine.newBuilder()
// 初始的缓存空间大小
.initialCapacity(5)
// 缓存的最大条数
.maximumSize(10)
.expireAfterWrite(4, TimeUnit.SECONDS)
.expireAfterAccess(10, TimeUnit.SECONDS)
.refreshAfterWrite(6, TimeUnit.SECONDS)
.recordStats()
//设置缓存的移除通知
.removalListener(new RemovalListener<Long, User>() {
@Override
public void onRemoval(@Nullable Long key, @Nullable User user, @NonNull RemovalCause removalCause) {
System.out.printf("Key: %s ,值:%s was removed!原因 (%s) n", key, user, removalCause);
}
})
.build(id -> {
System.out.println("缓存未命中,从数据库加载,用户id:" id);
return User.builder().id(id).userName("Lily").age(new Random().nextInt(20)).build();
});
参数说明:
- initialCapacity 初始的缓存空间大小
- maximumSize 缓存的最大条数
- maximumWeight 缓存的最大权重
- expireAfterAccess 最后一次写入或访问后,经过固定时间过期
- expireAfterWrite 最后一次写入后,经过固定时间过期
- refreshAfterWrite 写入后,经过固定时间过期,下次访问返回旧值并触发刷新
- weakKeys 打开 key 的弱引用
- weakValues 打开 value 的弱引用
- softValues 打开 value 的软引用
- recordStats 缓存使用统计
- expireAfterWrite 和 expireAfterAccess 同时存在时,以 expireAfterWrite 为准。
- weakValues 和 softValues 不可以同时使用。
- maximumSize 和 maximumWeight 不可以同时使用。
清除策略
Caffeine提供了三种缓存驱逐策略:
基于容量:设置缓存的数量上限
代码语言:javascript复制// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(1) // 设置缓存大小上限为 1
.build();
基于时间:设置缓存的有效时间
代码语言:javascript复制// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
// 设置缓存有效期为 10 秒,从最后一次写入开始计时
.expireAfterWrite(Duration.ofSeconds(10))
.build();
基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。
代码语言:javascript复制 // 构建cache对象
Cache<String, String> cache = Caffeine.newBuilder()
.weakKeys().weakValues().build();
Caffeine.weakKeys() 使用弱引用存储key。如果没有强引用这个key,则GC时允许回收该条目 Caffeine.weakValues() 使用弱引用存储value。如果没有强引用这个value,则GC时允许回收该条目 Caffeine.softValues() 使用软引用存储value, 如果没有强引用这个value,则GC内存不足时允许回收该条目
引用类型 | 被垃圾回收时间 | 用途 | 生存时间 |
---|---|---|---|
强引用 | 从来不会 | 对象的一般状态 | JVM停止运行时终止 |
软引用 | 在内存不足时 | 对象缓存 | 内存不足时终止 |
弱引用 | 在垃圾回收时 | 对象缓存 | gc运行后终止 |
虚引用 | Unknown | Unknown | Unknown |
GuavaCache和Caffeine差异
- 剔除算法方面,GuavaCache采用的是「LRU」算法,而Caffeine采用的是「Window TinyLFU」算法,这是两者之间最大,也是根本的区别。
- 立即失效方面,Guava会把立即失效 (例如:expireAfterAccess(0) and expireAfterWrite(0)) 转成设置最大Size为0。这就会导致剔除提醒的原因是SIZE而不是EXPIRED。Caffiene能正确识别这种剔除原因。
- 取代提醒方面,Guava只要数据被替换,不管什么原因,都会触发剔除监听器。而Caffiene在取代值和先前值的引用完全一样时不会触发监听器。
- 异步化方方面,Caffiene的很多工作都是交给线程池去做的(默认:ForkJoinPool.commonPool()),例如:剔除监听器,刷新机制,维护工作等。
EhCache
EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认CacheProvider。Ehcache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存,Java EE和轻量级容器。它具有内存和磁盘存储,缓存加载器,缓存扩展,缓存异常处理程序,一个gzip缓存servlet过滤器,支持REST和SOAP api等特点。
缓存支持三种类型:堆内存储、堆外存储、磁盘存储(支持持久化)。
使用方法如下:
代码语言:javascript复制<dependency><dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.9.7</version>
</dependency>
代码语言:javascript复制@Slf4j
public class EhcacheTest {
private static final String ORDER_CACHE = "orderCache";
public static void main(String[] args) {
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
// 创建实例
.withCache(ORDER_CACHE, CacheConfigurationBuilder
// 声明一个容量为30的堆内缓存
.newCacheConfigurationBuilder(String.class, String.class, ResourcePoolsBuilder.heap(30)))
.build(true);
// 得到缓存实例
Cache<String, String> cache = cacheManager.getCache(ORDER_CACHE, String.class, String.class);
String orderId = String.valueOf(6666666);
String orderInfo = cache.get(orderId);
if (StrUtil.isBlank(orderInfo)) {
orderInfo = getInfo(orderId);
cache.put(orderId, orderInfo);
}
log.info("orderInfo = {}", orderInfo);
}
private static String getInfo(String orderId) {
String info = "";
// 首先从redis查
log.info("get data from redis");
// 不存在 查db
log.info("get data from mysql");
info = String.format("{orderId=%s}", orderId);
return info;
}
}