前言

Django自带了缓存机制,djangorestframework也可以使用drf-extensions扩展进行接口缓存,但这两种缓存都是基于url, 基于request.get_full_path(),这就导致管理缓存不是很方便。比如要删除一个带参数的url缓存,就要cache.deletepattern(“url*“), 如果reids key很多的话,会很影响性能(redis是单线程要阻塞,keys命令会遍历整个key,会很慢,可以考虑scan,但这不在本文讨论范围内)。

一种方案

因为redis有丰富的数据结构,所以分页缓存可以有一种解决方案:对于不带参数的页面或接口缓存,可以直接用前面提到的方式,对于带参数的页面缓存,可以用reids的hash结构,其中url作为key,url的参数作为hash的key,response作为value,这样要删除缓存的话,只要删除作为url的key就可以了。

django&drf-extensions缓存

django基于redis的缓存只用到了redis的string数据结构,比较简单,其原理

given a URL, try finding that page in the cache
if the page is in the cache:
    return the cached page
else:
    generate the page
    save the generated page in the cache (for next time)
    return the generated page

其中视图缓存用@cache_page(timeout, cache, key_prefix)

>>> from django.core.cache import cache
>>> cache.set('my_key', 'hello, world!', 30)
>>> cache.get('my_key')
# Wait 30 seconds for 'my_key' to expire...
>>> cache.get('my_key')
None
>>> cache.get('my_key', 'has expired')
'has expired'
>>> cache.set('add_key', 'Initial value')
>>> cache.add('add_key', 'New value')
>>> cache.get('add_key')
'Initial value'
>>> cache.set('a', 1)
>>> cache.set('b', 2)
>>> cache.set('c', 3)
>>> cache.get_many(['a', 'b', 'c'])
>>> cache.delete_maney(['a', 'b', 'c'])
{'a': 1, 'b': 2, 'c': 3}

其他的api,详见django/core/cache/backends/base.py drf-extensions缓存原理大体也差不多,其可以通过key_func自定义key的名字

分页缓存的实现

# -*- coding: utf-8 -*-
""""
Name: 分页缓存装饰器
Author: itswcg
Datetime: 2018-9-20
Desc: fork drf-extensions/cache/decorators.py 基于drf-extentions修改,以下注释的地方即表示修改的地方
"""
import json # json
from functools import wraps

from rest_framework.response import Response # 使用restframework响应
from django.utils.decorators import available_attrs

from rest_framework_extensions.settings import extensions_api_settings
from django.utils import six


def get_page_cache():
    from django_redis import get_redis_connection # 使用django_redis操作redis其他数据结构
    return get_redis_connection()


class CacheResponse(object):
    def __init__(self,
                 timeout=None,
                 key_func=None,
                 cache=None,
                 cache_errors=None,
                 page=False): # 加一个参数是否分页
        if timeout is None:
            self.timeout = extensions_api_settings.DEFAULT_CACHE_RESPONSE_TIMEOUT
        else:
            self.timeout = timeout

        if key_func is None:
            self.key_func = extensions_api_settings.DEFAULT_CACHE_KEY_FUNC
        else:
            self.key_func = key_func

        if cache_errors is None:
            self.cache_errors = extensions_api_settings.DEFAULT_CACHE_ERRORS
        else:
            self.cache_errors = cache_errors

        self.page = page

        self.cache = get_page_cache()

    def __call__(self, func):
        this = self

        @wraps(func, assigned=available_attrs(func))
        def inner(self, request, *args, **kwargs):
            return this.process_cache_response(
                view_instance=self,
                view_method=func,
                request=request,
                args=args,
                kwargs=kwargs,
            )

        return inner

    def process_cache_response(self,
                               view_instance,
                               view_method,
                               request,
                               args,
                               kwargs):
        key = self.calculate_key(
            view_instance=view_instance,
            view_method=view_method,
            request=request,
            args=args,
            kwargs=kwargs
        )

        params = request.get_full_path().replace(request.path, '') # 获取参数
        hash_key = params if params else '?page=1'

        if self.page:
            response = self.cache.hget(key, hash_key) # 如果使用分页缓存,用hash
        else:
            response = self.cache.get(key) # 否则,用string
        if not response:
            response = view_method(view_instance, request, *args, **kwargs)
            response = view_instance.finalize_response(request, response, *args, **kwargs)
            response.render()  # should be rendered, before picklining while storing to cache

            if not response.status_code >= 400 or self.cache_errors:
                response_dict = (
                    response.rendered_content,
                    response.status_code,
                    response._headers
                )
                content = response_dict[0] # 只缓存内容
                if self.page:
                    self.cache.hset(key, hash_key, content) # 设置缓存
                    self.cache.expire(key, self.timeout)
                else:
                    self.cache.set(key, content, self.timeout)
        else:
            content = response
            response = Response(json.loads(content)) # 解析

        if not hasattr(response, '_closable_objects'):
            response._closable_objects = []

        return response

    def calculate_key(self,
                      view_instance,
                      view_method,
                      request,
                      args,
                      kwargs):
        if isinstance(self.key_func, six.string_types):
            key_func = getattr(view_instance, self.key_func)
        else:
            key_func = self.key_func
        return key_func(
            view_instance=view_instance,
            view_method=view_method,
            request=request,
            args=args,
            kwargs=kwargs,
        )

cache_response = CacheResponse

# 使用
@cache_response(timeout=60 * 60 * 1, cache='default', page=True, key_func=calculate_key)
def main(request, *args, **kwargs):
    pass

参考

https://docs.djangoproject.com/en/2.1/topics/cache/
https://www.v2ex.com/t/491596#reply41
https://blog.csdn.net/permike/article/details/53217742