Djangorestframework分页缓存的实现

前言

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数据结构,比较简单,其原理

1
2
3
4
5
6
7
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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> 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的名字

分页缓存的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# -*- 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
1
2
3
4
# 使用
@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

----------本文完,感谢您的阅读----------