4 【hutool】hutool-cache

913 阅读5分钟

该系列文章主要是对 hutool 工具类的介绍,详情可以参考

hutool.cn/docs/#/

此模块提供一种缓存的简单实现方案,在小型项目中对于简单的缓存需求非常好用。

4.1 缓存策略

FIFOCache

FIFO(first in first out) 先进先出策略。元素不停的加入缓存直到缓存满为止,当缓存满时,清理过期缓存对象,清理后依旧满则删除先入的缓存(链表首部对象)。

优点:简单快速 缺点:不灵活,不能保证最常用的对象总是被保留

LFUCache

LFU(least frequently used) 最少使用率策略。根据使用次数来判定对象是否被持续缓存(使用率是通过访问次数计算),当缓存满时清理过期对象,清理后依旧满的情况下清除最少访问(访问计数最小)的对象并将其他对象的访问数减去这个最小访问数,以便新对象进入后可以公平计数。

LRUCache

LRU (least recently used)最近最久未使用缓存。根据使用时间来判定对象是否被持续缓存,当对象被访问时放入缓存,当缓存满了,最久未被使用的对象将被移除。此缓存基于LinkedHashMap,因此当被缓存的对象每被访问一次,这个对象的key就到链表头部。这个算法简单并且非常快,他比FIFO有一个显著优势是经常使用的对象不太可能被移除缓存。缺点是当缓存满时,不能被很快的访问。

TimedCache

定时缓存,对被缓存的对象定义一个过期时间,当对象超过过期时间会被清理。此缓存没有容量限制,对象只有在过期后才会被移除

WeakCache

弱引用缓存。对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。丢弃某个键时,其条目从映射中有效地移除。该类使用了WeakHashMap做为其实现,缓存的清理依赖于JVM的垃圾回收。

FileCache

FileCache是一个独立的缓存,主要是将小文件以byte[]的形式缓存到内容中,减少文件的访问,以解决频繁读取文件引起的性能问题。

主要实现有:

LFUFileCache

LRUFileCache

4.2 使用方式

@Test
public void fifoCacheTest() {
    Cache<String, String> fifoCache = CacheUtil.newFIFOCache(3);
    fifoCache.setListener((key, value) -> {
        // 监听测试,此测试中只有key1被移除,测试是否监听成功
        System.out.println("key:" + key);
        System.out.println("value:" + value);
    });

    fifoCache.put("key1", "value1", DateUnit.SECOND.getMillis() * 3);
    fifoCache.put("key2", "value2", DateUnit.SECOND.getMillis() * 3);
    fifoCache.put("key3", "value3", DateUnit.SECOND.getMillis() * 3);
    fifoCache.put("key4", "value4", DateUnit.SECOND.getMillis() * 3);
    // 由于缓存容量只有3,当加入第四个元素的时候,根据FIFO规则,最先放入的对象将被移除
    String value1 = fifoCache.get("key1");
    // 应该为空
    System.out.println(value1);
}

@Test
public void lfuCacheTest() {
    Cache<String, String> lfuCache = CacheUtil.newLFUCache(3);
    lfuCache.put("key1", "value1", DateUnit.SECOND.getMillis() * 3);
    //使用次数+1
    lfuCache.get("key1");
    lfuCache.put("key2", "value2", DateUnit.SECOND.getMillis() * 3);
    lfuCache.put("key3", "value3", DateUnit.SECOND.getMillis() * 3);
    lfuCache.put("key4", "value4", DateUnit.SECOND.getMillis() * 3);

    //由于缓存容量只有3,当加入第四个元素的时候,根据LFU规则,最少使用的将被移除(2,3被移除)
    String value1 = lfuCache.get("key1");
    String value2 = lfuCache.get("key2");
    String value3 = lfuCache.get("key3");
    System.out.println(value1);
    // 应该为空
    System.out.println(value2);
    // 应该为空
    System.out.println(value3);
}

@Test
public void lruCacheTest() {
    Cache<String, String> lruCache = CacheUtil.newLRUCache(3);
    lruCache.put("key1", "value1", DateUnit.SECOND.getMillis() * 3);
    lruCache.put("key2", "value2", DateUnit.SECOND.getMillis() * 3);
    lruCache.put("key3", "value3", DateUnit.SECOND.getMillis() * 3);
    //使用时间推近
    lruCache.get("key1");
    lruCache.put("key4", "value4", DateUnit.SECOND.getMillis() * 3);

    String value1 = lruCache.get("key1");
    // 应该不为空
    System.out.println(value1);
    //由于缓存容量只有3,当加入第四个元素的时候,根据LRU规则,最少使用的将被移除(2被移除)
    String value2 = lruCache.get("key2");
    // 应该为空
    System.out.println(value2);
}

