python实现类redis缓存

作者: VxCoder | 来源:发表于2016-08-29 19:57 被阅读1680次

    越来越觉得的缓存是计算机科学里最NB的发明(没有之一), 现在项目用的是redis做的缓存, 它的两个特性用的蛮顺手的:

    1. 键值查找功能
    2. 缓存可设置过期时间

    突突然的,觉得用python也可以简单的模拟一下,做一个本地的轻量级缓存.(不过, 注意一点:redis的缓存可以用于分布式, python模拟的则不行, 但是如果把本地缓存的过期时间设的短一点,比如10s, 在大并发下还是有不错表现的)

    对于键值查找功能, python原生的字典dict完美胜任. 对于缓存自动过期, 简单的想法后台起个服务, 定期检测, 但是这样代码会相对复杂,而且还要抢占宝贵的cpu资源,失去了轻量级的初衷.

    那么思路是这样的:

    1. 只要将每个对应的缓存键值,加一个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步, 基本功能已实现,现在收拾"烂摊子". 步骤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.

    1. 最后较成熟的方案

    重新关注下第二步, 有彩蛋的那句话, 这句话再深入理解下就是,如果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类还有很多可完善的地方,这里只是讲解下它的形成过程。

    1. 附加一个应用这个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
    

    相关文章

      网友评论

      • CrazyTianC:通过expire判断的时候,如果过期不能直接把key删了么?
      • VxCoder:functools.lru_cache类似这功能,只是没有过期功能

      本文标题:python实现类redis缓存

      本文链接:https://www.haomeiwen.com/subject/pqbtettx.html