内存缓存-caffeine

1 快速入门案例

maven依赖

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.8.4</version>
</dependency>

1.1 构建cache工具类

public enum LoadTypeEnum {
    /**
     * 手动加载
     */
    MANUAL,

    /**
     * 同步加载
     */
    SYNC,

    /**
     * 异步加载
     */
    ASYNC,

    /**
     * 后台刷新
     */
    REFRESH
}

@Slf4j
public class CacheUtil {

    private static final Random RANDOM = new Random();

    private static ExecutorService executorService = Executors.newCachedThreadPool();

    public static int randomInt() {
        return RANDOM.nextInt(1000);
    }

    public static void execute(Runnable runnable) {
        executorService.execute(runnable);
    }

    public static LoadingCache buildLoadCache(CacheConfig config) {
        return (LoadingCache) buildLoadCache(LoadTypeEnum.REFRESH, config.getInitialCapacity(), config.getMaximumSize());
    }

    public static Object buildLoadCache(LoadTypeEnum loadTypeEnum) {
        return buildLoadCache(loadTypeEnum, 10, 100);
    }

    private static Object buildLoadCache(LoadTypeEnum loadTypeEnum, int initialCapacity, int maximumSize) {
        Caffeine<String, Object> caffeine = Caffeine.newBuilder()
                // 初始容量
                .initialCapacity(initialCapacity)
                // 最大容量为(基于容量进行回收)
                .maximumSize(maximumSize)
                // 监控
                .recordStats()
                //当Entry被移除时的监听器
                .removalListener((key, value, removalCause) -> log.info("remove cache:{}-{}, cause:{}", key, value, removalCause));
        if (loadTypeEnum == LoadTypeEnum.REFRESH) {
            // 配置写入后多久调用load方法刷新缓存
            caffeine.refreshAfterWrite(5, TimeUnit.SECONDS);
        } else {
            // 配置写入后多久使缓存过期
            caffeine.expireAfterWrite(5, TimeUnit.SECONDS);
        }
        if (loadTypeEnum == LoadTypeEnum.ASYNC) {
            // 异步加载
            return caffeine.buildAsync(key -> {
                String value = "value_" + CacheUtil.randomInt();
                log.info(Thread.currentThread().getName() + " load value:{}, cost 3s", value);
                Thread.sleep(3000);
                return value;
            });
        } else if (loadTypeEnum != LoadTypeEnum.MANUAL) {
            // 同步加载
            return caffeine.build(key -> {
                String value = "value_" + CacheUtil.randomInt();
                log.info(Thread.currentThread().getName() + " load value:{}, cost 3s", value);
                Thread.sleep(3000);
                return value;
            });
        } else {
            // 手动加载
            return caffeine.build();
        }
    }
}

1.2 手动加载测试

@Slf4j
public class ManualCacheTest {

    private Cache<String, Object> cache = (Cache<String, Object>) CacheUtil.buildLoadCache(LoadTypeEnum.MANUAL);

    @Test
    public void test() throws InterruptedException {
        String key = "key1";
        cache.put(key, "v1");
        log.info("exist key:" + key + "--->" + cache.getIfPresent(key));
        log.info("not exist key:key2" + "--->" + cache.getIfPresent("key2"));
        // 覆盖key
        cache.put(key, "v2");
        log.info("override key:" + key + "--->" + cache.getIfPresent(key));
        // 作废key
        cache.invalidate(key);
        log.info("invalid key:" + key + "--->" + cache.getIfPresent(key));
        // 过期失效
        cache.put(key, "v1");
        log.info("exist key:" + key + "--->" + cache.getIfPresent(key));
        Thread.sleep(6000);
        log.info("expired key:" + key + "--->" + cache.getIfPresent(key));
    }
}

输出

16:28:37.193 [main] INFO com.cache.demo.ManualCacheTest - exist key:key1--->v1
16:28:37.197 [main] INFO com.cache.demo.ManualCacheTest - not exist key:key2--->null
16:28:37.201 [main] INFO com.cache.demo.ManualCacheTest - override key:key1--->v2
16:28:37.201 [ForkJoinPool.commonPool-worker-1] INFO com.cache.CacheUtil - remove cache:key1-v1, cause:REPLACED
16:28:37.203 [main] INFO com.cache.demo.ManualCacheTest - invalid key:key1--->null
16:28:37.203 [ForkJoinPool.commonPool-worker-1] INFO com.cache.CacheUtil - remove cache:key1-v2, cause:EXPLICIT
16:28:37.204 [main] INFO com.cache.demo.ManualCacheTest - exist key:key1--->v1
16:28:43.206 [main] INFO com.cache.demo.ManualCacheTest - expired key:key1--->null

