亚洲视频二区_亚洲欧洲日本天天堂在线观看_日韩一区二区在线观看_中文字幕不卡一区

公告:魔扣目錄網(wǎng)為廣大站長(zhǎng)提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請(qǐng)做好本站友鏈:【 網(wǎng)站目錄:http://www.430618.com 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

Redis:緩存被我寫(xiě)滿了,該怎么辦?

 

介紹

Redis:緩存被我寫(xiě)滿了,該怎么辦?

 

redis是一個(gè)內(nèi)存數(shù)據(jù)庫(kù),當(dāng)Redis使用的內(nèi)存超過(guò)物理內(nèi)存的限制后,內(nèi)存數(shù)據(jù)會(huì)和磁盤產(chǎn)生頻繁的交換,交換會(huì)導(dǎo)致Redis性能急劇下降。所以在生產(chǎn)環(huán)境中我們通過(guò)配置參數(shù)maxmemoey來(lái)限制使用的內(nèi)存大小。

當(dāng)實(shí)際使用的內(nèi)存超過(guò)maxmemoey后,Redis提供了如下幾種可選策略。

noeviction:寫(xiě)請(qǐng)求返回錯(cuò)誤

volatile-lru:使用lru算法刪除設(shè)置了過(guò)期時(shí)間的鍵值對(duì) volatile-lfu:使用lfu算法刪除設(shè)置了過(guò)期時(shí)間的鍵值對(duì) volatile-random:在設(shè)置了過(guò)期時(shí)間的鍵值對(duì)中隨機(jī)進(jìn)行刪除 volatile-ttl:根據(jù)過(guò)期時(shí)間的先后進(jìn)行刪除,越早過(guò)期的越先被刪除

allkeys-lru:在所有鍵值對(duì)中,使用lru算法進(jìn)行刪除 allkeys-lfu:在所有鍵值對(duì)中,使用lfu算法進(jìn)行刪除 allkeys-random:所有鍵值對(duì)中隨機(jī)刪除

我們來(lái)詳細(xì)了解一下lru和lfu算法,這是2個(gè)常見(jiàn)的緩存淘汰算法。「因?yàn)橛?jì)算機(jī)緩存的容量是有限的,所以我們要?jiǎng)h除那些沒(méi)用的數(shù)據(jù),而這兩種算法的區(qū)別就是判定沒(méi)用的緯度不一樣。」

LRU算法

「lru(Least recently used,最近最少使用)算法,即最近訪問(wèn)的數(shù)據(jù),后續(xù)很大概率還會(huì)被訪問(wèn)到,即是有用的。而長(zhǎng)時(shí)間未被訪問(wèn)的數(shù)據(jù),應(yīng)該被淘汰」

lru算法中數(shù)據(jù)會(huì)被放到一個(gè)鏈表中,鏈表的頭節(jié)點(diǎn)為最近被訪問(wèn)的數(shù)據(jù),鏈表的尾節(jié)點(diǎn)為長(zhǎng)時(shí)間沒(méi)有被訪問(wèn)的數(shù)據(jù)

Redis:緩存被我寫(xiě)滿了,該怎么辦?

 

「lru算法的核心實(shí)現(xiàn)就是哈希表加雙向鏈表」。鏈表可以用來(lái)維護(hù)訪問(wèn)元素的順序,而hash表可以幫我們?cè)贠(1)時(shí)間復(fù)雜度下訪問(wèn)到元素。

「至于為什么是雙向鏈表呢」?主要是要?jiǎng)h除元素,所以要獲取前繼節(jié)點(diǎn)。數(shù)據(jù)結(jié)構(gòu)圖示如下

Redis:緩存被我寫(xiě)滿了,該怎么辦?

 

使用雙向鏈表+HashMap

雙向鏈表節(jié)點(diǎn)定義如下

public class ListNode<K, V> {
    K key;
    V value;
    ListNode pre;
    ListNode next;

    public ListNode() {}

    public ListNode(K key, V value) {
        this.key = key;
        this.value = value;
    }
}

封裝雙向鏈表的常用操作

public class DoubleList {

    private ListNode head;
    private ListNode tail;

    public DoubleList() {
        head = new ListNode();
        tail = new ListNode();
        head.next = tail;
        tail.pre = head;
    }

    public void remove(ListNode node) {
        node.pre.next = node.next;
        node.next.pre = node.pre;
    }

    public void addLast(ListNode node) {
        node.pre = tail.pre;
        tail.pre = node;
        node.pre.next = node;
        node.next = tail;
    }

    public ListNode removeFirst() {
        if (head.next == tail) {
            return null;
        }
        ListNode first = head.next;
        remove(first);
        return first;
    }
}

封裝一個(gè)緩存類,提供最基本的get和put方法。「需要注意,這兩種基本的方法都涉及到對(duì)兩種數(shù)據(jù)結(jié)構(gòu)的修改」

public class MyLruCache<K, V> {

