问题暴露
某个缓存key和对应的value被缓存到guava后,一段时间后该key对应的记录在数据库中被删除;
当这个key再次被获取的时候,到达refreshAfterWrite设置时间触发refresh方法, 然后委托给我们自己实现的reload方法,我们的reload方法又调用了load方法来重新刷新该key对应的最新值。那么这个时候问题来了。因为我们知道,如果一个key曾经没有存在过,触发load方法的时候,我们return null,最终guava不会去增加一个key来保存这一对无效键值对。
但是现在我们的问题是这个key是存在的,只是现在刷新的时候发现值不存在了,变成了null。程序如果不管这种情况的话,由于guava不接收value为null的缓存,然后内部会抛出异常,最终那本该不存在的旧值就又会一直占用这个key的缓存。然后获取缓存的线程又会只会判断是否为null,然后执行业务,最终就会拿着被删除的垃圾数据走业务,而且这个垃圾数据会一直存在。
问题演示
下面会简单写一个demo,然后用来演示上面那个问题, 步骤描述
- 有一个对象为
Person, 然后使用guava的refreshAfterWrite来进行缓存 - 某一个
Person被缓存后,随后这个对象对应的记录在数据库被删除了 - 然后发现我们从缓存中获取对应被删除的对象的key即使已经刷新了无数遍发现数据还是会一直存在,而不是null
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListenableFutureTask;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import jdk.reflect.StringUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* <p>description</p >
*
* @author Snowball
* @version 1.0
* @date 2020/08/27 14:46
*/
@Slf4j
public class Demo {
/**
* 原始数据集合,模拟数据库记录
*/
private final static Map<String, Person> DATA_MAP;
/**
* guava缓存Person
*/
private static LoadingCache<String, Person> INIT_LOADING_CACHE;
/**
* reload线程池
*/
private final static ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),
Runtime.getRuntime().availableProcessors() * 2, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(500), new ThreadFactoryBuilder().setNameFormat("reload-executor-pool").build());
static {
// 初始化原始数据,模拟数据库中的记录
DATA_MAP = new HashMap<>();
DATA_MAP.put("1", new Person("1", "jack"));
DATA_MAP.put("2", new Person("2", "tom"));
// 初始化缓存
initLazyCache();
}
/**
* 初始化缓存
*/
private static void initLazyCache() {
INIT_LOADING_CACHE = CacheBuilder.newBuilder()
.maximumSize(500)
.recordStats()
.softValues()
.refreshAfterWrite(3, TimeUnit.SECONDS)
.build(new CacheLoader<String, Person>() {
@Override
public Person load(String key) throws Exception {
System.out.println("=================load===================: " + key);
if (StringUtil.isBlank(key)) {
return null;
}
String[] data= key.split(CacheKeyEnum.SPLIT_CHAR);
// LoadingCacheDemo1:INIT:{0}
String id = data[2];
return get(id);
}
@Override
public ListenableFuture<Person> reload(String key, Person oldValue) throws Exception {
// 如果使用异步的话,短期内多次获取,因为此时异步尚未执行结束,大概率会造成获取的时候使用的还是旧的缓存值
ListenableFutureTask<Person> task = ListenableFutureTask.create(() -> load(key));
EXECUTOR.execute(task);
// 可选是否采用闭锁再刷新的时候让读线程等待刷新完成,一般不需要,就异步刷新即可
task.

探讨Guava缓存中处理null值引发的问题,及如何避免无效key占用缓存资源。通过实例演示,分析问题根源,并提供解决方案,确保缓存一致性。
最低0.47元/天 解锁文章
4071

被折叠的 条评论
为什么被折叠?



