Django基于对象的缓存机制
以博客为例说一下我实现的一个缓存机制,它用起来是这样的
@cache('Entry', 'id')
# 下面的view 中有个关键词参数叫做 id 用它和'Entry'来标识这个缓存
def entry_detail(request, id):
pass
@clear_cache('Entry', '$entry_id')
# 清除一个Entry 的缓存,它的id 是POST参数中的 entry_id
def entry_comment(request):
pass
平台
以GAE和Django 为例,稍加修改就可以运行在纯Django或者纯GAE项目上
缓存与逻辑的分离
将缓存的代码放入view 内我觉得很ugly ,所以使用decorator 实现缓存机制,这样缓存不会和业务逻辑耦合在一起
缓存与精确清理
页面的请求一般对应着某个对象,比如Category, Entry
而一个对象可以通过一个id, 或者key_name 唯一的识别这个对象,那么我就可以使用 “对象的类型名_对象识别符” 作为缓存的键值在缓存中唯一的标识这个对象
(注:下文中的id 泛指标识对象唯一性的一个值,如对象的数字id,或者文章的slug)
这样设计出缓存函数的原型
def cache(type, id):
"""
type -- 对象的类型
id -- 标识对象唯一的id
"""
pass
那么怎么获取这个对象的id呢,Django 有很漂亮的URL机制,我们可以从URL 中匹配这个值,并且传给view,我们当然就可以从view 函数截获这个id
@def cache(name, param_pos=''):
"""param_pos -- 对象的id在view 中的位置,可为数字,可为关键值参数的key"""
def wrap(fn):
def view(request, *args, **kwargs):
if type(param_pos) == int:
id = args[param_pos - 1]
elif type(param_pos) == str:
id = kwargs.get(param_pos, '')
else:
raise ValueError
...
return view
wrap.__name__ = fn.__name__
return wrap
对于不需要id 的页面,比如首页我们可以只使用第一个参数
@cache('index')
def index(request);
pass
如我有一个view 负责显示一个文章页面,于是我想这样写:
@cache('Entry', 1)
def entry_detail(request, id):
"""URL Pattern:/entry/(\d+)"""
pass
在我修改一篇文章时我想精确的清理掉这个缓存
@clear_cache('Entry', 1)
def entry_edit(request, id):
"""URL Pattern: /entry/(\d+)/edit"""
pass
当然在我对这篇文章发表评论时我也要清理掉这个缓存
@clear_cache('Entry', 'entry_id')
def comment_new(request, entry_id):
"""URL Pattern: /entry/(?P<entry_id>\d+)/comment"""
pass
有包含关系对象的缓存
在我发表一篇文章时,我需要更新首页和它的Category页面
@clear_cache('index')
@clear_cache('Category', '?')
def entry_new(request):
"""URL Pattern: /entry/new"""
pass
Category 后我该写什么呢,因为URL中未提供此category 的ID
当然你的URL可能是 /category_slug/archive/new 这时可以提取到category的slug_name,但一般会在发表文章页面让用户选择一个category
这时候我们可以重构我们的cache函数,让它支持从POST中提取值
@clear_cache('Category', '$category_id')
def entry_new(request):
"""URL Pattern: /entry/new"""
pass
# 哈哈我觉得非常漂亮 :) ~~
这些功能基本上已经能够缓存完一个完整的博客了,而且结构清晰,非常容易维护 :)
附上全部代码:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
from google.appengine.api import memcache
def cache(model_name, id_or_name, seconds=60*10, clear_when_post=False):
"""
通过 Model 和用来标识 Model 的唯一一个实例的值的连接建立一个唯一的 memcache key 值
model_name -- Model 的类名,或者为一个指定的标识前缀
id_or_name -- 标识唯一的一个 Model 实例的标识在view 函数中的参数位置.
当类型为 int 时则应为其在view *args 中的位置
为 str 时则应为关键值参数的名字
seconds -- 缓存的秒数
>>> @cache('Entry', 1)
>>> entry_detail(request, id):
>>> '''URL pattern : /entry/(\d+)'''
>>> pass
"""
def wrap(fn):
def view(request, *args, **kwargs):
"""用来替代原view"""
if type(id_or_name) == int:
value = args[id_or_name-1]
cache_key = '%s_%s' % (model_name, value)
elif type(id_or_name) == str:
if not id_or_name.startswith('$'):
value = kwargs.get(id_or_name)
cache_key = '%s_%s' % (model_name, value)
else:
value = request.POST.get(id_or_name[1:], '')
cache_key = '%s_%s' % (model_name, value)
else:
raise ValueError, 'unsupported memcache key type'
if request.method == 'POST' and clear_when_post is True:
memcache.delete(cache_key)
return fn(request, *args, **kwargs)
if not request.method == 'GET':
return fn(request, *args, **kwargs)
hit = memcache.get(cache_key)
if not hit:
resp = fn(request, *args, **kwargs)
memcache.set(cache_key, resp, seconds)
return resp
return hit
view.__name__ = fn.__name__
return view
return wrap
def clear_cache(*args, **kwargs):
"""
清楚缓存
"""
return cache(clear_when_post=True, *args, **kwargs )
def blank(*args, **kwargs):
def wrap(fn):
def view(request, *args, **kwargs):
return fn(request, *args, **kwargs)
view.__name__ = fn.__name__
return view
return wrap
from django.conf import settings
if settings.DEBUG:
cache = blank
clear_cache = blank
单元测试:
class CacheTest(unittest.TestCase):
def test_cache(self):
"""测试缓存系统"""
class Request(object):
method = ''
POST = {}
@cache('X', 1)
def x_detail(request, id):
return id
@clear_cache('X', 1)
def x_remove(request, id):
return True
request = Request()
request.method = 'GET'
assert x_detail(request, 1) == x_detail(request, 1)
assert x_detail(request, 1) == 1
assert memcache.get('X_1') == 1
assert memcache.get_stats()['hits'] == 3
x_detail(request, '2')
assert memcache.get('X_2') == '2'
assert memcache.get_stats()['items'] == 2
# 测试自动清除
request.method = 'POST'
x_remove(request, 1)
assert memcache.get('X_1') is None
assert memcache.get_stats()['items'] == 1
# POST 请求不缓存
request.POST = {'the_id':'2'}
x_detail(request, 3)
assert memcache.get('X_3') is None
assert memcache.get_stats()['items'] == 1
x_remove = clear_cache('X', '$the_id')(x_remove)
x_remove(request, '$the_id')
assert memcache.get('X_2') is None
assert memcache.get_stats()['items'] == 0