Django 模板缓存设计
问题
项目中的模板渲染速度特别慢,模板又特别多,逐个修改模板不太现实,只能通过有限制的批量替换以及对 context 做一些处理实现速度优化。
QuerySet 如何缓存
Django 的 QuerySet 是 lazy 的,只在一些特定情形下执行,细节可以参考 doc , 这样就带来了一个问题,比如以下很常见的代码,其中的 object_list
是一个 QuerySet
{% for obj in object_list|slice:":10" %}
{{ obj.name }}
{% endfor %}
由于 slice 操作是在模板中的,如何在不更改模板的前提下缓存 QuerySet ?
很容易想到重写 slice,slice 虽然是一个内置的 template tag ,但是也是通过 @register.filter() 注册的,非常方便覆盖。
@register.filter("slice", is_safe=True)
def slice_filter(value, arg):
try:
bits = []
for x in arg.split(':'):
if len(x) == 0:
bits.append(None)
else:
bits.append(int(x))
slice_object = slice(*bits)
if slice_object.start or slice_object.stop:
if isinstance(value, (models.QuerySet)):
if value.model is User:
# Do something you want...
return value[slice_object]
except (ValueError, TypeError):
return value
上述代码展示了重写 slice ,可以通过 isinstance(value, (models.QuerySet)) 判断前置类型,还可以通过 value.model 判断 对应的 Model,这样似乎可以实现缓存逻辑。
但是,很多时候缓存要求非常精细,比如,需要区分不同 User 做不同的缓存,在不执行 QuerySet 的前提下无法读出数据,也就无法判断是哪个 User 的数据。当然也不是没有办法,比如如果 QuerySet 是通过 QuerySet.filter(user_id=1) 类似的语句生成的,可以访问 QuerySet 的属性 拿到具体的 WHERE 查询对象,通过 WHERE 条件判断 QuerySet 的意图。但是这样做比较复杂而且丧失了通用性。
还有一种方法是给 QuerySet 对象加上一层代理,根据 Python magic method 的规则,for 循环会首先调用 __iter__
方法,而 slice 会调用 __getitem__
方法,参考如下。
class QuerySetProxy(object):
def __init__(self, queryset):
self._queryset = queryset
def __iter__(self):
cache_value = get_cache()
if cache_value is hit:
return cache_value
cache_value = list(self._queryset.__iter__())
set_cache(cache_value)
return cache_value
def __getitem__(self, k):
if isinstance(k, slice):
cache_value = get_cache()
if cache_value is hit:
return cache_value
cache_value = list(self._queryset.__getitem__(k))
set_cache(cache_value)
return cache_value
return self._queryset.__getitem__(k)
然后,上述代码中的 set_cache 是如何区分不同 QuerySet 的呢?有个方法是计算 SQL 的 hash 值判断唯一性。通过 str(QuerySet.query) 可以拿到原始的 SQL 查询语句。
缓存的清除问题
缓存的清除是一个很头疼的问题,简单的想到两个自动化的方法,一个是通过 post_save 之类的信号;另一个是在 API 入口处统一处理,判断 REST API 的 method 为 POST, PUT, DELETE, PATCH 时可以进行对应的缓存操作。
模板片段的缓存
上述缓存 QuerySet 之后,对于模板变量特别多的片段,效果并不是特别好,对于这种模板直接缓存渲染之后的片段效果会更好。
Django 自带了一个 cache tag github ,支持设置时间和 key,但是不够智能,因为需要手动设置 key 并不灵活。或许我们可以通过获取模板文件路径和行号作为缓存 key ?思路是对的,但是 Django 只有在开启 template_debug 的时候才能在 tag 的作用域内访问到模板文件元信息。
好在每个 Node 对象都可以获得 child_nodelists ,然后递归寻找子 Node,而且对于 TextNode 可以访问到 text 属性!递归一遍可以计算出模板片段的 Hash。
class CacheNode(Node):
def get_node_hash(self, context):
for attr in self.child_nodelists:
if hasattr(self, attr):
print getattr(self, attr)