@Test
public void timedCacheTest() {
    TimedCache<String, String> timedCache = CacheUtil.newTimedCache(4);
    timedCache.put("key1", "value1", 1);//1毫秒过期
    timedCache.put("key2", "value2", DateUnit.SECOND.getMillis() * 5);//5毫秒过期
    timedCache.put("key3", "value3");//默认过期(4毫秒)
    timedCache.put("key4", "value4", Long.MAX_VALUE);//永不过期

    //启动定时任务,每5毫秒检查一次过期
    timedCache.schedulePrune(5);
    //等待5毫秒
    ThreadUtil.sleep(5);

    //5毫秒后由于value2设置了5毫秒过期,因此只有value2被保留下来
    String value1 = timedCache.get("key1");
    // 应该为空
    System.out.println(value1);
    String value2 = timedCache.get("key2");
    // 不为空
    System.out.println(value2);
    //5毫秒后,由于设置了默认过期,key3只被保留4毫秒,因此为null
    String value3 = timedCache.get("key3");
    // 应该为空
    System.out.println(value3);

    String value3Supplier = timedCache.get("key3", () -> "Default supplier");
    // 过期了,但是会有默认值
    System.out.println(value3Supplier);

    String value4 = timedCache.get("key4");
    // 永不过期
    System.out.println(value4);

    //取消定时清理
    timedCache.cancelPruneSchedule();
}

4.3 实现原理

重点关注各个策略的 pruneCache 方法

FIFOCache

/**
 * 先进先出的清理策略<br>
 * 先遍历缓存清理过期的缓存对象,如果清理后还是满的,则删除第一个缓存对象
 */
@Override
protected int pruneCache() {
   int count = 0;
   CacheObj<K, V> first = null;

   // 清理过期对象并找出链表头部元素(先入元素)
   final Iterator<CacheObj<K, V>> values = cacheObjIter();
   if (isPruneExpiredActive()) {
      // 清理过期对象并找出链表头部元素(先入元素)
      while (values.hasNext()) {
         CacheObj<K, V> co = values.next();
         if (co.isExpired()) {
            values.remove();
            onRemove(co.key, co.obj);
            count++;
            continue;
         }
         if (first == null) {
            first = co;
         }
      }
   } else {
      first = values.hasNext() ? values.next() : null;
   }

   // 清理结束后依旧是满的,则删除第一个被缓存的对象
   if (isFull() && null != first) {
      removeWithoutLock(first.key, false);
      onRemove(first.key, first.obj);
      count++;
   }
   return count;
}

LFUCache

/**
 * 清理过期对象。<br>
 * 清理后依旧满的情况下清除最少访问(访问计数最小)的对象并将其他对象的访问数减去这个最小访问数,以便新对象进入后可以公平计数。
 *
 * @return 清理个数
 */
@Override
protected int pruneCache() {
   int count = 0;
   CacheObj<K, V> comin = null;

   // 清理过期对象并找出访问最少的对象
   Iterator<CacheObj<K, V>> values = cacheObjIter();
   CacheObj<K, V> co;
   while (values.hasNext()) {
      co = values.next();
      if (co.isExpired() == true) {
         values.remove();
         onRemove(co.key, co.obj);
         count++;
         continue;
      }

      //找出访问最少的对象
      if (comin == null || co.accessCount.get() < comin.accessCount.get()) {
         comin = co;
      }
   }

   // 减少所有对象访问量,并清除减少后为0的访问对象
   if (isFull() && comin != null) {
      long minAccessCount = comin.accessCount.get();

      values = cacheObjIter();
      CacheObj<K, V> co1;
      while (values.hasNext()) {
         co1 = values.next();
         if (co1.accessCount.addAndGet(-minAccessCount) <= 0) {
            values.remove();
            onRemove(co1.key, co1.obj);
            count++;
         }
      }
   }

   return count;
}

LRUCache

/**
 * 只清理超时对象,LRU的实现会交给{@code LinkedHashMap}
 */
@Override
protected int pruneCache() {
   if (isPruneExpiredActive() == false) {
      return 0;
   }
   int count = 0;
   Iterator<CacheObj<K, V>> values = cacheObjIter();
   CacheObj<K, V> co;
   while (values.hasNext()) {
      co = values.next();
      if (co.isExpired()) {
         values.remove();
         onRemove(co.key, co.obj);
         count++;
      }
   }
   return count;
}

TimeCache

/**
 * 清理过期对象
 *
 * @return 清理数
 */
@Override
protected int pruneCache() {
   int count = 0;
   final Iterator<CacheObj<K, V>> values = cacheObjIter();
   CacheObj<K, V> co;
   while (values.hasNext()) {
      co = values.next();
      if (co.isExpired()) {
         values.remove();
         onRemove(co.key, co.obj);
         count++;
      }
   }
   return count;
}

正常清理,另外提供了 schedulePrune 方法来定时清理

/**
 * 定时清理
 *
 * @param delay 间隔时长,单位毫秒
 */
public void schedulePrune(long delay) {
   this.pruneJobFuture = GlobalPruneTimer.INSTANCE.schedule(this::prune, delay);
}