flask-cache 之缓存cache实现原理

前言

flask-cache的版本为:0.13.1。具体的使用例程见官方网站
flask-cache主要实现了两种功能,一种是对模板的缓存,一种是对视图函/其他函数的缓存。其中对模板缓存的原理分析请戳这里。下边我们主要写对函数的缓存原理。


源码之旅

flask-cache 对函数的缓存有两种方式,通俗的讲可以分为:
记忆参数型缓存:由@cached装饰器实现
无记忆参数型缓存:由@memoize装饰器实现

源码结构解读

flask-cache 是利用werkzeug提供的缓存函数实现的。
其中对缓存的存储是由werkzeug.contrib.cache.py文件实现的。

1)cache.py结构图

flask-cache 之缓存cache实现原理_第1张图片
由上图可以看出,这里主要实现了SimpleCache,redis,memcached等缓存的配置功能。其中类BaseCache是其他缓存的基类,定义了缓存操作的接口操作。其方法图如下:
flask-cache 之缓存cache实现原理_第2张图片
下边我们看一下cache.py中自己实现的一个简单的cache,SimpleCache

2)SimpleCache 实现原理

SimpleCache 是一个简单的内存缓存,只能用于单进程的环境,不能保证线程的安全性。而且也没有实现数据的持久化操作。其缓存的失效机制是由两个参数控制:threshold 与default_timeout。
基本原理是:当缓存中的key的个数超过阈值threshold ,删除生命周期大于default_timeout的key。如果default_timeout =0 表示永远不超期。

    def __init__(self, threshold=500, default_timeout=300):
        BaseCache.__init__(self, default_timeout)
        self._cache = {}
        self.clear = self._cache.clear
        self._threshold = threshold

上边是初始化方法,默认的缓存key的容量是500,默认超期时间是300s.
初始化时,首先初始化基类BaseCache,然后创建了一个字典self._cache用于存储缓存数据。

    def _prune(self):
        if len(self._cache) > self._threshold:   # 当前缓存的key的个数超过阈值
            now = time()
            toremove = []
            for idx, (key, (expires, _)) in enumerate(self._cache.items()):
                # 查找的生命周期已经超时的key
                if (expires != 0 and expires <= now) or idx % 3 == 0:
                    toremove.append(key)
            for key in toremove:
                self._cache.pop(key, None)

_prune 方法实现超期检测功能,每当进行set缓存操作时,会首先调用_prune 来校验剔除超期的元素。

    def set(self, key, value, timeout=None):
        expires = self._normalize_timeout(timeout)
        self._prune()
        self._cache[key] = (expires, pickle.dumps(value,
                                                  pickle.HIGHEST_PROTOCOL))
        return True

set函数是缓存的核心函数,其流程是首先计算期望的超期时间,然后调用_prune函数 检查校验操起元素。讲要缓存的元素放入内存的字典中,注意,这里字典的值不是直接存储数据的值,而是将数据的value进行序列化之后与expires组成元组作为key的value放入字典中。序列化的目的是为了方便对象的存储和网络传输。
get函数负责中缓存中取值,实现很简单,就是从字典中取值,然后判断是否超期,如果超期返回None

3)对函数缓存的具体实现

首先看一下flask-cache包的结构图:
flask-cache 之缓存cache实现原理_第3张图片
init.py文件实现核心的功能:函数的缓存功能
_compat.py文件是python2与3的兼容转换
backends.py 文件是与werkzeug的cache.py的对接,调用具体的cache类型。
jinja2ext.py 文件 用来缓存模板

def simple(app, config, args, kwargs):
    kwargs.update(dict(threshold=config['CACHE_THRESHOLD']))
    return SimpleCache(*args, **kwargs)

由上边backends.py的一部分代码可以看出,主要实现werkzeug的cache类的代理,以及参数更新功能。
接下来看一下__init__.py对缓存逻辑的核心处理:
精简之后的核心代码如下:

