Django基于对象的缓存机制

分类-Google App Engine 二月 27, 2010 23:03 896 Views 2 Comments
标签:

 

以博客为例说一下我实现的一个缓存机制,它用起来是这样的

@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



COMMENTS | 评论

自言自语  --  发表新文章 管理

Powered by pLite :)  由 Google AppEngine 强力驱动