一个简单的 Cache 淘汰策略

本文是《轻量级 Java Web 框架架构设计》的系列博文。 java

Smart 框架一切都围绕着轻量、简单、实用的方向在努力,对于 Cache 插件也不例外。最近忙着拉项目,因此投入在 Smart 的精力就很少了。前几天有朋友想让我写一个 Cache 淘汰策略,当时脑海里有几个算法,例如: git

  • LRU(Least Recently Used):最近最少使用算法,淘汰“最后访问时间”较早的。
  • LFU(Least Frequently Used):最不常常使用算法,淘汰“访问次数”较少的。
  • FIFO(First In First Out):先进先出算法,淘汰“建立时间”较早的。

固然还有其余更加优秀的算法,我目前只想选择一种最简单的做为缺省的实现,对于特定的需求,未来还能够进行扩展。 算法

以上前两种比较相似,都是针对是访问时间或访问次数来进行淘汰,稍许有些复杂,因此我选择了 FIFO 的算法,经过建立时间来判断是否淘汰。具体来讲,在放入 Cache 的时候指定一个过时时间(1分钟、10分钟、1个小时、1天等),再用一个线程去轮询放在 Cache 里的过时时间,若已过时,则直接从 Cache 中移除。 架构

这种策略每每实现起来比较简单,也很是实用,不妨先实现这种算法吧。 框架

第一步:定义一个 Expiry 常量接口 ide

public interface Expiry {

    long ETERNAL = -1;
    long ZERO = 0;
    long ONE_MINUTE = 60 * 1000;
    long FIVE_MINUTES = 5 * ONE_MINUTE;
    long TEN_MINUTES = 10 * ONE_MINUTE;
    long TWENTY_MINUTES = 20 * ONE_MINUTE;
    long THIRTY_MINUTES = 30 * ONE_MINUTE;
    long ONE_HOUR = 60 * ONE_MINUTE;
    long ONE_DAY = 24 * ONE_HOUR;
}

该接口里面有许多经常使用的时间选项。该接口的定义参考了 JSR 107 规范。 this

第二步:定义一个 Duration 类 spa

public class Duration {

    private long start;  // 开始时间
    private long expiry; // 过时时长(毫秒)

    public Duration(long start, long expiry) {
        this.start = start;
        this.expiry = expiry;
    }

    public long getStart() {
        return start;
    }

    public long getExpiry() {
        return expiry;
    }
}

其中包括了开始时间,也就是将数据放入 Cache 的时间。此外还包括一个过时时长,用毫秒表示,也就是多长时间过时,这个字段可使用 Expiry 常量接口中的选项。 .net

第三步:为 CachePut 注解添加一个 expiry 属性 插件

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CachePut {

    String value();

    long expiry() default Expiry.ETERNAL;
}

默认过时时间为 Expiry.ETERNAL,也就是永不过时的意思。

第四步:扩展几个 Cache 相关

Cache 接口:

public interface Cache<K, V> {

    // 从 Cache 中获取数据
    V get(K key);

    // 将数据放入 Cache 中
    void put(K key, V value);

    // 将数据放入 Cache 中,指定有效期(ms)
    void put(K key, V value, long expiry);

    // 从 Cache 中移除数据
    void remove(K key);

    // 清空 Cache
    void clear();

    // 获取全部的 Duration
    Map<K, Duration> getDurations();
}

添加了两个方法:

void put(K key, V value, long expiry); 当初始化 Cache 的时候能够传入一个过时时间。
Map<K, Duration> getDurations(); 返回全部的过时对象,也就是一组 Cache key 与 Duration 的映射关系。

CacheManager 接口:

public interface CacheManager {

    // 获取全部的 Cache
    Iterable<Cache> getCaches();

    // 建立 Cache
    <K, V> Cache<K, V> createCache(String cacheName);

    // 获取 Cache
    <K, V> Cache<K, V> getCache(String cacheName);

