python实现类redis缓存

越来越觉得的缓存是计算机科学里最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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容

  • 分布式缓存技术PK:选择Redis还是Memcached? 经平台同意授权转载 作者:田京昆(腾讯后台研发工程师)...
    meng_philip123阅读 68,915评论 7 60
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,686评论 0 9
  • // +-----------------------------------------------------...
    Robinbing阅读 1,414评论 0 0
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,626评论 18 139
  • 今天实现了一个愿望。 我和魏先生都喜欢玩网络游戏,从大学到现在一直都喜欢打英雄联盟。以前我们就幻想有一天,我们买了...
    小菠萝的日常阅读 175评论 0 0