掌握Django(十八)–缓存

本文介绍Django的缓存。


什么是缓存

前面我们简单介绍过缓存,当进行复杂查询时,我们可以将结果放在内存中,后续的请求直接从内存中去取,大多数情况(并非所有)下能提升访问速度。

缓存可以让数据库空出能力更好地服务于其他请求,听起来好像不错,但如果数据库数据变了,内存中的数据未变,数据将是老旧的,一直要等到缓存过期才会更新。我们可以根据数据的变化频繁程度设置缓存过期时间如5分钟、3小时等。如果数据变化很频繁,缓存也就失去了意义。

当用户访问一个API很慢甚至不能响应,缓存帮助非常大,我们可以设置celery 后台任务更新缓存。但是好事情用得太多就变坏了,不能想当然地认为从内存取数总比从磁盘读数要快得多,比如我们将全部数据库缓存到内存就不是一个好主意,那是会占用过多内存影响处理任务的能力。

在使用缓存前一定要做相关性能测试,确有必要才采取。老话说得好:

过早的优化是邪恶的根源


缓存后端

有各种缓存后端或者说引擎。

  • 本地内存(默认)。它与我们的项目在同一个处理进程,适合开发过程,在生产环境中不是很合适。在生产环境中应使用合适的缓存服务器。
  • Memcached。生产环境使用。
  • Redis。生产环境适合,由于之前我们已使用Redis作为消息中介器,这里就选它,避免再安装依赖。
  • 数据库。如果你有一个非常复杂的查询涉及多张表,你可以将查询结果存到数据库的某个表里,这样下次可以直接取出使用。数据库取数可能没有从内存取数快,但也是一种选项。
  • 文件系统。将结果缓存在文件中,这不是常见选项,但在Django某些时候你可以使用。

以上这些缓存后端除了Redis,其它都已默认安装。


模拟一个慢速API

我们修改/playground/views.py,修改say_hello,访问一个模拟慢速的网站 https://httpbin.org/delay/2,2表示是模拟2秒的延迟。

...
import requests
​def say_hello(request):
    requests.get('https://httpbin.org/delay/2')
    return render(request, 'hello.html', {'name': 'kelemi'})

我们接着访问:http://127.0.0.1:8000/playground/hello

发现需要大概2秒后才有响应,这样我们就模拟了慢速API。


设置并获得一个性能基准

在 locustfiles/browse_products.py 添加一个测试 say_hello,测试的内容就是简单地访问 /playground/hello/

    ...
    @task
    def say_hello(self):
        self.client.get('/playground/hello/')
    ...

然后我们运行 locust,并输入 300用户,每秒10个用户的测试压力。我们在大约总计4500个请求时保存一下基准。我们看到 /playground/hello/响应非常慢。等到我们实现缓存后,再测试同样的压力作下比对。


安装Redis

之前我们就说过,安装Redis最简单的方式就是使用Docker。

sudo docker run -d -p 6379:6379 redis

配置缓存

为了在Django中使用Redis,需要安装一个库 django-redis

pipenv install django-redis

查看github。

https://github.com/jazzband/django-redis

我们知道需要在 settings中设置CACHES。其中的redis数据库,我们选择了2,因为之前celery 已使用了1。

CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/2",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        }
    }}

这样我们就完成缓存的设置。


使用低级别的缓存API

修改 playground/views.py。say_hello使用了缓存技术。首先查看缓存中是否存在设定的键值,如果没有,就访问网站去获取数据。如果已存在,我们就直接获取缓存的值返回给用户。

from django.core.cache import cache
from django.shortcuts import render
import requests​
def say_hello(request):
    key = 'httpbin_result'
    if cache.get(key) is None:
        response = requests.get('https://httpbin.org/delay/2')
        data = response.json()
        cache.set(key, data)
    return render(request, 'hello.html', {'name': cache.get(key)})

再访问 /playground/hello/,第一次有点慢,因为需要访问httpbin网站,而再刷新时就很快了,因为已从缓存读取了。

你可能好奇缓存中的数据多长时间是有效的。默认是5分钟或者说是300秒,我们可以修改它,在cache.set方法中提供第3个参数即可,比如10分钟就设置10*60.

cache.set(key, data, 10*60)

