guava cache 的refreshAfterWrite模式存在无效key的问题记录

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

问题暴露

某个缓存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, 然后使用guavarefreshAfterWrite来进行缓存
  • 某一个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.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值