越來越覺得的緩存是計(jì)算機(jī)科學(xué)里最NB的發(fā)明(沒有之一), 現(xiàn)在項(xiàng)目用的是redis做的緩存, 它的兩個(gè)特性用的蠻順手的:
- 鍵值查找功能
- 緩存可設(shè)置過期時(shí)間
突突然的凯肋,覺得用python也可以簡單的模擬一下,做一個(gè)本地的輕量級(jí)緩存.(不過, 注意一點(diǎn):redis的緩存可以用于分布式, python模擬的則不行, 但是如果把本地緩存的過期時(shí)間設(shè)的短一點(diǎn),比如10s, 在大并發(fā)下還是有不錯(cuò)表現(xiàn)的)
對(duì)于鍵值查找功能, python原生的字典dict完美勝任. 對(duì)于緩存自動(dòng)過期, 簡單的想法后臺(tái)起個(gè)服務(wù), 定期檢測, 但是這樣代碼會(huì)相對(duì)復(fù)雜莉撇,而且還要搶占寶貴的cpu資源诈皿,失去了輕量級(jí)的初衷.
那么思路是這樣的:
- 只要將每個(gè)對(duì)應(yīng)的緩存鍵值蚤霞,加一個(gè)expire字段,代表過期時(shí)間點(diǎn)昔馋,
- 第一次獲取, expire=當(dāng)前時(shí)間點(diǎn)+過期時(shí)間
- 非第一次獲取時(shí), 通過expire判斷是否已過期, 如果過期則可認(rèn)為數(shù)據(jù)沒找到五芝,反之返回正常的緩存
簡單的代碼如下:
def get(self, key):
value = self._cache.get(key, self.notFound)
if(value is not self.notFound):
expire = value[r'expire']
if( self.nowTime() > expire):
return self.notFound
else:
return value
else:
return self.notFound
- 通過第1步, 基本功能已實(shí)現(xiàn)刊咳,現(xiàn)在收拾"爛攤子". 步驟1明顯的確點(diǎn)就是內(nèi)存泄漏了(搞python的應(yīng)該不怎么聽的到"內(nèi)存泄漏"),原因很明顯彪见,通過字典模擬redis緩存, 所有數(shù)據(jù)都保存在緩存字典中(即使它過期了), 因?yàn)闆]人去刪除它們.
解決的方法是娱挨,采用弱引用
python 文檔有有句:
A primary use for weak references is to implement caches or mappings holding large objects, where it’s desired that a large object not be kept alive solely because it appears in a cache or mapping.
就是說余指,弱引用主要的用途也是為了實(shí)現(xiàn)緩存.
對(duì)于我們的應(yīng)用,WeakValueDictionary這貨就很符合需求跷坝,那么代碼就如下這樣了:
class LocalCache():
notFound = object()
class Dict(dict):
def __del__(self):
pass
def __init__(self, maxlen=2):
self.weak = weakref.WeakValueDictionary()
@staticmethod
def nowTime():
return int(time.time())
def get(self, key):
value = self.weak.get(key, self.notFound)
if(value is not self.notFound):
expire = value[r'expire']
if( self.nowTime() > expire):
return self.notFound
else:
return value
else:
return self.notFound
def set(self, key, value):
self.weak[key] = LocalCache.Dict(value)
幾點(diǎn)說明下:
- 創(chuàng)建內(nèi)部類Dict的原因
python文檔的說法
Several built-in types such as list and dict do not directly support weak references but can add support through subclassing:
classDict(dict):
pass
obj=Dict(red=1,green=2,blue=3)
就是說內(nèi)建的list和dict不能直接支持弱引用酵镜,但是繼承他們的子類就支持弱引用了. so..
- weakref.WeakValueDictionary說明
現(xiàn)在就是用WeakValueDictionary來代替之前的dict了,弱引用的優(yōu)勢就是:
WeakValueDictionary隨時(shí)都可能被回收(聽上去很不靠譜,不過下面有解決方法), 所以不用擔(dān)心之前的內(nèi)存泄漏問題.
可能有人擔(dān)心,WeakValueDictionary只是value值是弱引用柴钻,也就說value可以被回收淮韭,但是key值回一直存在,導(dǎo)致泄漏,don't wrong, 有文檔為證:
Entries in the dictionary will be discarded when no strong reference to the value exists any more.
即如果value值沒有強(qiáng)引用了贴届,那么對(duì)應(yīng)的記錄就會(huì)被丟棄(這句話有彩蛋). 所以也不用擔(dān)心key導(dǎo)致的泄漏了.
當(dāng)初寫完這個(gè)后靠粪,感覺一切都比較的perfect. 但是還是too young too simple.
- 最后較成熟的方案
重新關(guān)注下第二步, 有彩蛋的那句話, 這句話再深入理解下就是,如果self.weak[key] = LocalCache.Dict(value)
這樣毫蚓,LocalCache.Dict(value)
沒有其他強(qiáng)引用, 那么對(duì)不起占键,下一瞬間這個(gè)記錄就沒了(WTF).
所以之前的寫法會(huì)導(dǎo)致--沒有任何緩存作用(如果你耐著性子看到這,估計(jì)要罵娘了元潘。畔乙。。)柬批,不過既然都寫了這么多啸澡,方案還是有的(我在 http://stackoverflow.com 找到類似的方案,稍微改進(jìn)了下)氮帐,既然要強(qiáng)引用嗅虏,那就給他強(qiáng)引用了,代碼如下:
import weakref, collections
import time
class LocalCache():
notFound = object()
class Dict(dict):
def __del__(self):
pass
def __init__(self, maxlen=10):
self.weak = weakref.WeakValueDictionary()
self.strong = collections.deque(maxlen=maxlen)
@staticmethod
def nowTime():
return int(time.time())
def get(self, key):
value = self.weak.get(key, self.notFound)
if(value is not self.notFound):
expire = value[r'expire']
if( self.nowTime() > expire):
return self.notFound
else:
return value
else:
return self.notFound
def set(self, key, value):
self.weak[key] = strongRef = LocalCache.Dict(value)
self.strong.append(strongRef)
代碼跟之前的差不多上沐,就是多了self.strong這個(gè)隊(duì)列來保存強(qiáng)引用, 并利用collections.deque的一個(gè)特性:
the deque is bounded to the specified maximum length. Once a bounded length deque is full, when new items are added, a corresponding number of items are discarded from the opposite end.
意思是如果給deque設(shè)置了大小(通過maxlen皮服,不傳或設(shè)為None則沒有限制), 那么deque滿的時(shí)候,新添加的對(duì)象會(huì)將之前的'老家伙擠出去'.
整個(gè)過程是這樣的,剛開始往緩存加數(shù)據(jù)時(shí), 添加的每一個(gè)值都有一個(gè)弱引用和強(qiáng)引用龄广,不停的加硫眯,直到deque的隊(duì)列滿了(地主家也沒余糧啊), 這時(shí)后面每加一個(gè)择同,都將導(dǎo)致deque中最早加入的強(qiáng)引用被deque廢棄两入,而被廢棄的強(qiáng)引用對(duì)應(yīng)的值只有弱引用了,于是與之相關(guān)的WeakValueDictionary記錄也被回收了(不知道有沒人閃過虛擬內(nèi)存中內(nèi)存不夠用時(shí)敲才,數(shù)據(jù)在硬盤和內(nèi)存搗鼓的畫面)
到這基本已經(jīng)寫完裹纳。當(dāng)然這個(gè)LocalCache類還有很多可完善的地方,這里只是講解下它的形成過程紧武。
- 附加一個(gè)應(yīng)用這個(gè)LocalCache類的函數(shù)調(diào)用緩存裝飾器
from functools import wraps
def funcCache(expire=0):
caches = LocalCache()
def _wrappend(func):
@wraps(func)
def __wrapped(*args , **kwargs):
#計(jì)算出緩存的key值
key = str(func) + str(args) + str(kwargs)
result = caches.get(key)
if(result is LocalCache.notFound):
result = func(*args, **kwargs)
caches.set(key, {r'result':result, r'expire':expire + caches.nowTime() })
result = caches.get(key)
return result
return __wrapped
return _wrappend