越来越觉得的缓存是计算机科学里最NB的发明(没有之一), 现在项目用的是redis做的缓存, 它的两个特性用的蛮顺手的:
- 键值查找功能
- 缓存可设置过期时间
突突然的,觉得用python也可以简单的模拟一下,做一个本地的轻量级缓存.(不过, 注意一点:redis的缓存可以用于分布式, python模拟的则不行, 但是如果把本地缓存的过期时间设的短一点,比如10s, 在大并发下还是有不错表现的)
对于键值查找功能, python原生的字典dict完美胜任. 对于缓存自动过期, 简单的想法后台起个服务, 定期检测, 但是这样代码会相对复杂,而且还要抢占宝贵的cpu资源,失去了轻量级的初衷.
那么思路是这样的:
- 只要将每个对应的缓存键值,加一个expire字段,代表过期时间点,
- 第一次获取, expire=当前时间点+过期时间
- 非第一次获取时, 通过expire判断是否已过期, 如果过期则可认为数据没找到,反之返回正常的缓存
简单的代码如下:
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步, 基本功能已实现,现在收拾"烂摊子". 步骤1明显的确点就是内存泄漏了(搞python的应该不怎么听的到"内存泄漏"),原因很明显,通过字典模拟redis缓存, 所有数据都保存在缓存字典中(即使它过期了), 因为没人去删除它们.
解决的方法是,采用弱引用
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.
就是说,弱引用主要的用途也是为了实现缓存.
对于我们的应用,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)
几点说明下:
- 创建内部类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)
就是说内建的list和dict不能直接支持弱引用,但是继承他们的子类就支持弱引用了. so..
- weakref.WeakValueDictionary说明
现在就是用WeakValueDictionary来代替之前的dict了,弱引用的优势就是:
WeakValueDictionary随时都可能被回收(听上去很不靠谱,不过下面有解决方法), 所以不用担心之前的内存泄漏问题.
可能有人担心,WeakValueDictionary只是value值是弱引用,也就说value可以被回收,但是key值回一直存在,导致泄漏,don't wrong, 有文档为证:
Entries in the dictionary will be discarded when no strong reference to the value exists any more.
即如果value值没有强引用了,那么对应的记录就会被丢弃(这句话有彩蛋). 所以也不用担心key导致的泄漏了.
当初写完这个后,感觉一切都比较的perfect. 但是还是too young too simple.
- 最后较成熟的方案
重新关注下第二步, 有彩蛋的那句话, 这句话再深入理解下就是,如果self.weak[key] = LocalCache.Dict(value)
这样,LocalCache.Dict(value)
没有其他强引用, 那么对不起,下一瞬间这个记录就没了(WTF).
所以之前的写法会导致--没有任何缓存作用(如果你耐着性子看到这,估计要骂娘了。。。),不过既然都写了这么多,方案还是有的(我在 http://stackoverflow.com 找到类似的方案,稍微改进了下),既然要强引用,那就给他强引用了,代码如下:
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这个队列来保存强引用, 并利用collections.deque的一个特性:
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设置了大小(通过maxlen,不传或设为None则没有限制), 那么deque满的时候,新添加的对象会将之前的'老家伙挤出去'.
整个过程是这样的,刚开始往缓存加数据时, 添加的每一个值都有一个弱引用和强引用,不停的加,直到deque的队列满了(地主家也没余粮啊), 这时后面每加一个,都将导致deque中最早加入的强引用被deque废弃,而被废弃的强引用对应的值只有弱引用了,于是与之相关的WeakValueDictionary记录也被回收了(不知道有没人闪过虚拟内存中内存不够用时,数据在硬盘和内存捣鼓的画面)
到这基本已经写完。当然这个LocalCache类还有很多可完善的地方,这里只是讲解下它的形成过程。
- 附加一个应用这个LocalCache类的函数调用缓存装饰器
from functools import wraps
def funcCache(expire=0):
caches = LocalCache()
def _wrappend(func):
@wraps(func)
def __wrapped(*args , **kwargs):
#计算出缓存的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