介绍
当服务的访问量过大、并发量过高时,为了减轻数据库的压力,可以考虑使用缓存。缓存分为两种,一种是本地缓存,比如Caffeine、Ehache,另外一种是分布式缓存,最常用的就是redis。本地缓存的数据可存在于主存当中,所以速度略快于分布式缓存。
Caffeine是一款基于java8开发的本地缓存框架,拥有着较高的性能、命中率以及优秀的缓存驱逐策略。
它与ConcurrentHashMap非常相似的,但后者并不能自动的移除那些访问不频繁的数据。下面主要讲一下Caffeine的使用。
缓存的添加策略
手动加载
private static void buildCache() {
Cache<String, Set<String>> cache = Caffeine.newBuilder()
//设置过期时间;向缓存中写入的数据会在1分钟之后过期
.expireAfterWrite(1, TimeUnit.MINUTES)
//设置最大值;最大可以放1条数据,当数据量超过最大值之后,则进行覆盖
.maximumSize(1)
.build();
String cacheKey = "cacheKey";
//从缓存中获取数据;如果数据不存在则通过第二个参数自动生成;如果生成失败则返回null
Set<String> value0 = cache.get(cacheKey, key -> Sets.newHashSet("value1", "value2"));
//从缓存中获取,如果不存在,则返回null
Set<String> value1 = cache.getIfPresent(cacheKey);
//向缓存中添加元素
cache.put(cacheKey, Sets.newHashSet("value3", "value4"));
//可通过asMap方法对产生的ConcurrentMap进行操作;
cache.asMap().forEach((key, value) -> {
if (key.equals(cacheKey)) {
value.add("value5");
}
});
//删除缓存
cache.invalidate(cacheKey);
}
以上是手动加载的常用方法,之所以被称为手动加载,是因为在构建之后,需要我们手动进行put等操作来生成缓存中的数据。需要注意的是,cache.get方法是一个原子操作,多线程环境下,如果key是相同的,最后的操作会覆盖之前的数据。
自动加载
private static void autoLoadCache() {
String cacheKey = "cacheKey";
String cacheKey1 = "cacheKey1";
LoadingCache<String, Set<String>> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.maximumSize(100)
//根据key来生成value
.build(key -> createCache(key));
//如果未命中缓存,则通过key值生成value
cache.get(cacheKey);
//批量查找key对应的value;"cacheKey"会获取,"cacheKey1"返回的value为0条。
Map<String, Set<String>> all = cache.getAll(Arrays.asList(cacheKey, cacheKey1));
}
private static Set<String> createCache(String key) {
return new HashSet<String>() {{
if (key.equals("cacheKey1")) {
add(key.concat("value1"));
add(key.concat("value2"));
}
}};
}
自动加载相对于手动加载,是将生成缓存的步骤放在了创建缓存里,并在此基础上增加了批量查找的功能。
自动加载返回的是一个LoadingCache,我们可以自定义该接口里面的load(对应cache.get)方法和loadAll(对应cache.getAll)方法。
LoadingCache<String, Set<String>> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.maximumSize(100)
//根据key来生成value
.build(new CacheLoader<String, Set<String>>() {
@Override
public Set<String> load(String key) throws Exception {
//重写load方法...
return null;
}
@Override
public Map<String, Set<String>> loadAll(Iterable<? extends String> keys) throws Exception {
//重写loadAll方法...
return null;
}
});
手动异步加载
private static void buildAsyncCache() throws ExecutionException, InterruptedException {
String cacheKey = "cacheKey";
AsyncCache<String, Set<String>> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.maximumSize(100)
//表示是异步构建
.buildAsync();
// 查找value,没有返回null
CompletableFuture<Set<String>> value = cache.getIfPresent(cacheKey);
// 查找缓存元素,如果不存在,则异步生成结束后再返回
value = cache.get(cacheKey, key -> createCache(key));
// 添加或者更新一个缓存元素
cache.put(cacheKey, value);
//阻塞移除,需要等到缓存生成之后才能移除
cache.synchronous().invalidate(cacheKey);
}
private static Set<String> createCache(String key) {
//直到休眠完毕才能执行并返回
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return new HashSet<String>() {{
if (key.equals("cacheKey")) {
add(key.concat("value1"));
add(key.concat("value2"));
}
}};
}
手动异步加载方式使用了java8的CompletableFuture,在执行cache.get方法时,会进行异步获取value,直到value返回,下面是cache.get方法的源码,里面进行了异步的处理
并且该方式在清除缓存的时候是同步方式,防止缓存写入操作在清除之后执行。
自动异步加载
private static void autoAsyncCache() {
String cacheKey = "cacheKey";
AsyncLoadingCache<String, Set<String>> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.maximumSize(100)
//加载时创建自动异步生成数据的方法
.buildAsync((String key) -> createCache(key));
//value不存在的时候异步获取
CompletableFuture<Set<String>> value = cache.get(cacheKey);
//异步获取所有数据
cache.getAll(Arrays.asList(cacheKey));
}
自动异步加载的方式和自动加载方式很像,也可以重写CacheLoader接口中的AsyncCacheLoader.asyncLoad 和AsyncCacheLoader.asyncLoadAll方法。重写方式可参考上面的自动加载。
缓存的驱逐策略
基于容量
上面简单涉及到了缓存驱逐的策略,maximumSize方法可设置最大容量,当超过该容量之后则会进行覆盖,如下是基于缓存数量的驱逐方式:
Cache<String, Set<String>> cache = Caffeine.newBuilder()
//设置最大值;最大可以放1条数据,当数据量超过最大值之后,则进行覆盖
.maximumSize(1)
.build();
基于权重的驱逐方式:
private static void buildCache() {
Cache<String, Set<String>> cache = Caffeine.newBuilder()
//最大只能存下value数量为1的值
.maximumWeight(1)
.weigher((String key, Set<String> value) -> value.size())
.build();
cache.get("1", key -> new HashSet<String>() {{
add(key.concat("value"));
}});
cache.get("2", key -> new HashSet<String>() {{
add(key.concat("value"));
add(key.concat("value1"));
}});
}
如上设置了value的权重,意思是只能保留条数为1条的数据,则缓存中只会保留key为“1”的那条数据。
基于时间
如下是基于固定的时间方式:
Cache<String, Set<String>> cache = Caffeine.newBuilder()
//设置过期时间;向缓存中写入或更新的数据会在1分钟之后过期
.expireAfterWrite(1, TimeUnit.MINUTES)
//最大只能存下value数量为1的值
.build();
Cache<String, Set<String>> cache1 = Caffeine.newBuilder()
//设置过期时间;没有被访问的时间超过了1分钟,数据会过期
.expireAfterAccess(1, TimeUnit.MINUTES)
//最大只能存下value数量为1的值
.build();
基于自定义过期时间的方式:
Cache<String, Set<String>> cache2 = Caffeine.newBuilder()
//自定义设置过期时间
.expireAfter(new Expiry<String, Set<String>>() {
@Override
public long expireAfterCreate(@NonNull String key, @NonNull Set<String> value, long currentTime) {
//创建多久过期
return 1;
}
@Override
public long expireAfterUpdate(@NonNull String key, @NonNull Set<String> value, long currentTime, @NonNegative long currentDuration) {
//更新多久过期
return 2;
}
@Override
public long expireAfterRead(@NonNull String key, @NonNull Set<String> value, long currentTime, @NonNegative long currentDuration) {
//访问多久过期
return 3;
}
})
.build();
基于引用
Cache<String, Set<String>> cache2 = Caffeine.newBuilder()
//基于软引用
.softValues()
.build();
Cache<String, Set<String>> cache2 = Caffeine.newBuilder()
//基于弱引用;当key和value都不再存在其他强引用的时候
.weakKeys()
.weakValues()
.build();
各类引用的demo:
void reference() {
//强引用;永远不会被jvm回收
UserModel userModel = new UserModel();
//软引用;内存足够则不会回收,内存不够则回收
SoftReference<UserModel> softUserModel = new SoftReference<UserModel>(userModel);
//弱引用;内存够与不够都会被回收
WeakReference<UserModel> weekUserModel = new WeakReference<UserModel>(userModel);
//虚引用;随时都会被回收
ReferenceQueue<UserModel> rq = new ReferenceQueue<UserModel>();
PhantomReference<UserModel> prA = new PhantomReference<UserModel>(userModel, rq);
}
感谢您的观看,欢迎一起探讨。