LFU的基本原理與實現

来源:https://www.cnblogs.com/wyq178/archive/2019/11/04/11790015.html
-Advertisement-
Play Games

前言:之前有寫過一篇關於LRU的文章鏈接https://www.cnblogs.com/wyq178/p/9976815.html LRU全稱:Least Recently Used:最近最少使用策略,判斷最近被使用的時間,距離目前最遠的數據優先被淘汰,作為一種根據訪問時間來更改鏈表順序從而實現緩存 ...


前言:之前有寫過一篇關於LRU的文章鏈接https://www.cnblogs.com/wyq178/p/9976815.html  LRU全稱:Least Recently Used:最近最少使用策略,判斷最近被使用的時間,距離目前最遠的數據優先被淘汰,作為一種根據訪問時間來更改鏈表順序從而實現緩存淘汰的演算法,它是redis採用的淘汰演算法之一。redis還有一個緩存策略叫做LFU, 那麼LFU是什麼呢?我們本期博客來分下一下LFU:

本篇博客的目錄:

一:  LRU是什麼

二:LRU的實現

三:測試

四:LRU和LFU的區別以及缺陷

五:總結

 

正文

一:LRU是什麼

     LFU,全稱是:Least Frequently Used,最不經常使用策略,在一段時間內,數據被使用頻次最少的,優先被淘汰。最少使用LFU)是一種用於管理電腦記憶體的緩存演算法。主要是記錄和追蹤記憶體塊的使用次數,當緩存已滿並且需要更多空間時,系統將以最低記憶體塊使用頻率清除記憶體.採用LFU演算法的最簡單方法是為每個載入到緩存的塊分配一個計數器。每次引用該塊時,計數器將增加一。當緩存達到容量並有一個新的記憶體塊等待插入時,系統將搜索計數器最低的塊並將其從緩存中刪除(本段摘自維基百科)

 解釋:上面這個圖就是一個LRU的簡單實現思路,在鏈表的開始插入元素,然後每插入一次計數一次,接著按照次數重新排序鏈表,如果次數相同的話,按照插入時間排序,然後從鏈表尾部選擇淘汰的數據~

二:LRU實現

2.1 定義Node節點

Node主要包含了key和value,因為LFU的主要實現思想是比較訪問的次數,如果在次數相同的情況下需要比較節點的時間,越早放入的越快      被淘汰,因此我們需要在Node節點上加入time和count的屬性,分別用來記錄節點的訪問的時間和訪問次數。其他的版本實現方式里有新加個內部類來記錄 key的count和time,但是我覺得不夠方便,還得單獨維護一個map,成本有點大。還有一點註意的是這裡實現了comparable介面,覆寫了compare方法,這裡 的主要作用就是在排序的時候需要用到,在compare方法裡面我們首先比較節點的訪問次數,在訪問次數相同的情況下比較節點的訪問時間,這裡是為了 在排序方法裡面通過比較key來選擇淘汰的key

 /**
     * 節點
     */
    public static class Node implements Comparable<Node>{
            //
            Object key;
            //
            Object value;
            /**
             * 訪問時間
             */
            long time;

            /**
             * 訪問次數
             */
            int count;

            public Node(Object key, Object value, long time, int count) {
                this.key = key;
                this.value = value;
                this.time = time;
                this.count = count;
            }

            public Object getKey() {
                return key;
            }

            public void setKey(Object key) {
                this.key = key;
            }

            public Object getValue() {
                return value;
            }

            public void setValue(Object value) {
                this.value = value;
            }

            public long getTime() {
                return time;
            }

            public void setTime(long time) {
                this.time = time;
            }

            public int getCount() {
                return count;
            }

            public void setCount(int count) {
                this.count = count;
            }

            @Override
            public int compareTo(Node o) {
                int compare = Integer.compare(this.count, o.count);
                //在數目相同的情況下 比較時間
                if (compare==0){
                    return Long.compare(this.time,o.time);
                }
                return compare;
            }
        }

 2.2:定義LFU類

 定義LFU類,這裡採用了泛型,聲明瞭K和V,還有總容量和一個Map(caches)用來維護所有的節點。在構造方法里將size傳遞進去,並且創建了一個LinkedHashMap,採用linkedHashMap的主要原因是維護key的順序

public class LFU<K,V> {

        /**
         *  總容量
         */
        private int capacity;

        /**
         * 所有的node節點
         */
        private Map<K, Node> caches;
        /**
         * 構造方法
         * @param size
        */
        public LFU(int size) {
           this.capacity = size;
           caches = new LinkedHashMap<K,Node>(size);
        }
}

