前言
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可以唯一標示實例的方法,並實現對參數有記憶性