我是靠谱客的博主 唠叨烧鹅,这篇文章主要介绍一个简单的 Cache 淘汰策略,现在分享给大家,希望可以做个参考。

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

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

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

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

当然还有其他更加优秀的算法,我目前只想选择一种最简单的作为缺省的实现,对于特定的需求,将来还可以进行扩展。

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

这种策略往往实现起来比较简单,也非常实用,不妨先实现这种算法吧。

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

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
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 规范。

第二步:定义一个 Duration 类

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 常量接口中的选项。

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

复制代码
1
2
3
4
5
6
7
8
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface CachePut { String value(); long expiry() default Expiry.ETERNAL; }

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

第四步:扩展几个 Cache 相关

Cache 接口:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 接口:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 工厂类:

复制代码
1
2
3
4
5
6
7
8
9
10
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,用于实现淘汰算法

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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 接口

复制代码
1
2
3
4
5
6
7
public class CachePlugin implements Plugin {     @Override     public void init() {         new CacheThread().start();     } }

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

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

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

不妨使用注解的方式来配置过期时间,当然也可以通过 API 的方式来设置。

复制代码
1
2
3
4
5
6
7
8
9
10
11
@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,打完收工!回家抱小孩了。

期待您的意见或建议!

转载于:https://my.oschina.net/huangyong/blog/177559

最后

以上就是唠叨烧鹅最近收集整理的关于一个简单的 Cache 淘汰策略的全部内容,更多相关一个简单的内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(71)

评论列表共有 0 条评论

立即
投稿
返回
顶部