mako根据条件判断是否使用页面缓存

最近遇到网站速度慢的情况,排查许久没查出什么原因。于是想着匿名用户的访问量占据了一半多,如果这一部分的请求全部缓存下来,那么应该能够很大程度上提升网站的响应速度。 之前已经在一些页面里面使用了 Mako 的页面缓存,具体的文档可以查看 Mako 官网

# CacheImepl的定义
class CMemcachedImpl(CacheImpl):
    def __init__(self, cache):
        from mysite.model.init_db import page_mc as mc
        super(CMemcachedImpl, self).__init__(cache)
        self.mc = mc

    def get_or_create(self, key, creation_function, **kw):
        value = self.mc.get(key)
        if not value:
            value = creation_function()
            timeout = kw.get('timeout', 60)
            try:
                timeout = int(timeout)
            except ValueError:
                timeout = 60

            # 防止缓存雪崩,即大量待缓存在同一时刻失效
            timeout = timeout + random.randint(0, timeout / 10)
            self.mc.set(key, value, timeout)

        return value

    def set(self, key, value, **kw):
        timeout = kw.get('timeout', 60)
        return self.mc.set(key, value, timeout)

    def get(self, key, **kw):
        return self.mc.get(key)

    def invalidate(self, key, **kw):
        return self.mc.delete(key)

模板中用法:

<%block cached="True" cache_key="cache_key_to_generate" cache_timeout="3600">
page content
</%block>

因为只对匿名用户缓存,最开始想到的就是动态的设置`cached`的值,登录用户为`False`,匿名用户为`True`。于是开始第一次尝试

<%block cached="${request.user}" cache_key="cache_key_to_generate" cache_timeout="3600">
page content
</%block>

结果Mako直接报错了,${request.user}`无法`eval。查看代码得知,Mako直接把 cached 的值当做 Python 代码执行。那么把 ${} 去掉怎么样?

<%block cached="request.user" cache_key="cache_key_to_generate" cache_timeout="3600">
page content
</%block>

又报了错:request 未定义。eval 的上下文中没有 request 的定义。这个方法失败。

Mako是可以在 block 里面加 cache_xxx 这种 cache_ 为前缀的参数,这些参数是可以被 CacheImpl 读取到的。既然 cached 没办法动态的修改, 那么我新加一个 cache_passthrough 的参数(用来表示穿透缓存,即不使用缓存)。匿名用户访问的时候 cache_passthrough 设置为 True, 登录用户访问的时候改为 False。然后在 CacheImpl 里面根据读取到的 passthrough 来决定是否直接跳过缓存。

def get_or_create(self, key, creation_function, **kw):
    passthrough = kw.get('passthrough', False)
    if passthrough:
      return creation_function()

    value = self.mc.get(key)
    if not value:
        value = creation_function()
        timeout = kw.get('timeout', 60)
        try:
            timeout = int(timeout)
        except ValueError:
            timeout = 60

        # 防止缓存雪崩,即大量待缓存在同一时刻失效
        timeout = timeout + random.randint(0, timeout / 10)
        self.mc.set(key, value, timeout)

    return value
<%block cached="True" cache_key="cache_key_to_generate" cache_timeout="3600" cache_passthrough="bool(request.user)">
page content
</%block>

然后结果很奇怪。如果第一次是登录状态访问,那么之后无论登录与否,页面都不会被缓存。如果第一次是匿名访问,那么之后无论登录与否, 返回的都是缓存下来的同一个结果。花时间看 Mako 的代码发现,Mako 对除了 cache_key 以外的 cache_ 的参数进行了缓存。

def _get_cache_kw(self, kw, context):
    defname = kw.pop('__M_defname', None)
    if not defname:
        tmpl_kw = self.template.cache_args.copy()
        tmpl_kw.update(kw)
    elif defname in self._def_regions:   # 这个分支,参数被缓存在了 self._def_regions
        tmpl_kw = self._def_regions[defname]
    else:
        tmpl_kw = self.template.cache_args.copy()
        tmpl_kw.update(kw)
        self._def_regions[defname] = tmpl_kw
    if context and self.impl.pass_context:
        tmpl_kw = tmpl_kw.copy()
        tmpl_kw.setdefault('context', context)
    return tmpl_kw

完整代码在 mako/cache.py

因为不明白为什么要缓存参数,还去Mako的邮件组里面提问了下。Michael Bayerh 回复说 CacheImpl 这个东西的存在是为了抽象出从不同地方的缓存去数据这个行为。 而不是用来做其他一些逻辑上的东西。原话如下

but the arguments that are passed to get_or_create() were intended to be for the purposes of executing the “data retrieval” function, and not for the benefit of the cache impl wrapper itself.
— Michael Bayerh

他推荐用 decorator 自己实现一个缓存的机制来做这个事情。decorator 的文档在 http://docs.makotemplates.org/en/latest/filtering.html#decorating

我的需求是对全站的页面根据登录状态进行缓存,所以缓存的 key 就根据 URL 来自动生成了。代码中的 request_hash 就是根据 URL 来生成 hash 值。

<%!
from mako.runtime import capture
from mysite.ctrl.utils import request_hash

def cache_for_anomynous(fn):
    def def_func(context, *args, **kwargs):
        cache = context.get('local').cache
        cache_enabled = getattr(cache.template, 'cache_enabled', True)
        user = context.get('request').user
        if not cache_enabled or user:
            val = capture(context, fn, *args, **kwargs)
        else:
            key = 'html.cache_for_anoumynous:%s' % request_hash()
            val = cache.get(key)
            if not val:
                val = capture(context, fn, *args, **kwargs)
                cache.set(key, val, timeout=300)
        context.write(val)
        return ''
    return def_func
%>

<%block decorator="cache_for_anonymous">

page content

</%block>