flask-cache 之緩存cache實現原理

前言

flask-cache的版本爲:0.13.1。具體的使用例程見官方網站
flask-cache主要實現了兩種功能,一種是對模板的緩存,一種是對視圖函/其他函數的緩存。其中對模板緩存的原理分析請戳這裏。下邊我們主要寫對函數的緩存原理。


源碼之旅

flask-cache 對函數的緩存有兩種方式,通俗的講可以分爲:
記憶參數型緩存:由@cached裝飾器實現
無記憶參數型緩存:由@memoize裝飾器實現

源碼結構解讀

flask-cache 是利用werkzeug提供的緩存函數實現的。
其中對緩存的存儲是由werkzeug.contrib.cache.py文件實現的。

1)cache.py結構圖

這裏寫圖片描述
由上圖可以看出,這裏主要實現了SimpleCache,redis,memcached等緩存的配置功能。其中類BaseCache是其他緩存的基類,定義了緩存操作的接口操作。其方法圖如下:
這裏寫圖片描述
下邊我們看一下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包的結構圖:
這裏寫圖片描述
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====                <ex1>
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可以唯一標示實例的方法,並實現對參數有記憶性

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章