    private int capacity;
    private DoubleList doubleList;
    private Map<K, ListNode> map;

    public MyLruCache(int capacity) {
        this.capacity = capacity;
        map = new HashMap<>();
        doubleList = new DoubleList();
    }

    public V get(Object key) {
        ListNode<K, V> node = map.get(key);
        if (node == null) {
            return null;
        }
        // 先刪除該節(jié)點(diǎn),再接到尾部
        doubleList.remove(node);
        doubleList.addLast(node);
        return node.value;
    }

    public void put(K key, V value) {
        // 直接調(diào)用這邊的get方法,如果存在,它會(huì)在get內(nèi)部被移動(dòng)到尾巴,不用再移動(dòng)一遍,直接修改值即可
        if ((get(key)) != null) {
            map.get(key).value = value;
            return;
        }

        // 如果超出容量,把頭去掉
        if (map.size() == capacity) {
            ListNode listNode = doubleList.removeFirst();
            map.remove(listNode.key);
        }

        // 若不存在,new一個(gè)出來(lái)
        ListNode node = new ListNode(key, value);
        map.put(key, node);
        doubleList.addLast(node);
    }
}

這里我們的實(shí)現(xiàn)為最近訪問(wèn)的放在鏈表的尾節(jié)點(diǎn),不經(jīng)常訪問(wèn)的放在鏈表的頭節(jié)點(diǎn)

測(cè)試一波,輸出為鏈表的正序輸出(代碼為了簡(jiǎn)潔沒(méi)有貼toString方法)

MyLruCache<String, String> myLruCache = new MyLruCache<>(3);
// {5 : 5}
myLruCache.put("5", "5");
// {5 : 5}{3 : 3}
myLruCache.put("3", "3");
// {5 : 5}{3 : 3}{4 : 4}
myLruCache.put("4", "4");
// {3 : 3}{4 : 4}{2 : 2}
myLruCache.put("2", "2");
// {4 : 4}{2 : 2}{3 : 3}
myLruCache.get("3");

「因?yàn)長(zhǎng)inkedHashMap的底層實(shí)現(xiàn)就是哈希表加雙向鏈表,所以你可以用LinkedHashMap替換HashMap和DoubleList來(lái)改寫(xiě)一下上面的類」

我來(lái)演示一下更騷的操作,只需要重寫(xiě)一個(gè)構(gòu)造函數(shù)和removeEldestEntry方法即可。

使用LinkedHashMap實(shí)現(xiàn)LRU

public class LruCache<K, V> extends LinkedHashMap<K, V> {

    private int cacheSize;


    public LruCache(int cacheSize) {
        /**
         * initialCapacity: 初始容量大小
         * loadFactor: 負(fù)載因子
         * accessOrder: false基于插入排序(默認(rèn)),true基于訪問(wèn)排序
         */
        super(cacheSize, 0.75f, true);
        this.cacheSize = cacheSize;
    }

    /**
     * 當(dāng)調(diào)用put或者putAll方法時(shí)會(huì)調(diào)用如下方法,是否刪除最老的數(shù)據(jù),默認(rèn)為false
     */
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > cacheSize;
    }
}

注意這個(gè)緩存并不是線程安全的,可以調(diào)用Collections.synchronizedMap方法返回線程安全的map

LruCache<String, String> lruCache = new LruCache(3);
Map<String, String> safeMap = Collections.synchronizedMap(lruCache);

Collections.synchronizedMap實(shí)現(xiàn)線程安全的方式很簡(jiǎn)單,只是返回一個(gè)代理類。代理類對(duì)Map接口的所有方法加鎖

public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
    return new SynchronizedMap<>(m);
}

LFU算法

「LRU算法有一個(gè)問(wèn)題,當(dāng)一個(gè)長(zhǎng)時(shí)間不被訪問(wèn)的key,偶爾被訪問(wèn)一下后,可能會(huì)造成一個(gè)比這個(gè)key訪問(wèn)更頻繁的key被淘汰。」

即LRU算法對(duì)key的冷熱程度的判斷可能不準(zhǔn)確。而LFU算法(Least Frequently Used,最不經(jīng)常使用)則是按照訪問(wèn)頻率來(lái)判斷key的冷熱程度的,每次刪除的是一段時(shí)間內(nèi)訪問(wèn)頻率較低的數(shù)據(jù),比LRU算法更準(zhǔn)確。

使用3個(gè)hash表實(shí)現(xiàn)lfu算法

那么我們應(yīng)該如何組織數(shù)據(jù)呢?

為了實(shí)現(xiàn)鍵值的對(duì)快速訪問(wèn),用一個(gè)map來(lái)保存鍵值對(duì)

private HashMap<K, Integer> keyToFreq;

還需要用一個(gè)map來(lái)保存鍵的訪問(wèn)頻率

private HashMap<K, Integer> keyToFreq;

