本文所指TwoQueues緩衝模型,是說資料在記憶體中的緩衝模型。
無論何種語言,都可能需要把一部分資料放在記憶體中,避免重複運算、讀取。最常見的情境就是JQuery選取器,有些Dom元素的選取是非常耗時的,我們希望能把這些資料緩衝起來,不必每次調用都去重新遍曆Dom樹。
存就存吧,但總得有個量吧!總不能把所有的曆史資料都放在記憶體中,畢竟目前記憶體的容量還是相當可憐的,就算記憶體夠大,理論上每個線程分配的記憶體也是有限制的。
那麼問題來了,如何才能高效的把真正有用的資料緩衝起來呢?這就涉及到淘汰演算法,需要把垃圾資料淘汰掉,才能保住有用的資料。
比較常用的思路有以下幾種:
FIFO:就是一個先進先出的隊列,最先緩衝的資料,最早被淘汰,著名的JQuery架構內部就是用的這種模型。
LRU:雙鏈表結構,每次有新資料存入,直接放在鏈表頭;每次被訪問的資料,也轉移到鏈表頭,這樣一來,鏈表尾部的資料即是最近沒被使用過的,淘汰之。
TwoQueues:FIFO+ LRU,FIFO主要存放初次存入的資料,LRU中存放至少使用過兩次的熱點資料,此演算法命中率高,適應性強,複雜度低。
其他淘汰演算法還有很多很多,但實際用的比較多的也就這兩種。因為他們本身演算法不複雜,容易實現,執行效率高,緩衝的命中率在大多數場合也還可以接受。畢竟緩衝演算法也是需要消耗CPU的,如果太過複雜,雖然命中率有所提高,但得不償失。試想一下,如果從緩衝中取資料,比從原始位置取還消耗時間,要緩衝何用?
具體理論就不多說了,網上有的是,我也不怎麼懂,今天給大家分享的是JavaScript版的TwoQueues緩衝模型。
還是先說說使用方法,很簡單。
基本使用方法如下:
var tq = initTwoQueues(10);
tq.set("key", "value");
tq.get("key");
初始化的時候,指定一下緩衝容量即可。需要注意的是,由於內部採用FIFO+LRU實現,所以實際容量是指定容量的兩倍,上例指定的是10個(索引值對),實際上可以存放20個。
容量大小需要根據實際應用情境而定,太小命中率低,太大效率低,物極必反,需要自己衡量。
在開發過程中,為了審查緩衝效果如何,可以將緩衝池初始化成開發版:
var tq = initTwoQueues(10, true);
tq.hitRatio();
就是在後邊加一個參數,直接true就可以了。這樣初始化的緩衝池,會自動統計命中率,可以通過hitRatio方法擷取命中率。如果不加這個參數,hitRatio方法擷取的命中率永遠為0。
統計命中率肯定要消耗資源,所以生產環境下不建議開啟。
是時候分享代碼了:
(function(exports){ /** * 繼承用的純淨類 * @constructor */ function Fn(){} Fn.prototype = Elimination.prototype; /** * 基於鏈表的緩衝淘汰演算法父類 * @param maxLength 緩衝容量 * @constructor */ function Elimination(maxLength){ this.container = {}; this.length = 0; this.maxLength = maxLength 30; this.linkHead = this.buildNode("", ""); this.linkHead.head = true; this.linkTail = this.buildNode("", ""); this.linkTail.tail = true; this.linkHead.next = this.linkTail; this.linkTail.prev = this.linkHead; } Elimination.prototype.get = function(key){ throw new Error("This method must be override!"); }; Elimination.prototype.set = function(key, value){ throw new Error("This method must be override!"); }; /** * 建立鏈表中的節點 * @param data 節點包含的資料,即快取資料值 * @param key 節點的唯一標示符,即緩衝的鍵 * @returns {{}} */ Elimination.prototype.buildNode = function(data, key){ var node = {}; node.data = data; node.key = key; node.use = 0; return node; }; /** * 從鏈表頭彈出一個節點 * @returns {*} */ Elimination.prototype.shift = function(){ var node = null; if(!this.linkHead.next.tail){ node = this.linkHead.next; this.linkHead.next = node.next; node.next.prev = this.linkHead; delete this.container[node.key]; this.length--; } return node; }; /** * 從鏈表頭插入一個節點 * @param node 節點對象 * @returns {*} */ Elimination.prototype.unshift = function(node){ node.next = this.linkHead.next; this.linkHead.next.prev = node; this.linkHead.next = node; node.prev = this.linkHead; this.container[node.key] = node; this.length++; return node; }; /** * 從鏈表尾插入一個節點 * @param node 節點對象 * @returns {*} */ Elimination.prototype.append = function(node){ this.linkTail.prev.next = node; node.prev = this.linkTail.prev; node.next = this.linkTail; this.linkTail.prev = node; this.container[node.key] = node; this.length++; return node; }; /** * 從鏈表尾彈出一個節點 * @returns {*} */ Elimination.prototype.pop = function(){ var node = null; if(!this.linkTail.prev.head){ node = this.linkTail.prev; node.prev.next = this.linkTail; this.linkTail.prev = node.prev; delete this.container[node.key]; this.length--; } return node; }; /** * 從鏈表中移除指定節點 * @param node 節點對象 * @returns {*} */ Elimination.prototype.remove = function(node){ node.prev.next = node.next; node.next.prev = node.prev; delete this.container[node.key]; this.length--; return node; }; /** * 節點被訪問需要做的處理,具體是把該節點移動到鏈表頭 * @param node */ Elimination.prototype.use = function(node){ this.remove(node); this.unshift(node); }; /** * LRU緩衝淘汰演算法實現 * @constructor */ function LRU(){ Elimination.apply(this, arguments); } LRU.prototype = new Fn(); LRU.prototype.get = function(key){ var node = undefined; node = this.container[key]; if(node){ this.use(node); } return node; }; LRU.prototype.set = function(key, value){ var node = this.buildNode(value, key); if(this.length === this.maxLength){ this.pop(); } this.unshift(node); }; /** * FIFO緩衝淘汰演算法實現 * @constructor */ function FIFO(){ Elimination.apply(this, arguments); } FIFO.prototype = new Fn(); FIFO.prototype.get = function(key){ var node = undefined; node = this.container[key]; return node; }; FIFO.prototype.set = function(key, value){ var node = this.buildNode(value, key); if(this.length === this.maxLength){ this.shift(); } this.append(node); }; /** * LRU、FIFO演算法封裝,成為新的twoqueues緩衝淘汰演算法 * @param maxLength * @constructor */ function Agent(maxLength){ this.getCount = 0; this.hitCount = 0; this.lir = new FIFO(maxLength); this.hir = new LRU(maxLength); } Agent.prototype.get = function(key){ var node = undefined; node = this.lir.get(key); if(node){ node.use++; if(node.use >= 2){ this.lir.remove(node); this.hir.set(node.key, node.data); } }else{ node = this.hir.get(key); } return node; }; Agent.prototype.getx = function(key){ var node = undefined; this.getCount++; node = this.get(key); if(node){ this.hitCount++; } return node; }; Agent.prototype.set = function(key, value){ var node = null; node = this.lir.container[key] this.hir.container[key]; if(node){ node.data = value; }else{ this.lir.set(key, value); } }; /** * 擷取命中率 * @returns {*} */ Agent.prototype.hitRatio = function(){ var ret = this.getCount; if(ret){ ret = this.hitCount / this.getCount; } return ret; }; /** * 對外介面 * @param maxLength 緩衝容量 * @param dev 是否為開發環境,開發環境會統計命中率,反之不會 * @returns {{get, set: Function, hitRatio: Function}} */ exports.initTwoQueues = function(maxLength, dev){ var api = new Agent(maxLength); return { get: (function(){ if(dev){ return function(key){ var ret = api.getx(key); return ret && ret.data; }; }else{ return function(key){ var ret = api.get(key); return ret && ret.data; }; } }()), set: function(){ api.set.apply(api, arguments); }, hitRatio: function(){ return api.hitRatio.apply(api, arguments); } }; };}(this));
最後,再次提醒,緩衝演算法需要和實際應用情境相結合,沒有萬能演算法,合適的才是最好的!