簡介
我們都知道,Redis會使用“淘汰策略”來進行熱點數據的管理,其中大部分場景下都會使用LRU(Least Recently used)算法,本文從一個簡單的使用dict緩存斐波那契數列的值為例引出LRU的使用場景並使用Python實現一個簡單的LRUCache。
使用緩存減少計算或者主數據庫的開銷
在實際的業務場景中,我們常常會使用緩存來減少程序的計算或者用戶頻繁訪問主數據庫的開銷。比如說,我這里有一個接口函數fib,用戶使用某一個天數來請求數據時,接口將計算結果返回給用戶:
def fib(n): if n <= 2: return 1 return fib(n-1) + fib(n-2)
為了減少程序計算的開銷,我們可以使用一個字典去“緩存”對應的結果,也就是說,我們可以提前計算好用戶請求的天數與對應結果的對應信息,如果用戶請求的天數在“緩存”中的話我們可以直接將緩存中的結果返回給用戶,這樣就有效的減少了一次程序的開銷!
裝飾器方式實現“緩存效果”
# 字典中有的會從這里取值而不用計算,減少計算開銷 dic = {1:1,2:1,3:2} def wrapper(func): global dic def inner(n): if n in dic: return dic[n] res = func(n) dic[n] = res return res return inner @wrapper def fib(n): if n <= 2: return 1 return fib(n-1) + fib(n-2) if __name__ == "__main__": for i in range(1,6): ret = fib(i) print(ret)
上面的代碼有效的實現了“緩存”效果。
但是問題又來了:事實上緩存的大小由機器的內存大小決定的,因此緩存中的數據不可能無限大!如果任憑緩存中的數據無限的增加總有一個時刻會撐爆內存,影響服務的性能!
這就需要有一種緩存數據的管理策略,而LRU是管理緩存熱點數據常用的算法。
簡單的LRU實現
LRU(Least Recently used)策略是將最早訪問的數據從緩存中刪除(當然是在緩存超過閾值的情況)。
實現的思路
拿上面的緩存字典dic來講解:
如果用戶請求的數據在緩存中的話,我們直接在緩存中找到該數據對應的value值返回給用戶,並且將剛剛訪問的這個鍵值對放到后面;
如果請求的數據不在緩存中的話需要調用fib方法計算結果,並且還得判斷一下緩存是否滿:如果緩存沒有滿的話將結果返回並更新緩存空間——將剛剛訪問的鍵值對放在后面,如果緩存滿了的話需要將“最早”訪問的那個鍵值對從dic中刪掉,然后將新計算的結果放到dic的后面。
使用雙端鏈表實現簡單的LRUCache
# -*- coding:utf-8 -*- class Node(object): def __init__(self,prev=None,next=None,key=None,value=None): self.prev,self.next,self.key,self.value = prev,next,key,value # 雙端鏈表 class CircularDoubeLinkedList(object): def __init__(self): node = Node() node.prev,node.next = node,node self.rootnode = node # 頭節點 def headnode(self): return self.rootnode.next # 尾節點 def tailnode(self): return self.rootnode.prev # 移除一個節點 def remove(self,node): if node is self.rootnode: return node.prev.next = node.next node.next.prev = node.prev # 追加一個節點到鏈表的尾部 def append(self,node): tailnode = self.tailnode() tailnode.next = node node.next = self.rootnode # 需要把根節點的prev設置為node self.rootnode.prev = node # LRU Cache class LRUCache(object): def __init__(self,maxsize=10): self.maxsize = maxsize self.cache = {} self.access = CircularDoubeLinkedList() self.isfull = len(self.cache) >= self.maxsize ### 類裝飾器的寫法 def __call__(self,func): def inner(n): cachenode = self.cache.get(n,None) # 如果緩存中有,從緩存中獲取並移動n對應的節點 if cachenode: self.access.remove(cachenode) self.access.append(cachenode) # 返回value值 return cachenode.value # 如果緩存中沒有,需要先看一下緩存是否滿了,再去處理 else: result = func(n) # 如果緩存沒滿往里面添加數據 if not self.isfull: tailnode = self.access.tailnode() new_node = Node(tailnode,self.access.rootnode,n,result) self.access.append(new_node) # 將新節點緩存下來 self.cache[n] = new_node self.isfull = len(self.cache) >= self.maxsize # return result # 如果緩存滿了,刪除lru_node else: lru_node = self.access.headnode() # 先把lru_node刪除 del self.cache[lru_node.key] self.access.remove(lru_node) # 講新節點放進去 tailnode = self.access.tailnode() new_node = Node(tailnode,self.access.rootnode,n,result) self.access.append(new_node) # 將新節點緩存下來 self.cache[n] = new_node return result # 裝飾器,最后返回inner return inner @LRUCache() def fib(n): if n <= 2: return 1 return fib(n-1) + fib(n-2) for i in range(1,10): print(fib(i))
~~~