模板方法模式-缓存淘汰策略有哪些?

504 阅读4分钟

这是我参与8月更文挑战的第20天,活动详情查看:8月更文挑战


1. 缓存淘汰策略有哪些?

之前做项目时,系统需要用到【支持超时的缓存】功能,本来使用Redis是非常适合的,但是由于系统技术较老旧,没有用到Redis,也不想因为这一个小场景去引入Redis,于是决定自己手写一个简单的缓存功能。

需求:

  1. 基于内存的缓存功能。
  2. 支持超时。
  3. 缓存写满后启用淘汰机制清理缓存。
  4. 需要支持多种淘汰策略,例如:FIFO、LFU、LRU等。

分析一下需求,首先是可以自定义:容量和超时时间,利用Map容器来存放缓存对象,当缓存数量达到上限后,启用淘汰机制清理缓存。这些步骤是所有缓存类共用的,唯一的不同是:不同的子类淘汰策略不一样。 因此,我们完全可以将公共的逻辑写在父类,让子类实现自己特有的逻辑即可,实现代码复用,避免冗余代码。 在这里插入图片描述

定义Cache抽象类,定义公共的属性,实现公共逻辑。

abstract class Cache<K, V> {
    Map<K, CacheObj<K, V>> cacheMap = new ConcurrentHashMap<>();//容器
    int capacity;//容量
    long timeout;//过期时间 纳秒

    // 省略构造函数

    // 添加元素
    public void put(K k, V v) {
        if (isFull()) {
                // 满了就清理
                prune();
        }
        cacheMap.put(k, new CacheObj<>(k, v, timeout));
    }

    // 获取元素
    public V get(K k) {
        ......
    }

    // 容器清理,子类不允许重写
    public final int prune() {
        // 优先清理已过期的
        Iterator<CacheObj<K, V>> iterator = cacheMap.values().iterator();
        int count = 0;
        while (iterator.hasNext()) {
            CacheObj<K, V> next = iterator.next();
            if (next.isExpired()) {
                    iterator.remove();
                    count++;
            }
        }
        if (isFull()) {
            // 还是满,执行子类强制清除算法
            return count + forcePrune();
        }
        return count;
    }

    // 强制清理,子类实现淘汰策略
    protected abstract int forcePrune();
}

上述代码中,prune()就是一个模板方法,它定义了一个算法骨架:首先遍历容器中所有的缓存,先清理一遍已过期的缓存,如果容器还是满的,则调用子类的淘汰算法,强制清理,至于子类的淘汰算法如何实现,父类并不关心。

编写CacheObj,用来包装缓存对象,记录缓存是什么时候产生的,访问了的次数,最后一次访问是什么时候,这些都是为后期的淘汰算法准备的。

class CacheObj<K,V> {
    final K k;
    final V v;
    final long expire;//过期时间
    AtomicLong accessCount;// 访问次数
    long createTime;// 生成时间
    long lastTime;// 最近一次访问时间
    // 省略Getter、Setter
}

接下来就是各种子类实现了: 先进先出

class FIFOCache<K,V> extends Cache<K,V>{

    @Override
    protected int forcePrune() {
        // 根据createTime找到最早生成的缓存,清理掉
    }
}

最少使用

class LFUCache<K, V> extends Cache<K, V> {

    @Override
    protected int forcePrune() {
        // 根据accessCount找到访问次数最少的缓存,清理掉
    }
}

最久未使用

class LRUCache<K, V> extends Cache<K, V> {

    @Override
    protected int forcePrune() {
        // 根据lastTime找到最久未使用的缓存,清理掉
    }
}

可以看到,缓存的大部分公用逻辑都在基类Cache中实现了,子类只需要实现自己的淘汰算法即可,如果要再新增一种淘汰策略也非常方便,继承Cache类,编写自己的淘汰算法即可,非常方便。 这就是模板方法模式!

2. 模板方法模式的定义

定义一个操作中的算法的框架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

在这里插入图片描述

模板方法模式通用类图
  • Template:模板类,它的方法分为两种:基本方法和模板方法。基本方法如:method1()method2()是子类可以自定义实现的方法,模板方法如logic(),它是一个算法骨架,完成固定的逻辑,子类是不可以重写的。
  • Concrete*:具体模板类,实现父类定义的基本方法,完成特有的逻辑和功能。

注意:为了防止子类恶意破坏,模板方法一般定义为final,禁止子类重写算法骨架。基本方法一般定义为protected,符合迪米特法则,不需要对客户端暴露。

3. 模板方法模式的优缺点

优点

  1. 分离程序中变与不变的部分。将固定不变的算法骨架封装在基类中,将可变的逻辑由子类扩展实现,符合开闭原则。
  2. 代码复用,避免冗余代码,将公共代码提取到基类,也便于维护。
  3. 类的整体行为由基类控制,特定的细节实现又子类扩展。
  4. 子类扩展非常简单,无需了解基类复杂的算法。

缺点 在模板方法模式中,子类的实现可能会改变父类的行为,这会使代码的阅读变得困难,调试也相对不易。

4. 总结

模板方法模式应用非常广泛,在很多开源框架中都能看到它的身影。 当多个子类有相同的逻辑时,就应该考虑使用模板方法模式,将公共可复用的逻辑提取到父类,子类只负责实现自己特有的细节即可。 对于一些非常重要或复杂的算法,也建议使用模板方法模式进行封装,避免子类实现困难。 模板方法模式还可以在算法的任意位置调用一些钩子函数来做增强。