2.3: 添加元素

添加元素的邏輯主要是先從緩存中根據key獲取節點,如果獲取不到,證明是新添加的元素,然後和容量比較,大於預定容量的話,需要找出count計數最小(計數相同的情況下,選擇時間最久)的節點,然後移除掉那個。如果在預定的大小之內,就新創建節點,註意這裡不能使用 System.currentTimeMillis()方法,因為毫秒級別的粒度無法對插入的時間進行區分,在運行比較快的情況下,只有System.nanoTime()才可以將key的插入時間區分,預設設置count計數為1.如果能獲取到,表示是舊的元素,那麼就用新值覆蓋舊值,計數+1,設置key的time為當前納秒時間。最後還需要進行排序,這裡可以看出插入元素的邏輯主要是添加進入緩存,更新元素的時間和計數~

       /**
         * 添加元素
         * @param key
         * @param value
         */
        public void put(K key, V value) {
        Node node = caches.get(key);
        //如果新元素
        if (node == null) {
            //如果超過元素容納量
            if (caches.size() >= capacity) {
                //移除count計數最小的那個key
                K leastKey = removeLeastCount();
                caches.remove(leastKey);
            }
            //創建新節點
            node = new Node(key,value,System.nanoTime(),1);
            caches.put(key, node);
        }else {
            //已經存在的元素覆蓋舊值
            node.value = value;
            node.setCount(node.getCount()+1);
            node.setTime(System.nanoTime());
        }
        sort();
    }

 每次put或者get元素都需要進行排序,排序的主要意義在於按照key的cout和time進行一個key順序的重組,這裡的邏輯是首先將緩存map創建成一個list,然後按照Node的value進行,重組整個map。然後將原來的緩存清空,遍歷這個map, 把key和value的值放進去原來的緩存中的順序就進行了重組~這裡區分於LRU的不同點在於使用了java的集合API,LRU的排序是進行節點移動。而在LFU中實現比較複雜,因為put的時候不光得比較基數還有時間。如果不藉助java的API的話,可以新維護一個節點頻率鏈表,每次將key保存在這個節點頻率鏈表中移動指針,從而也間接可以實現排序~

   /**
     * 排序
     */
    private void sort() {

        List<Map.Entry<K, Node>> list = new ArrayList<>(caches.entrySet());
        Collections.sort(list, new Comparator<Map.Entry<K, Node>>() {
            @Override
            public int compare(Map.Entry<K, Node> o1, Map.Entry<K, Node> o2) {
                return o2.getValue().compareTo(o1.getValue());
            }
        });

        caches.clear();
        for (Map.Entry<K, Node> kNodeEntry : list) {
            caches.put(kNodeEntry.getKey(),kNodeEntry.getValue());
        }
    }

 移除最小的元素

淘汰最小的元素這裡調用了Collections.min方法,然後通過比較key的compare方法,找到計數最小和時間最長的元素,直接從緩存中移除~

       /**
         * 移除統計數或者時間比較最小的那個
         * @return
         */
        private K removeLeastCount() {
            Collection<Node> values = caches.values();
            Node min = Collections.min(values);
            return (K)min.getKey();

         }

2.4:獲取元素

 獲取元素首先是從緩存map中獲取,否則返回null,在獲取到元素之後需要進行節點的更新,計數+1和刷新節點的時間,根據LFU的原則,在當前時間獲取到這個節點以後,這個節點就暫時變成了熱點節點,但是它的cout計數也有可能是小於某個節點的count的,所以

此時不能將它直接移動到鏈表頂,還需要進行一次排序,重組它的位置~

/**
         * 獲取元素
         * @param key
         * @return
         */
        public V get(K key){
        Node node = caches.get(key);
        if (node!=null){
            node.setCount(node.getCount()+1);
            node.setTime(System.nanoTime());
            sort();
            return (V)node.value;
        }
        return null;
    }

三:測試

 首先聲明一個LRU,然後預設的最大的大小為5,依次put進入A、B、C、D、E、F6個元素,此時將會找到計數最小和時間最短的元素,那麼將會淘汰A(因為count值都是1)。記著get兩次B元素,那麼B元素的count=3,時間更新為最新。此時B將會移動到頂,接著在getC元素,C元素的count=2,時間會最新,那麼此時因為它的count值依然小於B,所以它依然在B後面,再getF元素,F元素的count=2,又因為它的時間會最新,所以在與C相同的計數下,F元素更新(時間距離現在最近),所以鏈表將會移動,F會在C的前面,再次put一次C,此時C的count=3,同時時間為最新,那麼此刻C的count和B保持一致,則他們比較時間,C明顯更新,所以C將會排在B的前面,最終的順序應該是:C->B->F->E->D

