java从零手写实现redis(一)如何实现固定大小的缓存?java
java从零手写实现redis(三)redis expire 过时原理git
java从零手写实现redis(三)内存数据如何重启不丢失?github
前面实现了 redis 的几个基本特性,其中在 expire 过时原理时,提到了另一种实现方式。算法
这里将其记录下来,能够拓展一下本身的思路。缓存
原来的实现方式见:数据结构
java从零手写实现redis(三)redis expire 过时原理
https://mp.weixin.qq.com/s/BW...框架
之前的设计很是简单,符合最基本的思路,就是将过时的信息放在一个 map 中,而后去遍历清空。dom
为了不单次操做时间过长,相似 redis,单次操做 100 个元素以后,直接返回。ide
不过定时任务之心时,其实存在两个不足:
(1)keys 的选择不够随机,可能会致使每次循环 100 个结束时,真正须要过时的没有被遍历到。
不过 map 的随机比较蠢,就是将 map 的 keys 所有转为集合,而后经过 random 返回。
转的过程就是一个时间复杂度为 O(n) 的遍历,因此一开始没有去实现。
还有一种方式,就是用空间换区时间,存储的时候,同时存储在 list 中,而后随机返回处理,这个后续优化。
(2)keys 的遍历可能大部分都是无效的。
咱们每次都是根据 keys 从前日后遍历,可是没有关心对应的过时时间,因此致使不少无效遍历。
本文主要提供一种以过时时间为维度的实现方式,仅供参考,由于这种方式也存在缺陷。
咱们每次 put 放入过时元素时,根据过时时间对元素进行排序,相同的过时时间的 Keys 放在一块儿。
优势:定时遍历的时候,若是时间不到当前时间,就能够直接返回了,大大下降无效遍历。
缺点:考虑到惰性删除问题,仍是须要存储以删除信息做为 key 的 map 关系,这样内存基本翻倍。
咱们这里使用 TreeMap
帮助咱们进行过时时间的排序,这个集合后续有时间能够详细讲解了,我大概看了下 jdk1.8 的源码,主要是经过红黑树实现的。
public class CacheExpireSort<K,V> implements ICacheExpire<K,V> { /** * 单次清空的数量限制 * @since 0.0.3 */ private static final int LIMIT = 100; /** * 排序缓存存储 * * 使用按照时间排序的缓存处理。 * @since 0.0.3 */ private final Map<Long, List<K>> sortMap = new TreeMap<>(new Comparator<Long>() { @Override public int compare(Long o1, Long o2) { return (int) (o1-o2); } }); /** * 过时 map * * 空间换时间 * @since 0.0.3 */ private final Map<K, Long> expireMap = new HashMap<>(); /** * 缓存实现 * @since 0.0.3 */ private final ICache<K,V> cache; }
每次存入新元素时,同时放入 sortMap 和 expireMap。
@Override public void expire(K key, long expireAt) { List<K> keys = sortMap.get(expireAt); if(keys == null) { keys = new ArrayList<>(); } keys.add(key); // 设置对应的信息 sortMap.put(expireAt, keys); expireMap.put(key, expireAt); }
咱们定义一个定时任务,100ms 执行一次。
/** * 线程执行类 * @since 0.0.3 */ private static final ScheduledExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor(); public CacheExpireSort(ICache<K, V> cache) { this.cache = cache; this.init(); } /** * 初始化任务 * @since 0.0.3 */ private void init() { EXECUTOR_SERVICE.scheduleAtFixedRate(new ExpireThread(), 100, 100, TimeUnit.MILLISECONDS); }
实现源码以下:
/** * 定时执行任务 * @since 0.0.3 */ private class ExpireThread implements Runnable { @Override public void run() { //1.判断是否为空 if(MapUtil.isEmpty(sortMap)) { return; } //2. 获取 key 进行处理 int count = 0; for(Map.Entry<Long, List<K>> entry : sortMap.entrySet()) { final Long expireAt = entry.getKey(); List<K> expireKeys = entry.getValue(); // 判断队列是否为空 if(CollectionUtil.isEmpty(expireKeys)) { sortMap.remove(expireAt); continue; } if(count >= LIMIT) { return; } // 删除的逻辑处理 long currentTime = System.currentTimeMillis(); if(currentTime >= expireAt) { Iterator<K> iterator = expireKeys.iterator(); while (iterator.hasNext()) { K key = iterator.next(); // 先移除自己 iterator.remove(); expireMap.remove(key); // 再移除缓存,后续能够经过惰性删除作补偿 cache.remove(key); count++; } } else { // 直接跳过,没有过时的信息 return; } } } }
这里直接遍历 sortMap,对应的 key 就是过时时间,而后和当前时间对比便可。
删除的时候,须要删除 expireMap/sortMap/cache。
惰性删除刷新时,就会用到 expireMap。
由于有时候刷新的 key 就一个,若是没有 expireMap 映射关系,可能要把 sortMap 所有遍历一遍才能找到对应的过时时间。
就是一个时间复杂度与空间复杂度衡量的问题。
@Override public void refreshExpire(Collection<K> keyList) { if(CollectionUtil.isEmpty(keyList)) { return; } // 这样维护两套的代价太大,后续优化,暂时不用。 // 判断大小,小的做为外循环 final int expireSize = expireMap.size(); if(expireSize <= keyList.size()) { // 通常过时的数量都是较少的 for(Map.Entry<K,Long> entry : expireMap.entrySet()) { K key = entry.getKey(); // 这里直接执行过时处理,再也不判断是否存在于集合中。 // 由于基于集合的判断,时间复杂度为 O(n) this.removeExpireKey(key); } } else { for(K key : keyList) { this.removeExpireKey(key); } } } /** * 移除过时信息 * @param key key * @since 0.0.10 */ private void removeExpireKey(final K key) { Long expireTime = expireMap.get(key); if(expireTime != null) { final long currentTime = System.currentTimeMillis(); if(currentTime >= expireTime) { expireMap.remove(key); List<K> expireKeys = sortMap.get(expireTime); expireKeys.remove(key); sortMap.put(expireTime, expireKeys); } } }
实现过时的方法有不少种,目前咱们提供的两种方案,都各有优缺点,我相信会有更加优秀的方式。
程序 = 数据结构 + 算法
redis 之因此性能这么优异,其实和其中的数据结构与算法用的合理是分不开的,优秀的框架值得反复学习和思考。
文中主要讲述了思路,实现部分由于篇幅限制,没有所有贴出来。
开源地址: https://github.com/houbb/cache
以为本文对你有帮助的话,欢迎点赞评论收藏关注一波~
你的鼓励,是我最大的动力~