java 從零開始手寫 redis(五)過時策略的另外一種實現思路

前言

java從零手寫實現redis(一)如何實現固定大小的緩存?java

java從零手寫實現redis(三)redis expire 過時原理git

java從零手寫實現redis(三)內存數據如何重啓不丟失?github

java從零手寫實現redis(四)添加監聽器redis

前面實現了 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

以爲本文對你有幫助的話,歡迎點贊評論收藏關注一波~

你的鼓勵,是我最大的動力~

png

相關文章
相關標籤/搜索