缓存通常是提高应用程序性能的最有效方法。
对于动态网站,在呈现模板时,通常必须从各种来源(如数据库、文件系统和第三方api等)收集数据,处理数据,并在将数据提供给客户端之前应用业务逻辑。任何由网络延迟引起的延迟都会被用户注意到。
例如,假设您必须对外部API进行HTTP调用,以获取呈现模板所需的数据。即使在完美的条件下,这也会增加渲染时间,从而增加整体的加载时间。还有,如果API服务挂了怎么办?或者API请求速率受限?无论采用哪种方式,如果数据不经常更新,最好实现缓存机制,以避免为每个客户机请求一起进行HTTP调用。
本文将通过首先全面回顾Django的缓存框架,然后逐步详细介绍如何缓存Django视图来了解如何使用缓存。
依赖:
- Django v3.0.5
- django-redis v.4.11.0
- Python v3.8.2
- python-memcached v1.59
- Requests v2.23.0
目标
在本教程结束时,你应该能够了解并掌握:
解释为什么需要缓存一个Django视图
描述Django用于缓存的内置选项
用Redis缓存一个Django视图
用Apache Bench负载测试Django应用
- 用Memcached缓存Django视图
Django缓存类型
Django有几个内置缓存后台功能,以及支持[自定义后台功能。
内置选项有:
Memcached: Memcached是一个基于内存、键值存储小块数据的存储。它支持跨多个服务器的分布式缓存。
Database:这里的缓存片段存储在数据库中。为了达到这个目的,可以使用Django的一个管理命令创建一个表。这不是性能最好的缓存类型,但它对于存储复杂的数据库查询很有用。
File system:缓存保存在文件系统上,每个缓存值分别保存在不同的文件中。这是所有缓存类型中最慢的一种,但是在生产环境中最容易设置。
本地内存:本地内存缓存,最适合本地开发或测试环境。虽然它几乎和Memcached一样快,但它不能扩展到单个服务器之外,因此它不适合用于任何使用多个web服务器的应用程序作为数据缓存。
Dummy:一个“虚拟”缓存,它实际上不缓存任何东西,但仍然实现了缓存接口。当您不想进行缓存,但又不想更改代码时,可以在开发或测试中使用它。
Django缓存级别
在Django中,缓存可以在不同的层级(或站点的不同部分)上实现。您可以缓存整个站点或特定的部分以不同的粒度级别(按粒度降序列出):
网站缓存
这是在Django中实现缓存的最简单方法。为此,你需要做的只是将两个中间件类添加到settings.py
文件:
MIDDLEWARE = [
'django.middleware.cache.UpdateCacheMiddleware', # NEW
'django.middleware.common.CommonMiddleware',
'django.middleware.cache.FetchFromCacheMiddleware', # NEW
]
中间件的顺序很关键,.
UpdateCacheMiddleware
要放在FetchFromCacheMiddleware
前面. 更多信息参考官方文档 Order of MIDDLEWARE 。
然后添加以下设置:
CACHE_MIDDLEWARE_ALIAS = 'default' # which cache alias to use
CACHE_MIDDLEWARE_SECONDS = '600' # number of seconds to cache a page for (TTL)
CACHE_MIDDLEWARE_KEY_PREFIX = '' # should be used if the cache is shared across multiple sites that use the same Django instance
如果您的站点只有很少内容或没有动态内容,那么缓存整个站点可能是一个不错的选择,但对于基于内存缓存的大型站点来说,这可能不适合使用,因为内存消耗很大。
视图缓存
如果不想在动态内容上消耗太大的内存,我们可以缓存特定的视图页面。
这就是本篇文章使用的方法。当你想在Django应用中实现缓存时,你也应该优先考虑使用视图缓存。
你可以使用cache_page装饰器来实现这种类型的缓存,可以直接在视图函数上实现,也可以在路径上使用。
# urls.py
from django.views.decorators.cache import cache_page
@cache_page(60 * 15)
def your_view(request):
...
# or
from django.views.decorators.cache import cache_page
urlpatterns = [
path('object/<int:object_id>/', cache_page(60 * 15)(your_view)),
]
视图缓存是基于URL路径的,所以对' object/1 '和' object/2 '的两个请求将会独立并分别缓存。
值得注意的是,直接在视图上实现缓存使得在某些情况下禁用缓存变得更加困难。例如,如果希望允许某些用户在没有缓存的情况下访问视图,该怎么办?通过' URLConf '启用缓存,可以将不同的URL关联到不使用缓存的视图:
from django.views.decorators.cache import cache_page
urlpatterns = [
path('object/<int:object_id>/', your_view),
path('object/cache/<int:object_id>/', cache_page(60 * 15)(your_view)),
]
模板片段缓存
如果模板中包含数据更改频繁的部分,那么你可能希望将它们排除在缓存之外。
例如,你可能在模板区域的导航栏中使用经过身份验证的用户的电子邮件。如果你有成千上万的用户,那么这个片段会在RAM中重复数千次,每个用户一个。这就是模板分片缓存发挥作用的地方,它允许你指定要缓存的模板的特定区域。
例如,缓存一个对象列表:
{% load cache %}
{% cache 500 object_list %}
<ul>
{% for object in objects %}
<li>{{ object.title }}</li>
{% endfor %}
</ul>
{% endcache %}
这里, {% load cache %}
会加载缓存的模板标签template tag
,然后定义一个500秒过期时间的object_list
缓存分片。
底层缓存API
如果前面的选项仍不能满足细粒度的情况下,我们可以使用底层API通过缓存键值
来管理缓存中的单个对象。
例如:
from django.core.cache import cache
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
objects = cache.get('objects')
if objects is None:
objects = Objects.all()
cache.set('objects', objects)
context['objects'] = objects
return context
在本例中,我们缓存了一个对象,但是,如果向数据库中添加、更改或删除对象时,我们希望使缓存失效(或删除)。要解决这个问题,需要更新缓存,其中一种方法是通过DJANGO信号(signal):
from django.core.cache import cache
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
@receiver(post_delete, sender=Object)
def object_post_delete_handler(sender, **kwargs):
cache.delete('objects')
@receiver(post_save, sender=Object)
def object_post_save_handler(sender, **kwargs):
cache.delete('objects')
下面,我们详细举例说明
项目设置
从cache-django-view 中克隆出base
分支:
$ git clone https://github.com/testdrivenio/cache-django-view.git --branch base --single-branch
$ cd cache-django-view
创建虚拟环境,激活并安装依赖:
$ python3.8 -m venv venv
$ source venv/bin/activate
(venv)$ pip install -r requirements.txt
执行 Django migrations,然后启动开发服务器:
(venv)$ python manage.py migrate
(venv)$ python manage.py runserver
现在浏览 http://127.0.0.1:8000,如果一切顺利,你将会看到下面的页面:
在终端上会显示页面的执行时间,记录这个数值:
Total time: 2.23s
执行时间来自中间件core/middleware.py
:
import logging
import time
def metric_middleware(get_response):
def middleware(request):
# Get beginning stats
start_time = time.perf_counter()
# Process the request
response = get_response(request)
# Get ending stats
end_time = time.perf_counter()
# Calculate stats
total_time = end_time - start_time
# Log the results
logger = logging.getLogger('debug')
logger.info(f'Total time: {(total_time):.2f}s')
print(f'Total time: {(total_time):.2f}s')
return response
return middleware
下面简单介绍一个视图apicalls/views.py:
import datetime
import requests
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
这个视图会对httpbin.org网站进行HTTP调用。为了模拟长时间请求,API的响应将延迟两秒钟。因此,不仅初次访问http://127.0.0.1:8000有延迟,而且在后续每个请求上都有大约两秒钟的延迟。虽然对第一次请求来说,两秒的加载在某种程度上是可以接受的,但对于后续的请求来说,这是完全不可接受的,因为数据没有改变。下面,让我们通过使用Django的视图缓存功能来解决这个问题。
流程:
在初次请求时对httpbin.org进行完整的HTTP调用;
缓存该视图;
在后续的请求中将从缓存中提出数据,绕过HTTP调用;
在一段时间(TTL)之后使缓存失效。
DJANGO压力测试方法
在添加缓存之前,让我们快速运行一个负载测试,使用Apache Bench获得基准测试,以大致了解我们的应用程序每秒可以处理多少请求。
Apache Bench在Mac上是预装的。
如果是Linux系统,可能已经安装好了。如果没有,你可以通过
APT -get install apache2-utils
或yum install httpd-tools
安装。Windows用户需要下载并解压Apache的二进制文件安装。
添加依赖 Gunicorn :
gunicorn==20.0.4
停止开发服务器并安装Gunicorn:
(venv)$ pip install -r requirements.txt
下面使用Gunicorn启动服务器,我们使用4个线程workers
(venv)$ gunicorn core.wsgi:application -w 4
打开一个新的终端,启动Apache Bench:
$ ab -n 100 -c 10 http://127.0.0.1:8000/
上面的命令将模拟10个并发线程,每个进程有100个连接(请求)。记录终端的执行时间:
Requests per second: 1.69 [#/sec] (mean)
请注意,Django调试工具栏会增加一些开销。一般来说,基准测试很难完全正确。重要的是保持一致性。选择一个关注的指标,并为每个测试使用相同的环境。
关闭Gunicorn服务器,重新启动Django开发服务器:
(venv)$ python manage.py runserver
接下来,让我们看看如何缓存视图。
如何缓存视图
首先用“@cache_page”装饰器装饰“ApiCalls”视图,如下所示:
import datetime
import requests
from django.utils.decorators import method_decorator # NEW
from django.views.decorators.cache import cache_page # NEW
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
@method_decorator(cache_page(60 * 5), name='dispatch') # NEW
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
因为我们使用的是class-based的视图,所以不能直接在类上放置装饰器。我们需要用' method_decorator '并指定name参数为' dispatch '。本例中的缓存将失效时间设置为5分钟。
或者,在settings.py
这样设置:
# Cache time to live is 5 minutes
CACHE_TTL = 60 * 5
然后在视图中引入CACHE_TTL
:
import datetime
import requests
from django.conf import settings
from django.core.cache.backends.base import DEFAULT_TIMEOUT
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
CACHE_TTL = getattr(settings, 'CACHE_TTL', DEFAULT_TIMEOUT)
@method_decorator(cache_page(CACHE_TTL), name='dispatch')
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
接下来,让我们添加一个缓存后端。
Redis vs Memcached
For more on this, review this StackOverflow answer.
Next, pick your data store of choice and let's look at how to cache a view.
Memcached和Redis是基于内存的键值型
数据存储。它们都易于使用,并针对高性能检索进行了优化。
你可能不会看到两者在性能或内存消耗方面有太大的区别。不过,Memcached的配置稍微容易一些,因为它的设计宗旨是简单易用。另一方面,Redis拥有更丰富的特性,所以除了缓存,它还有更广泛的用例。例如,它经常用于存储用户会话或作为发布/订阅系统中的消息代理。因为Redis的灵活性,除非你已经使用了Memcached,否则Redis是更好的解决方案。
Option 1: Redis 与 Django
安装Redis.
Mac上使用 Homebrew安装:
$ brew install redis
安装完成后,在一个新的终端窗口启动Redis服务器,并确保它运行在其默认端口=6379。当我们设置Django如何与Redis通信时,端口号是很重要的。
$ redis-server
如果Django 使用Redis作为缓存后端,我首先要安装 django-redis.
在依赖文件requirements.txt 中加入 :
django-redis==4.11.0
安装:
(venv)$ pip install -r requirements.txt
Next,
下面,在settings.py中设置自定义缓存后端:
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}
现在,重启http服务器后,缓存后端将会使用Redis。
(venv)$ python manage.py runserver
打开浏览器,打开 http://127.0.0.1:8000。第一次请求时,加载延迟仍然是大概2秒左右,但是重新刷新页面时,页面几乎在瞬间加载。看一下终端的加载时间,几乎接近于零:
Total time: 0.01s
想知道Redis内部缓存的数据是什么样的吗?
在新的终端窗口中以交互模式运行Redis CLI:
$ redis-cli
正常的话,会显示:
127.0.0.1:6379>
运行ping
命令确保一切正常工作:
127.0.0.1:6379> ping
PONG
在settings.py
中,我们使用Redis数据库编号1:'LOCATION': ' Redis://127.0.0.1:6379/1',
。因此,运行' select 1 '选择该数据库,然后运行' keys'查看所有的键:
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> keys *
1) ":1:views.decorators.cache.cache_header..17abf5259517d604cc9599a00b7385d6.en-us.UTC"
2) ":1:views.decorators.cache.cache_page..GET.17abf5259517d604cc9599a00b7385d6.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC"
我们可以看到Django在缓存中保存了一个header
键和一个cache_page
键。
要查看实际缓存的数据,运行get
命令并以键作为参数:
127.0.0.1:6379[1]> get ":1:views.decorators.cache.cache_page..GET.17abf5259517d604cc9599a00b7385d6.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC"
你会看到类似的内容:
"\x80\x05\x95D\x04\x00\x00\x00\x00\x00\x00\x8c\x18django.template.response\x94\x8c\x10TemplateResponse
\x94\x93\x94)\x81\x94}\x94(\x8c\x05using\x94N\x8c\b_headers\x94}\x94(\x8c\x0ccontent-type\x94\x8c\
x0cContent-Type\x94\x8c\x18text/html; charset=utf-8\x94\x86\x94\x8c\aexpires\x94\x8c\aExpires\x94\x8c\x1d
Fri, 01 May 2020 13:36:59 GMT\x94\x86\x94\x8c\rcache-control\x94\x8c\rCache-Control\x94\x8c\x0
bmax-age=300\x94\x86\x94u\x8c\x11_resource_closers\x94]\x94\x8c\x0e_handler_class\x94N\x8c\acookies
\x94\x8c\x0chttp.cookies\x94\x8c\x0cSimpleCookie\x94\x93\x94)\x81\x94\x8c\x06closed\x94\x89\x8c
\x0e_reason_phrase\x94N\x8c\b_charset\x94N\x8c\n_container\x94]\x94B\xaf\x02\x00\x00
<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <title>Home</title>\n
<link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css\
"\n integrity=\"sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh\"
crossorigin=\"anonymous\">\n\n</head>\n<body>\n<div class=\"container\">\n <div class=\"pt-3\">\n
<h1>Below is the result of the APICall</h1>\n </div>\n <div class=\"pt-3 pb-3\">\n
<a href=\"/\">\n <button type=\"button\" class=\"btn btn-success\">\n
Get new data\n </button>\n </a>\n </div>\n Results received!<br>\n
13:31:59\n</div>\n</body>\n</html>\x94a\x8c\x0c_is_rendered\x94\x88ub."
然后,退出交互式命令行:
127.0.0.1:6379[1]> exit
直接跳到“性能测试”一节。
Option 2: Memcached with Django
首先,在依赖文件requirements.txt中加入 python-memcached:
python-memcached==1.59
安装依赖文件包:
(venv)$ pip install -r requirements.txt
接下来,在 core/settings.py 中修改缓存后端设置,激活 Memcached backend:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': '127.0.0.1:11211',
}
}
在设置中,我使用本地服务器localhost (127.0.0.1) 和端口 11211,这是Memcached的默认配置。
接下来,我们需要安装并运行Memcached守护进程。最简单的方法是通过包管理器像APT, YUM, Homebrew或Chocolatey,这取决于你的操作系统:
# linux
$ apt-get install memcached
$ yum install memcached
# mac
$ brew install memcached
# windows
$ choco install memcached
然后打开另一个终端窗口,在端口11211上运行
$ memcached -p 11211
# test: telnet localhost 11211
有关Memcached安装和配置的更多信息,请查看官方的wiki。
再次在浏览器中导航到http://127.0.0.1:8000。第一个请求仍然需要两秒左右的时间,但是所有后续的请求都会使用缓存。因此,如果刷新或按下“Get new data”按钮,页面应该几乎马上加载。
Total time: 0.03s
性能测试
如果使用Django Debug Toolbar
来对比两次(缓存前后)的加载时间,我们可以看到类型下面的页面:
同时,在Django Debug Toolbar
中我们可以看到缓存后端的活动情况:
重新启动Gunicorn并重新运行性能测试:
$ ab -n 100 -c 10 http://127.0.0.1:8000/
在你的机子上显示结果是多少秒?在我的机器上大约是36 !
结论
在本文中,我们了解了Django内置的用于缓存的不同选项,以及不同级别的缓存。我们还详细介绍了如何使用Django的Memcached和Redis的每个视图缓存来缓存一个视图。
你可以在cache-django-view repo中找到Memcached和Redis作为缓存后端的最终代码。
一般来讲,当由于数据库查询或HTTP调用的网络延迟导致页面呈现缓慢时,我们就需要使用缓存。
这里,我们推荐使用自定义的Django-Redis
作为缓存后端,并使用视图缓存
的方式。如果您需要更细的粒度和控制,由于模板上的数据对于所有用户来说都是不一样的,或者部分数据经常更改,那么可以选择使用模板片段缓存
或底层缓存API
。