本文介绍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中使用缓存的方法。下一篇开始介绍项目的部署。