DJANGO 缓存详解与示例

缓存通常是提高应用程序性能的最有效方法。

对于动态网站,在呈现模板时,通常必须从各种来源(如数据库、文件系统和第三方api等)收集数据,处理数据,并在将数据提供给客户端之前应用业务逻辑。任何由网络延迟引起的延迟都会被用户注意到。

例如,假设您必须对外部API进行HTTP调用,以获取呈现模板所需的数据。即使在完美的条件下,这也会增加渲染时间,从而增加整体的加载时间。还有,如果API服务挂了怎么办?或者API请求速率受限?无论采用哪种方式,如果数据不经常更新,最好实现缓存机制,以避免为每个客户机请求一起进行HTTP调用。

本文将通过首先全面回顾Django的缓存框架,然后逐步详细介绍如何缓存Django视图来了解如何使用缓存。

依赖:

  1. Django v3.0.5
  2. django-redis v.4.11.0
  3. Python v3.8.2
  4. python-memcached v1.59
  5. Requests v2.23.0

目标

在本教程结束时,你应该能够了解并掌握:

  1. 解释为什么需要缓存一个Django视图

  2. 描述Django用于缓存的内置选项

  3. 用Redis缓存一个Django视图

  4. 用Apache Bench负载测试Django应用

  5. 用Memcached缓存Django视图

Django缓存类型

Django有几个内置缓存后台功能,以及支持[自定义后台功能。

内置选项有:

  1. Memcached: Memcached是一个基于内存、键值存储小块数据的存储。它支持跨多个服务器的分布式缓存。

  2. Database:这里的缓存片段存储在数据库中。为了达到这个目的,可以使用Django的一个管理命令创建一个表。这不是性能最好的缓存类型,但它对于存储复杂的数据库查询很有用。

  3. File system:缓存保存在文件系统上,每个缓存值分别保存在不同的文件中。这是所有缓存类型中最慢的一种,但是在生产环境中最容易设置。

  4. 本地内存:本地内存缓存,最适合本地开发或测试环境。虽然它几乎和Memcached一样快,但它不能扩展到单个服务器之外,因此它不适合用于任何使用多个web服务器的应用程序作为数据缓存。

  5. 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,如果一切顺利,你将会看到下面的页面:

uncached webpage

在终端上会显示页面的执行时间,记录这个数值:

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的视图缓存功能来解决这个问题。

流程:

  1. 在初次请求时对httpbin.org进行完整的HTTP调用;

  2. 缓存该视图;

  3. 在后续的请求中将从缓存中提出数据,绕过HTTP调用;

  4. 在一段时间(TTL)之后使缓存失效。

DJANGO压力测试方法

在添加缓存之前,让我们快速运行一个负载测试,使用Apache Bench获得基准测试,以大致了解我们的应用程序每秒可以处理多少请求。

Apache Bench在Mac上是预装的。

如果是Linux系统,可能已经安装好了。如果没有,你可以通过 APT -get install apache2-utilsyum 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.

MemcachedRedis是基于内存的键值型数据存储。它们都易于使用,并针对高性能检索进行了优化。

你可能不会看到两者在性能或内存消耗方面有太大的区别。不过,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来对比两次(缓存前后)的加载时间,我们可以看到类型下面的页面:

load time uncached

load time with cache

同时,在Django Debug Toolbar中我们可以看到缓存后端的活动情况:

cache operations

重新启动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

原文: https://testdriven.io/blog/django-caching/


views:2340