class Cache(object):

    def __init__(self, app=None, with_jinja2_ext=True, config=None):
        if app is not None:
            self.init_app(app, config)                  

    def init_app(self, app, config=None):
        base_config = app.config.copy()                                     # <1> 
        config.setdefault('CACHE_DEFAULT_TIMEOUT', 300)                     # <2> 
        if self.with_jinja2_ext:
            from .jinja2ext import CacheExtension, JINJA_CACHE_ATTR_NAME
            setattr(app.jinja_env, JINJA_CACHE_ATTR_NAME, self)
            app.jinja_env.add_extension(CacheExtension)                     # <3> 
        self._set_cache(app, config)

    def _set_cache(self, app, config):
        import_me = config['CACHE_TYPE']
        if '.' not in import_me:
            from . import backends
            try:
                cache_obj = getattr(backends, import_me)                    #<4> 
        if not hasattr(app, 'extensions'):
            app.extensions = {}
        app.extensions['cache'][self] = cache_obj(
                app, config, cache_args, cache_options)                     #<5>

    def cached(self, timeout=None, key_prefix='view/%s', unless=None):     #<6> 
        def decorator(f):
            @functools.wraps(f)
            def decorated_function(*args, **kwargs):
                if callable(unless) and unless() is True:                   #<7> 
                    return f(*args, **kwargs)
                try:
                    cache_key = decorated_function.make_cache_key(*args, **kwargs)   #<8> 
                    rv = self.cache.get(cache_key)
                except Exception:
                    #.....
                if rv is None:
                    rv = f(*args, **kwargs)                                         
                    try:
                        self.cache.set(cache_key, rv,
                                   timeout=decorated_function.cache_timeout)        #<9>
                    except Exception:
                        #......
                return rv

            def make_cache_key(*args, **kwargs):    #<10>
                if callable(key_prefix):            #<11> 
                    cache_key = key_prefix()
                elif '%s' in key_prefix:            #<12> 
                    cache_key = key_prefix % request.path     
                else:                               #<13> 
                    cache_key = key_prefix
                return cache_key

            decorated_function.uncached = f
            decorated_function.cache_timeout = timeout                  #<14> 
            decorated_function.make_cache_key = make_cache_key
            return decorated_function
        return decorator

    def _memvname(self, funcname):
        return funcname + '_memver'

    def _memoize_make_version_hash(self):
        return base64.b64encode(uuid.uuid4().bytes)[:6].decode('utf-8')

    def _memoize_version(self, f, args=None,
                         reset=False, delete=False, timeout=None):        # <23>
        fname, instance_fname = function_namespace(f, args=args)
        version_key = self._memvname(fname)
        fetch_keys = [version_key]

        if instance_fname:
            instance_version_key = self._memvname(instance_fname)
            fetch_keys.append(instance_version_key)

        # Only delete the per-instance version key or per-function version
        # key but not both.
        if delete:
            self.cache.delete_many(fetch_keys[-1])
            return fname, None

        version_data_list = list(self.cache.get_many(*fetch_keys))
        dirty = False

        if version_data_list[0] is None:
            version_data_list[0] = self._memoize_make_version_hash()
            dirty = True

        if instance_fname and version_data_list[1] is None:
            version_data_list[1] = self._memoize_make_version_hash()
            dirty = True

        # Only reset the per-instance version or the per-function version
        # but not both.
        if reset:                                                           # <22> 
            fetch_keys = fetch_keys[-1:]
            version_data_list = [self._memoize_make_version_hash()]
            dirty = True

        if dirty:
            self.cache.set_many(dict(zip(fetch_keys, version_data_list)),
                                timeout=timeout)

        return fname, ''.join(version_data_list)

    def _memoize_make_cache_key(self, make_name=None, timeout=None):
        def make_cache_key(f, *args, **kwargs):
            _timeout = getattr(timeout, 'cache_timeout', timeout)
            fname, version_data = self._memoize_version(f, args=args,
                                                        timeout=_timeout)    #<16> 
            if callable(f):
                keyargs, keykwargs = self._memoize_kwargs_to_args(f,
                                                                 *args,
                                                                 **kwargs) #<17> 
            else:
                keyargs, keykwargs = args, kwargs

            try:
                updated = "{0}{1}{2}".format(altfname, keyargs, keykwargs) #<18> 
            except AttributeError:
                updated = "%s%s%s" % (altfname, keyargs, keykwargs)

            cache_key = hashlib.md5()
            #......
            cache_key += version_data                                   # <19> 
            return cache_key
        return make_cache_key


    def memoize(self, timeout=None, make_name=None, unless=None):

        def memoize(f):
            @functools.wraps(f)
            def decorated_function(*args, **kwargs):
                                                                    #<15>
            return decorated_function
        return memoize

    def delete_memoized(self, f, *args, **kwargs):

        try:
            if not args and not kwargs:                             #<20> 
                self._memoize_version(f, reset=True)
            else:
                cache_key = f.make_cache_key(f.uncached, *args, **kwargs)   #<21> 
                self.cache.delete(cache_key)
        except Exception:
            #....

