缓存简介
Java里面常用的缓存有本地缓存和分布式缓存。本地缓存的代表技术主要有HashMap,Guava Cache,Caffeine和Encahche。分布式缓存主要有redis和memcache。
当我们在单机的场景或者对性能要求更高但一致性要求低的分布式场景中,实际上使用本地缓存会性能更佳,没有远端的网络延迟,接口性能也将更加稳定。
为什么是Caffeine
有的同学可能会疑惑,本地缓存直接用HashMap就好了,还引入别的组件来做,不是多此一举?
本地缓存HashMap介绍
通过Map的底层方式,直接将需要缓存的对象放在内存中。
优点:实现简单,不需要引入第三方包,比较适合一些比较简单的场景。
缺点:内存无法过期,没有缓存淘汰策略,对内存不友好。结构单一,不支持更加复杂的缓存场景,定制化开发成本高。
class LRUCache extends LinkedHashMap<Integer, Integer>{
private int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75F, true); // true设置按照访问顺序排序
this.capacity = capacity;
}
public int get(int key) {
return super.getOrDefault(key, -1);
}
public void put(int key, int value) {
super.put(key, value);
}
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > capacity;
}
}
Caffeine
Caffeine采用了W-TinyLFU(LUR和LFU的优点结合)开源的缓存技术。缓存性能接近理论最优,属于是Guava Cache的增强版。主要具备以下优势:
性能优越
Caffeine 在性能方面表现优异,尤其是在高并发环境下。相比于 Guava Cache,Caffeine 提供了更好的吞吐量和更低的延迟。这是因为 Caffeine 对内部数据结构进行了优化,并采用了高效的锁机制。丰富的缓存策略
Caffeine 提供了多种缓存策略,包括但不限于 LRU(Least Recently Used)、LFU(Least Frequently
Used)等。这意味着可以根据应用场景选择最适合的缓存淘汰策略,从而更好地满足业务需求。易于集成
Caffeine 设计简洁易用,可以很容易地与其他框架和库集成。无论是作为独立的缓存库还是与其他缓存系统(如 Redis)结合使用,都非常方便。内存友好
Caffeine 在内存管理和垃圾回收方面做了大量的优化工作,可以有效地减少内存消耗和 GC 暂停时间。这对于大型应用尤为重要,可以显著提升整体性能。异步加载
Caffeine 支持异步加载缓存项,这意味着可以在后台异步加载数据,而不会阻塞主线程。这对于需要从外部系统获取数据的应用特别有用,可以大幅提升响应速度。统计和监控
Caffeine 内置了详细的统计和监控功能,可以帮助开发者更好地了解缓存的状态和性能指标。这对于调试和优化缓存策略非常重要。高度可定制化
Caffeine 具有很高的可定制性,可以通过各种配置选项来调整缓存的行为。例如,可以自定义缓存容量、过期时间、刷新策略等。
使用示例:
public class CaffeineCache {
public static void main(String[] args) throws Exception {
//创建guava cache
Cache<String, String> loadingCache = Caffeine.newBuilder()
//cache的初始容量
.initialCapacity(5)
//cache最大缓存数
.maximumSize(10)
//设置写缓存后n秒钟过期
.expireAfterWrite(17, TimeUnit.SECONDS)
//设置读写缓存后n秒钟过期,实际很少用到,类似于expireAfterWrite
//.expireAfterAccess(17, TimeUnit.SECONDS)
.build();
String key = "key";
// 往缓存写数据
loadingCache.put(key, "v");
// 获取value的值,如果key不存在,获取value后再返回
String value = loadingCache.get(key, CaffeineCache::getValueFromDB);
// 删除key
loadingCache.invalidate(key);
}
private static String getValueFromDB(String key) {
return "v";
}
}
Caffeine详细介绍
缓存类型
-
Cache<K, V>
Cache 是一个简单的缓存类型,不支持自动加载缺失的键值对。它主要用于存储已经存在的键值对。Cache<String, String> cache = Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(10, TimeUnit.MINUTES) .build(); cache.put("key1", "value1"); System.out.println(cache.getIfPresent("key1")); // 输出: value1
-
LoadingCache<K, V>
LoadingCache 是一种常用的缓存类型,它可以自动加载缺失的键值对。当尝试获取一个不存在的键时,LoadingCache 会调用一个提供的函数来加载对应的值。LoadingCache<String, String> cache = Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(10, TimeUnit.MINUTES) .build(new CacheLoader<String, String>() { @Override public String load(String key) throws Exception { System.out.println("Loading value for key: " + key); return "Value for " + key; } }); System.out.println(cache.get("key1")); // 输出: Loading value for key: key1 // Value for key1 System.out.println(cache.get("key1")); // 输出: Value for key1
-
AsyncLoadingCache<K, V>
AsyncLoadingCache 类似于 LoadingCache,但它异步加载缺失的键值对。这对于高并发环境下的缓存加载特别有用。AsyncLoadingCache<String, String> cache = Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(10, TimeUnit.MINUTES) .buildAsync(new AsyncCacheLoader<String, String>() { @Override public ListenableFuture<String> asyncLoad(String key) throws Exception { System.out.println("Async loading value for key: " + key); return ListenableFutures.immediateFuture("Value for " + key); } }); System.out.println(cache.get("key1").get()); // 输出: Async loading value for key: key1 // Value for key1 System.out.println(cache.get("key1").get()); // 输出: Value for key1
内存清理
-
最大容量
Cache<String, String> cache = Caffeine.newBuilder() .maximumSize(100) // 设置最大容量为100个条目 .build(); for (int i = 0; i < 150; i++) { cache.put("key" + i, "value" + i); } System.out.println("Current size: " + cache.size()); // 输出当前缓存大小
-
过期时间
Cache<String, String> cache = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.SECONDS) // 写入后10秒过期 .build(); cache.put("key1", "value1"); Thread.sleep(11_000); // 等待11秒 System.out.println("Key 'key1' is present: " + cache.getIfPresent("key1")); // 输出: null
-
访问频率
Cache<String, String> cache = Caffeine.newBuilder() .weakKeys() // 使用弱引用管理键 .build(); cache.put("key1", "value1"); // 如果内存压力较大,"key1"可能会被回收 System.out.println("Key 'key1' is present: " + cache.getIfPresent("key1"));
统计
Caffinet 提供了一些便捷的统计方法。
Caffeine.recordStats()方法可以数据收集功能。Cache.stats()方法将会返回一个CacheStats对象。
Cache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(100)
.recordStats()
.build();
CacheStats对象的方法:
hitRate(): 查询缓存的命中率
evictionCount(): 被驱逐的缓存数量
averageLoadPenalty(): 新值被载入的平均耗时
SpringBoot整合Caffeine
引入Maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
配置缓存管理Bean,注入到Spring容器中
@Configuration
public class CacheConfig {
/**
* 配置缓存管理器
*
* @return 缓存管理器
*/
@Bean("caffeineCacheManager")
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
// 设置最后一次写入或访问后经过固定时间过期
.expireAfterAccess(60, TimeUnit.SECONDS)
// 初始的缓存空间大小
.initialCapacity(100)
// 缓存的最大条数
.maximumSize(1000));
return cacheManager;
}
}
使用示例:
@Slf4j
@Service
@CacheConfig(cacheNames = "caffeineCacheManager")
public class ProjectServiceImpl {
/**
* 模拟数据库存储数据
*/
private HashMap<Integer, Project> projectMap = new HashMap<>();
@CachePut(key = "#project.id")
public void put(Project project) {
projectMap.put(project.getId(), project);
}
@Cacheable(key = "#id")
public Project get(Integer id) {
return projectMap.get(id);
}
@CacheEvict(key = "#id")
public void deleteById(Integer id) {
projectMap.remove(id);
}
}