    // 销毁指定 Cache
    void destroyCache(String cacheName);
}

为该接口添加了一个方法:

Iterable<Cache> getCaches(); 返回 CacheManager 中所管理的全部 Cache 对象。为了在循环中迭代,因此返回了 Iterable 接口。

CacheFactory 工厂类:

public class CacheFactory {

    // 定义一个 CacheManager Map,用于存放目标类与 CacheManager 的对应关系(一个目标类对应一个 CacheManager),目标类通常为 Service 类
    private static final Map<Class<?>, CacheManager> cacheManagerMap = new HashMap<Class<?>, CacheManager>();

    public static Iterable<CacheManager> getCacheManagers() {
        return cacheManagerMap.values();
    }
...
}

对外提供了一个 getCacheManagers 方法,便于访问 CacheFactory 中的私有成员 cacheManagerMap。

具体的实现,请参考 Smart Cache Plugin 源码

第五步:提供一个 CacheThread,用于实现淘汰算法

public class CacheThread extends Thread {

    @Override
    @SuppressWarnings("unchecked")
    public void run() {
        try {
            while (true) {
                // 遍历全部的 Cache Manager
                Iterable<CacheManager> cacheManagers = CacheFactory.getCacheManagers();
                for (CacheManager cacheManager : cacheManagers) {
                    // 遍历全部的 Cache
                    Iterable<Cache> caches = cacheManager.getCaches();
                    for (Cache cache : caches) {
                        // 遍历全部的 Duration Map
                        Map<Object, Duration> durationMap = cache.getDurations();
                        for (Object entrySet : durationMap.entrySet()) {
                            // 获取 Duration Map 中的 key 与 value
                            Map.Entry<Object, Duration> entry = (Map.Entry<Object, Duration>) entrySet;
                            Object cacheKey = entry.getKey();
                            Duration duration = entry.getValue();
                            // 获取 Duration 中的相关数据
                            long start = duration.getStart();   // 开始时间
                            long expiry = duration.getExpiry(); // 过时时长
                            // 获取当前时间
                            long current = System.currentTimeMillis();
                            // 判断是否已过时
                            if (current - start >= expiry) {
                                // 若已过时,则首先移除 Cache(也会同时移除 Duration Map 中对应的条目)
                                cache.remove(cacheKey);
                            }
                        }
                    }
                }
                // 使线程休眠 5 秒钟
                sleep(5000);
            }
        } catch (InterruptedException e) {
            throw new CacheException("错误:运行 CacheThead 出现异常!", e);
        }
    }
}

以上代码从 CacheManager 开始进行遍历,最终获取相应的 Cache,并从中获取 Duration,经过很简单的方法就能判断出 Cache 是否已过时,过时了就从 Cache 中移除,同时也要移除 Duration Map 中相应的条目。

这个线程如何才能开启呢?须要找个地方初始化。

第六步:建立一个 CachePlugin 类并实现 Plugin 接口

public class CachePlugin implements Plugin {

    @Override
    public void init() {
        new CacheThread().start();
    }
}

Plugin 接口由 Smart 框架提供,此时只需实现该接口,在 init 方法中完成初始化工做便可,也就是开启 CacheThread 这个线程。

还差什么呢?那就是如何去使用这个新的特性了!

最后一步:配置 Cache 过时时间

不妨使用注解的方式来配置过时时间,固然也能够经过 API 的方式来设置。

@Bean
@Cachable
public class CustomerServiceCacheAnnotationImpl extends BaseService implements CustomerService {

    @Override
    @CachePut(value = "customer_list_cache", expiry = Expiry.ONE_MINUTE)
    public List<Customer> getCustomerList() {
        return DataSet.selectList(Customer.class, "", "");
    }
...
}

这里将 customer_list_cache 这个名称的 Cache 的过时时间设置为 1 分钟。

OK,打完收工!回家抱小孩了。

期待您的意见或建议!

相关文章
相关标签/搜索