代码的说明如下:
<1> 复制app的配置
<2> 设置默认参数
<3> jinja模板缓存处理
<4> 从backends获取具体的缓存对象,一个简单的例子如下:
由输出结果可以看出,当我们在backends文件定义类时,可以通过包的属性获取特定的类

from flask_cache import backends
cacheobj = getattr(backends,'simple')
cacheobj
Out[3]: <function flask_cache.backends.simple>

<5>当前的cache实例注册到 flask应用的扩展中
<6> 缓存装饰器的实现
<7> 当unless不为空时,此时变成回调功能,执行回调函数unless之后,直接返回函数当前计算的值
<8> 位cache生成key
<9> 缓存没有命中,计算函数的当前值,作为缓存值存储
<10>生成函数需要的键,主要有三种方式
<11> 第一种key,是由key_prefix函数的返回值决定
<12> 第二种key,默认方式,是由”view/”为前缀+request.path构成
<13> 第三种key,直接由key_prefix构成
<14> 内部的函数与装饰器函数绑定,便于外部调用
<16> 生成memoize的版本信息
<15> 与cache装饰器逻辑一致
<22> 进行reset操作,如何删除呢? 不明白
<23>更新hash版本,reset可以控制删除缓存
<17> 转化参数,有两种情况,当f 是函数时,返回值就是函数的入参数
函数返回的keykwargs 为{} 目前不支持关键字的cache
当f时某个类的方法时,返回值是将类的实例也做为参数返回。
详细见下方的实例代码的举例说明
<18> 未加工的key,同样有两种情形
<19> md5算法生成唯一的 随机码,在附加上版本信息。作为缓存唯一的Key
<20> 不带参数指定的删除,即删除函数或实例的所有缓存
<21> 带参数记忆的缓存删除方式
cache中对key的实例讲解

import time
from flask import Flask
from flask_cache import Cache
import random

app = Flask(__name__)
app.config['CACHE_TYPE'] = 'simple'
app.cache = Cache(app)

with app.test_request_context():
    @app.cache.memoize(5)
    def FUNCTION(a, b):
        return a + b + random.randrange(0, 100000)
    print("====cache the function====")
    result = FUNCTION(5, 2)

    class CLASS:
        @app.cache.memoize(5)
        def METHOD(self,a, b,):
            return a + b + random.randrange(0, 100000)+kwargs['x']
    print("====cache the class method====")
    result = CLASS().METHOD(5, 2)

上边代码的输出


====cache the function====                
PARAMS =  (5, 2) {}
KEY =  __main__.FUNCTION(5, 2){}
====cache the class method====            <ex2>
PARAMS =  ('<__main__.CLASS object at 0x0591E550>', 5, 2) {}
KEY =  __main__.CLASS.METHOD('<__main__.CLASS object at 0x0591E550>', 5, 2){}  

对函数的缓存,其中生成Key的参数就是函数的参数和关键字参数的拼接
生成key 的格式是 :包名.函数名.参数.关键字参数
对方法的缓存,生成key的参数是实例对象和参数的拼接
而生成的key格式是:包名.类名.方法名.参数 ,这样的key可以唯一标示实例的方法,并实现对参数有记忆性

你可能感兴趣的:(Flask)