1.3 同步加载测试

@Slf4j
public class SyncLoadTest {

    LoadingCache<String, Object> loadingCache = (LoadingCache<String, Object>) CacheUtil.buildLoadCache(LoadTypeEnum.SYNC);

    @Test
    public void test() throws InterruptedException {
        for (int i = 0; i < 2; i++) {
            CacheUtil.execute(() -> {
                while (true) {
                    try {
                        log.info(Thread.currentThread().getName() + " " + loadingCache.get("123456"));
                        Thread.sleep(1000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        TimeUnit.HOURS.sleep(2);
    }
}

输出

16:31:04.328 [pool-1-thread-1] INFO com.cache.CacheUtil - pool-1-thread-1 load value:value_647, cost 3s
16:31:07.342 [pool-1-thread-1] INFO com.cache.demo.SyncLoadTest - pool-1-thread-1 value_647
16:31:07.344 [pool-1-thread-2] INFO com.cache.demo.SyncLoadTest - pool-1-thread-2 value_647
16:31:08.344 [pool-1-thread-1] INFO com.cache.demo.SyncLoadTest - pool-1-thread-1 value_647
16:31:08.344 [pool-1-thread-2] INFO com.cache.demo.SyncLoadTest - pool-1-thread-2 value_647
16:31:09.347 [pool-1-thread-1] INFO com.cache.demo.SyncLoadTest - pool-1-thread-1 value_647
16:31:09.347 [pool-1-thread-2] INFO com.cache.demo.SyncLoadTest - pool-1-thread-2 value_647
16:31:10.348 [pool-1-thread-1] INFO com.cache.demo.SyncLoadTest - pool-1-thread-1 value_647
16:31:10.348 [pool-1-thread-2] INFO com.cache.demo.SyncLoadTest - pool-1-thread-2 value_647
16:31:11.353 [pool-1-thread-1] INFO com.cache.demo.SyncLoadTest - pool-1-thread-1 value_647
16:31:11.353 [pool-1-thread-2] INFO com.cache.demo.SyncLoadTest - pool-1-thread-2 value_647
16:31:12.359 [pool-1-thread-1] INFO com.cache.CacheUtil - pool-1-thread-1 load value:value_822, cost 3s
16:31:15.361 [pool-1-thread-2] INFO com.cache.demo.SyncLoadTest - pool-1-thread-2 value_822

16:31:15.363 [ForkJoinPool.commonPool-worker-1] INFO com.cache.CacheUtil - remove cache:123456-value_647, cause:EXPIRED
16:31:15.363 [pool-1-thread-1] INFO com.cache.demo.SyncLoadTest - pool-1-thread-1 value_822

可以看到缓存过期后其中一个读线程阻塞去加载数据,别的线程都阻塞等待

1.4 异步加载测试

@Slf4j
public class AsyncCacheTest {

    AsyncLoadingCache<String, Object> loadingCache = (AsyncLoadingCache<String, Object>) CacheUtil.buildLoadCache(LoadTypeEnum.ASYNC);

    @Test
    public void test() {
        while (true) {
            try {
                CompletableFuture<Object> future = loadingCache.get("123456");
                Thread.sleep(1000);
                log.info("执行别的业务,耗时1s");
                log.info("读取缓存:" + future.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

输出

16:38:22.751 [main] INFO com.cache.demo.AsyncCacheTest - 执行别的业务,耗时1s
16:38:22.751 [main] INFO com.cache.demo.AsyncCacheTest - 读取缓存:value_922
16:38:23.755 [main] INFO com.cache.demo.AsyncCacheTest - 执行别的业务,耗时1s
16:38:23.756 [main] INFO com.cache.demo.AsyncCacheTest - 读取缓存:value_922
16:38:23.758 [ForkJoinPool.commonPool-worker-2] INFO com.cache.CacheUtil - ForkJoinPool.commonPool-worker-2 load value:value_457, cost 3s
16:38:23.759 [ForkJoinPool.commonPool-worker-1] INFO com.cache.CacheUtil - remove cache:123456-value_922, cause:EXPIRED
16:38:24.763 [main] INFO com.cache.demo.AsyncCacheTest - 执行别的业务,耗时1s
16:38:26.763 [main] INFO com.cache.demo.AsyncCacheTest - 读取缓存:value_457

16:38:27.764 [main] INFO com.cache.demo.AsyncCacheTest - 执行别的业务,耗时1s

可以看到异步加载利用future模式实现加载缓存的同时不影响别的业务

1.5 自动刷新缓存

public class RefreshLoadTest {

    LoadingCache<String, Object> loadingCache = (LoadingCache<String, Object>) CacheUtil.buildLoadCache(LoadTypeEnum.REFRESH);

    @Test
    public void test() throws InterruptedException {
        for (int i = 0; i < 2; i++) {
            CacheUtil.execute(() -> {
                while (true) {
                    try {
                        log.info(Thread.currentThread().getName() + " " + loadingCache.get("123456"));
                        Thread.sleep(1000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        TimeUnit.HOURS.sleep(2);
    }
}

输出

16:41:20.254 [pool-1-thread-2] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-2 value_704
16:41:21.254 [pool-1-thread-2] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-2 value_704
16:41:21.255 [pool-1-thread-1] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-1 value_704
16:41:22.258 [pool-1-thread-2] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-2 value_704
16:41:22.261 [ForkJoinPool.commonPool-worker-1] INFO com.cache.CacheUtil - ForkJoinPool.commonPool-worker-1 load value:value_138, cost 3s
16:41:22.265 [pool-1-thread-1] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-1 value_704
16:41:23.263 [pool-1-thread-2] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-2 value_704

16:41:23.269 [pool-1-thread-1] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-1 value_704
16:41:24.267 [pool-1-thread-2] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-2 value_704
16:41:24.270 [pool-1-thread-1] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-1 value_704
16:41:25.269 [pool-1-thread-2] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-2 value_704
16:41:25.272 [pool-1-thread-1] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-1 value_704
16:41:25.277 [ForkJoinPool.commonPool-worker-2] INFO com.cache.CacheUtil - remove cache:123456-value_704, cause:REPLACED
16:41:26.270 [pool-1-thread-2] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-2 value_138
16:41:26.274 [pool-1-thread-1] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-1 value_138
16:41:27.274 [pool-1-thread-2] INFO com.cache.demo.RefreshLoadTest - pool-1-thread-2 value_138

可以看到缓存过期后读线程都返回老数据,单开一个线程加载数据

1.6 统计命中率

public class CacheStatTest {

    LoadingCache<String, Object> loadingCache = (LoadingCache<String, Object>) CacheUtil.buildLoadCache(LoadTypeEnum.REFRESH);

    @Test
    public void test() throws InterruptedException {
        for (int j = 0; j < 5; j++) {
            for (int i = 0; i < 10; i++) {
                loadingCache.get(i + "");
            }
        }
        loadingCache.get("11");
        CacheStats stats = loadingCache.stats();
        log.info("hit count:{}, missCount:{}, hit rate:{}",
                stats.hitCount(), stats.missCount(), stats.hitRate());
        TimeUnit.HOURS.sleep(1);
    }
}

测试前先注释掉工具类中的睡眠代码

输出:16:51:45.128 [main] INFO com.cache.demo.CacheStatTest - hit count:40, missCount:11, hit rate:0.7843137254901961

2 源码浅析

开发中常用1.5 自动刷新缓存,就看看LoadingCache.get()中做了什么

追到com.github.benmanes.caffeine.cache.BoundedLocalCache#computeIfAbsent

2.1 computeIfAbsent方法概览

  public @Nullable V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction,
      boolean recordStats, boolean recordLoad) {
    requireNonNull(key);
    // mappingFunction就是用cacheLoader构建的
    requireNonNull(mappingFunction);
    long now = expirationTicker().read();

    // data是ConcurrentHashMap
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
    if (node != null) {
      V value = node.getValue();
      if ((value != null) && !hasExpired(node, now)) {
        if (!isComputingAsync(node)) {
          // 如果采用expireAfterRead过期策略则修改过期时间
          tryExpireAfterRead(node, key, value, expiry(), now);
          // 修改缓存访问时间
          setAccessTime(node, now);
        }
        // 读缓存后的操作
        afterRead(node, now, /* recordHit */ recordStats);
        return value;
      }
    }
    if (recordStats) {
      // 统计信息,没有命中次数增加
      mappingFunction = statsAware(mappingFunction, recordLoad);
    }
    // 根据引用类型重新构建key
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
    // 2.2 计算缓存value
    return doComputeIfAbsent(key, keyRef, mappingFunction, new long[] { now }, recordStats);
  }

computeIfAbsent方法主要做了如下操作

1 根据key取node

2 如果node不为空且没过期则做一些额外操作,然后返回value

3 如果node为空则先统计命中信息,然后加载缓存

2.2 doComputeIfAbsent方法分析

 @Nullable V doComputeIfAbsent(K key, Object keyRef,
      Function<? super K, ? extends V> mappingFunction, long[/* 1 */] now, boolean recordStats) {
    // 旧值引用
    V[] oldValue = (V[]) new Object[1];
    // 新值引用
    V[] newValue = (V[]) new Object[1];
    // key引用
    K[] nodeKey = (K[]) new Object[1];
    // 移除的node引用
    Node<K, V>[] removed = new Node[1];

    int[] weight = new int[2]; // old, new
    RemovalCause[] cause = new RemovalCause[1];
    Node<K, V> node = data.compute(keyRef, (k, n) -> {
      if (n == null) {
        // 根据key获取新的value
        newValue[0] = mappingFunction.apply(key);
        if (newValue[0] == null) {
          return null;
        }
        now[0] = expirationTicker().read();
        weight[1] = weigher.weigh(key, newValue[0]);
        // 构建value node
        n = nodeFactory.newNode(key, keyReferenceQueue(),
            newValue[0], valueReferenceQueue(), weight[1], now[0]);
        setVariableTime(n, expireAfterCreate(key, newValue[0], expiry(), now[0]));
        return n;
      }
      // 如果旧值不为空则进行如下处理
      synchronized (n) {
        // 根据老的k-v设置移除原因
        nodeKey[0] = n.getKey();
        weight[0] = n.getWeight();
        oldValue[0] = n.getValue();
        if ((nodeKey[0] == null) || (oldValue[0] == null)) {
          cause[0] = RemovalCause.COLLECTED;
        } else if (hasExpired(n, now[0])) {
          cause[0] = RemovalCause.EXPIRED;
        } else {
          return n;
        }

        writer.delete(nodeKey[0], oldValue[0], cause[0]);
        // 计算新值
        newValue[0] = mappingFunction.apply(key);
        if (newValue[0] == null) {
          removed[0] = n;
          // 旧值清理
          n.retire();
          return null;
        }
        weight[1] = weigher.weigh(key, newValue[0]);
        // 旧node设置新值
        n.setValue(newValue[0], valueReferenceQueue());
        n.setWeight(weight[1]);

        now[0] = expirationTicker().read();
        // 设置读写时间
        setVariableTime(n, expireAfterCreate(key, newValue[0], expiry(), now[0]));
        setAccessTime(n, now[0]);
        setWriteTime(n, now[0]);
        return n;
      }
    });

    if (node == null) {
      if (removed[0] != null) {
        // 添加一个旧node移除任务
        afterWrite(new RemovalTask(removed[0]));
      }
      return null;
    }
    if (cause[0] != null) {
      if (hasRemovalListener()) {
        // 如果旧值移除原因不为空并且配置了移除监听则进行通知
        notifyRemoval(nodeKey[0], oldValue[0], cause[0]);
      }
      statsCounter().recordEviction(weight[0], cause[0]);
    }
    if (newValue[0] == null) {
      if (!isComputingAsync(node)) {
        // 如果设置了expireAfterRead则延长过期时间
        tryExpireAfterRead(node, key, oldValue[0], expiry(), now[0]);
        // 设置访问时间
        setAccessTime(node, now[0]);
      }
      // node读取后的操作
      afterRead(node, now[0], /* recordHit */ recordStats);
      // 返回旧值
      return oldValue[0];
    }
    if ((oldValue[0] == null) && (cause[0] == null)) {
      // 添加一个添加node的任务
      afterWrite(new AddTask(node, weight[1]));
    } else {
      int weightedDifference = (weight[1] - weight[0]);
      // 添加一个修改node的任务
      afterWrite(new UpdateTask(node, weightedDifference));
    }

    return newValue[0];
  }

doComputeIfAbsent主要做了如下操作

1 计算新值

2 旧值移除相关操作(添加移除任务,旧值移除监听处理)

3 新值添加相关操作(添加添加node、修改node任务)

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值