我们也可以全局设定缓存数据的有效时间,在settings.py中的CACHES设置TIMEOUT键即可。

CACHES = {
    "default": {
        ...
        "TIMEOUT": 10*60,
        ...
    }
}

使用低级别缓存API,我们在每个需要使用缓存的地方,都用get、set方法精确指定缓存的内容,显得很乏味而且有些重复,下一节介绍简单的方法。


缓存视图

Django有一种更好的缓存方法以代替重复的设置工作,使用装饰符@cache_page来标记。

装饰符@cache_pare(5*60) 括号里的数字代表缓存有效期,这里是5分钟。在say_hello中,我们去掉之前控制缓存的代码,只是获取httpbin的数据,然后在网页中呈现。看上去就跟没有缓存的代码是一样的,但它是如何控制缓存呢?

有了@cache_page装饰符,django会检查有无 ‘say_hello’键值的缓存,当然实际的名称django是自动生成的,首次肯定没有,django会执行views,然后将结果存于缓存。下次再执行时,自动从缓存取出返回。之前低级别的缓存读写代码不用再写,django自动搞定!

from django.views.decorators.cache import cache_page
...​
@cache_page(5*60)
def say_hello(request):
    response = requests.get('https://httpbin.org/delay/2')
    data = response.json()
    return render(request, 'hello.html', {'name': data})

我们访问API端点测试一下, /playground/hello/,第一次比较慢,然后再访问就很快了,缓存起作用了!

但有一个事我们需要了解,我们修改一下代码,将传给网页的data改成’kelemi’,如以下这样:

...
return render(request, 'hello.html', {'name': 'kelemi'})

刷新网页,发现还是之前的内容,而没有用 kelemi 代替,原因是这时的结果还是从缓存里获取,直到缓存过期才会更新。有些时候,我们可能希望立即看到最新的修改结果,那该如何做呢?下一节会介绍到。

装饰符@cache_page只用于基于函数的视图,不能用于基于类的视图。我们要用到另外的装饰符。

先将视图改造成基于类的视图:创建HelloView类并基于APIView,然后使用get方法将之前的view包装起来。

我们不能直接@cache_page标记,而是使用@method_decorator将之包起来。

...
from django.utils.decorators import method_decorator
from rest_framework.views import APIView​​
class HelloView(APIView):
    @method_decorator(cache_page(5*60))
    def get(self, request):
        response = requests.get('https://httpbin.org/delay/2')
        data = response.json()
        return render(request, 'hello.html', {'name': 'kelemi'})

再修改urls路由,playground/urls.py:

urlpatterns = [
    path('hello/', views.HelloView.as_view())
]

现进行测试,基于类的视图的缓存也生效了。


验证优化

为验证缓存的效果,我们重新运行一下locust。

locust -f locustfiles/browse_products.py

然后设同样的用户数300,速率也是10用户每秒,大约总发送请求数也是4500左右。我们看到性能已有巨大的提升了。


管理Redis缓存的内容

我们之前用Docker创建了Redis,Docker类似一个轻量的虚拟机,用docker ps 可以看到容器的id:

docker ps 

如果我们要在具体的容器里执行命令,可以使用exec命令带 -it 参数,然后跟着容器id ,再接着要执行的命令:

docker exec -it <Redis容器的id> redis-cli

上面命令,我们执行了redis-cli用于查看Redis的内容,注意Docker容器的ID只需输入前几个字符足以识别即可。

进入Redis提示符后,选择数据库 2,其中1是之前的消息中介数据库。Redis数据库没有名字,只用数字表示。

select 2

然后我们查询所有键值:

keys *

我们看到相关key,这是由Django自动生成的,示例如下:

1) ":1:views.decorators.cache.cache_page..GET.e58e888c06a2cff7ef17b423ce360397.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC"
2) ":1:views.decorators.cache.cache_header..e58e888c06a2cff7ef17b423ce360397.en-us.UTC"

我们可以用del 命令 单独删除某个键,del <键>

127.0.0.1:6379[2]> del :1:views.decorators.cache.cache_page..GET.e58e888c06a2cff7ef17b423ce360397.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC 

也可以清除全部键

127.0.0.1:6379[2]> flushall

小结

本文介绍了Django中使用缓存的方法。下一篇开始介绍项目的部署。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注