「當(dāng)然你也可以把值和訪問(wèn)頻率封裝到一個(gè)類中,用一個(gè)map來(lái)替代上述的2個(gè)map」

接下來(lái)就是最核心的部分,刪除訪問(wèn)頻率最低的數(shù)據(jù)。

  1. 為了能在O(1)時(shí)間復(fù)雜度內(nèi)找到訪問(wèn)頻率最低的數(shù)據(jù),我們需要一個(gè)變量minFreq記錄訪問(wèn)最低的頻率
  2. 每個(gè)訪問(wèn)頻率有可能對(duì)應(yīng)多個(gè)鍵。當(dāng)空間不夠用時(shí),我們要?jiǎng)h除最早被訪問(wèn)的數(shù)據(jù),所以需要如下數(shù)據(jù)結(jié)構(gòu),Map<頻率, 有序集合>。每次內(nèi)存不夠用時(shí),刪除有序集合的第一個(gè)元素即可。并且這個(gè)有序集合要能快速刪除某個(gè)key,因?yàn)槟硞€(gè)key被訪問(wèn)后,需要從這個(gè)集合中刪除,加入freq+1對(duì)應(yīng)的集合中
  3. 有序集合很多,但是能滿足快速刪除某個(gè)key的只有set,但是set插入數(shù)據(jù)是無(wú)序的。「幸虧JAVA有LinkedHashSet這個(gè)類,鏈表和集合的結(jié)合體,鏈表不能快速刪除元素,但是能保證插入順序。集合內(nèi)部元素?zé)o序,但是能快速刪除元素,完美」

下面就是具體的實(shí)現(xiàn)。

public class LfuCache<K, V> {

    private HashMap<K, V> keyToVal;
    private HashMap<K, Integer> keyToFreq;
    private HashMap<Integer, LinkedHashSet<K>> freqTokeys;

    private int minFreq;
    private int capacity;

    public LfuCache(int capacity) {
        keyToVal = new HashMap<>();
        keyToFreq = new HashMap<>();
        freqTokeys = new HashMap<>();
        this.capacity = capacity;
        this.minFreq = 0;
    }

    public V get(K key) {
        V v = keyToVal.get(key);
        if (v == null) {
            return null;
        }
        increaseFrey(key);
        return v;
    }

    public void put(K key, V value) {
        // get方法里面會(huì)增加頻次
        if (get(key) != null) {
            // 重新設(shè)置值
            keyToVal.put(key, value);
            return;
        }

        // 超出容量,刪除頻率最低的key
        if (keyToVal.size() >= capacity) {
            removeMinFreqKey();
        }

        keyToVal.put(key, value);
        keyToFreq.put(key, 1);
        // key對(duì)應(yīng)的value存在,返回存在的key
        // key對(duì)應(yīng)的value不存在,添加key和value
        freqTokeys.putIfAbsent(1, new LinkedHashSet<>());
        freqTokeys.get(1).add(key);
        this.minFreq = 1;
    }

    // 刪除出現(xiàn)頻率最低的key
    private void removeMinFreqKey() {
        LinkedHashSet<K> keyList = freqTokeys.get(minFreq);
        K deleteKey = keyList.iterator().next();
        keyList.remove(deleteKey);
        if (keyList.isEmpty()) {
            // 這里刪除元素后不需要重新設(shè)置minFreq
            // 因?yàn)閜ut方法執(zhí)行完會(huì)將minFreq設(shè)置為1
            freqTokeys.remove(keyList);
        }
        keyToVal.remove(deleteKey);
        keyToFreq.remove(deleteKey);
    }

    // 增加頻率
    private void increaseFrey(K key) {
        int freq = keyToFreq.get(key);
        keyToFreq.put(key, freq + 1);
        freqTokeys.get(freq).remove(key);
        freqTokeys.putIfAbsent(freq + 1, new LinkedHashSet<>());
        freqTokeys.get(freq + 1).add(key);
        if (freqTokeys.get(freq).isEmpty()) {
            freqTokeys.remove(freq);
            // 最小頻率的set為空,key被移動(dòng)到minFreq+1對(duì)應(yīng)的set了
            // 所以minFreq也要加1
            if (freq == this.minFreq) {
                this.minFreq++;
            }
        }
    }
}

測(cè)試一下

LfuCache<String, String> lfuCache = new LfuCache(2);
lfuCache.put("1", "1");
lfuCache.put("2", "2");
// 1
System.out.println(lfuCache.get("1"));
lfuCache.put("3", "3");
// 1的頻率為2,2和3的頻率為1,但2更早之前被訪問(wèn),所以被清除
// 結(jié)果為null
System.out.println(lfuCache.get("2"));

分享到:
標(biāo)簽:Redis
用戶無(wú)頭像

網(wǎng)友整理

注冊(cè)時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊(cè)賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過(guò)答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫(kù),初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績(jī)?cè)u(píng)定2018-06-03

通用課目體育訓(xùn)練成績(jī)?cè)u(píng)定