public static  void main(String[] args) {

        LFU<Integer, String> lruCache = new LFU<>(5);
        lruCache.put(1, "A");
        lruCache.put(2, "B");
        lruCache.put(3, "C");
        lruCache.put(4, "D");
        lruCache.put(5, "E");
        lruCache.put(6, "F");
        lruCache.get(2);
        lruCache.get(2);
        lruCache.get(3);
        lruCache.get(6);
//重新put節點3
lruCache.put(3,"C");
        final Map<Integer, Node> caches = (Map<Integer, Node>) lruCache.caches; for (Map.Entry<Integer, Node> nodeEntry : caches.entrySet()) { System.out.println(nodeEntry.getValue().value); } }

    運行結果:

 

 四:LRU和LFU的區別以及LFU的缺點

    LRU和LFU側重點不同,LRU主要體現在對元素的使用時間上,而LFU主要體現在對元素的使用頻次上。LFU的缺陷是:在短期的時間內,對某些緩存的訪問頻次很高,這些緩存會立刻晉升為熱點數據,而保證不會淘汰,這樣會駐留在系統記憶體裡面。而實際上,這部分數據只是短暫的高頻率訪問,之後將會長期不訪問,瞬時的高頻訪問將會造成這部分數據的引用頻率加快,而一些新加入的緩存很容易被快速刪除,因為它們的引用頻率很低。

 五:總結

    本篇博客針對LFU做了一個簡單的介紹,並詳細介紹瞭如何用java來實現LFU,並且和LRU做了一個簡單的比較。針對一種緩存策略。LFU有自己的獨特使用場景,如何實現LFU和利用LFU的特性來實現開發過程部分功能是我們需要思考的。實際在使用中LRU的使用頻率要高於LFU,不過瞭解這種演算法也算是程式員的必備技能。

 

最後: 如果對學習java有興趣可以加入群:618626589,本群旨在打造無培訓廣告、無閑聊扯皮、無註水斗圖的純技術交流群,群里每天會分享有價值的問題和學習資料,歡迎各位隨時加入~     


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • <template> <div :class="className"> <div :id="id" class="spiritChartBox"></div> </div> </template> <script> import { mapState } from "vuex"; import ec... ...
  • JS使用方法 模態窗提供了四個事件: 1.show.bs.modal在顯示之前觸發 2.shown.bs.modal在顯示之後觸發 3.hide.bs.modal在隱藏之前觸發 4.hidden.bs.modal在隱藏之後觸發 ...
  • jQuery的DOM遍歷模塊對DOM模型的原生屬性parentNode、childNodes、firstChild、lastChild、previousSibling、nextSibling進行了封裝和擴展,用於在DOM樹中遍歷父元素、子元素和兄弟元素。 可以通過jQuery的實例來訪問,方法如下: ...
  • "洛谷題目頁面傳送門" & "CodeForces題目頁面傳送門" 定義一個$1\sim n$的排列$a$的平方$a^2=b$,當且僅當$\forall i\in[1,n],b_i=a_{a_i}$,即$a^2$為將$a$在$[1,2,\cdots,n]$上映射$2$次所得的排列。現在給定一個$1\ ...
  • 2019-11-04-23:03:13 目錄: 1.常用的數據結構 2.棧 3.隊列 4.數組 5.鏈表 6.紅黑樹 常用的數據結構: 包含:棧、隊列、數組、鏈表和紅黑樹 棧: 棧:stack,又稱堆棧,它是運算受限的線性表,其限制是僅允許在標的一端進行插入和刪除操作,不允許在其 他任何位置進行添加 ...
  • 進程:通俗理解一個運行的程式或者軟體,進程是操作系統資源分配的基本單位 1.1、導入進程模塊 import multiprocessing 1.2、Process進程類的語法結構如下: Process([group[, target[, name[,args[,kwargs]]]]]) group: ...
  • 本文經授權轉自公眾號:石杉的架構筆記 一、問題起源 二、Eureka Server設計精妙的註冊表存儲結構 三、Eureka Server端優秀的多級緩存機制 四、總結 一、問題起源 Spring Cloud架構體系中,Eureka是一個至關重要的組件,它扮演著微服務註冊中心的角色,所有的服務註冊與 ...
  • pom.xml 華為雲鏡像: -基本web開發 2.安裝Lombok插件:plugins >lombok 3.實體類中 4.Controller: 請求參數兩種類型: @RequestParam 獲取查詢參數。即url?name=value 這種形式 @PathVariable 獲取路徑參數。即ur ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...