cache,也就是缓存,对于网页应用的快速响应是非常必须的,毕竟没有谁愿意一直等着服务器返回数据。那么缓存在前面学习的MTV模型中处于哪一环节?究竟有多少种缓存方式?又该如何将数据放入缓存呢?这一节我们一起来看看这些问题。
我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。
先总结下我的操作环境:
因为Django长期支持版本2.2LTS和前一个长期支持版本1.11LTS有许多地方不一样,需要小心区分。
离开之前的项目,新建一个DjangoCache项目。
步骤和之前是一样的,这里就不详细演示每一步的操作了:
django-admin startproject DjangoCache
新建项目manage.py
的上一级打开python manage.py startapp App
新建一个应用,并在settings中注册settings.py
,包括数据库连接信息,全局templates文件路径等等__init__.py
中对pymysql进行伪装pymysql.install_as_MySQLdb()
python manage.py migrate
urls.py
并include到项目的urls.py
中,设置好namespacecache就是缓存的意思,这里直接翻译一下官方文档的介绍。
对一个东西进行cache,就是将一个耗时的复杂运算的结果进行保存,这样下次进行同样访问的时候不用再重复进行计算
To cache something is to save the result of an expensive calculation so that you don’t have to perform the calculation next time
官方还给了一段伪代码来说明
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
所以应该就是将view函数返回的HttpResponse对象进行了cache,下次访问同样的view函数,如果cache还没过期就直接返回cache中的内容。如果cache过期了就重新运行一下view函数进行返回。
按照上面的伪代码,我自己画了一个简单的流程图来对比一下加了cache前后的流程变化
没有加cache的时候,流程从①到⑧按照顺序;有了cache以后,当view.py
中的view函数配置了cache,程序直接去cache中进行查找,有则直接⑨到⑩然后到⑦;如果cache已经过期则⑨到⑩再去③重复一遍没cache的流程并⑥以后除了返回结果给⑦,还要把结果放进cache以便下一次查询。
基于Memcached缓存
Memcached是一种内存数据库,所有数据保存在内存中,所以速度非常快,而且Django原生支持。但是因为Redis的走红,Memcached有被逐步取代的趋势。不过因为Redis没有被Django原生支持,所以要额外安装第三方模块再使用。建议使用Redis而不是Memcached。
基于数据库缓存
和普通数据模型差不多,关联一张表专门用来存储cache。因为只是单张表,没有额外关联,所以速度相对会快很多。
基于文件系统缓存
将数据保存在磁盘中,因为磁盘和内存的速率相差几万倍,这种方式速度并不快,所以不做推荐。
基于本地内存缓存
如果系统无法支持Memcached或者Redis这种内存数据库,直接存在本地内存中也是可以。但是这样别的设备就无法共享cache内容了。也不做推荐。
自定义缓存
例如安装第三方的Redis模块来使用。
按照官方文档的说明,Django的cache框架有下面三个设计宗旨,我觉得对于我自己的代码设计也有帮助,特意摘录如下
Less code
A cache should be as fast as possible. Hence, all framework code surrounding the cache backend should be kept to the absolute minimum, especially for get() operations.
Consistency
The cache API should provide a consistent interface across the different cache backends.
Extensibility
The cache API should be extensible at the application level based on the developer’s needs
如果有机会,后面我们再一起看看一些源码来更深入的理解。
虽说Django支持多种cache种类,但是通常以数据库和Redis来实现。这里分别来看看如何在Django中对这两种方式配置存储位置信息
在settings.py
中模仿DATABASES
的方式添加CACHES
,如下
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
'LOCATION': 'my_cache_table',
}
}
这里的LOCATION
是数据库中的表名,下一步中进行创建。
跑一下下面的命令创建cache表
python manage.py createcachetable [table_name]
如果table_name
省略的话会自动从settings.py
中的配置来寻找。
可以通过加--dry-run
来查看DDL
(django) [fuhx@testmachine DjangoCache]$ python manage.py createcachetable my_table --dry-run
CREATE TABLE `my_cache_table` (
`cache_key` varchar(255) NOT NULL PRIMARY KEY,
`value` longtext NOT NULL,
`expires` datetime(6) NOT NULL
);
CREATE INDEX `my_cache_table_expires` ON `my_cache_table` (`expires`);
可以看到多了一个表my_cache_table
,表结构和前面看到的django_session
表非常像
因为Redis不是Django原生的,所以首先要安装第三方库,一共有两个:
可以通过pip install
来安装,或者是在设置中的解释器图形界面安装,如下
在本地起一个redis实例以供使用。可以参考我的Redis系列专栏中的《Redis从入门到精通(1):centos7安装和启动redis》 进行安装和启动,如果想使用docker也可以参考另一篇《Redis从入门到精通(4):docker运行redis容器详解》
点击这里进入我写的Redis系列专栏
我这里是在本地的6780端口起了一个实例,密码是xiaofu,使用第1号库来存储cache,如下
[fuhx@testmachine bin]$ ./redis-cli -p 6380 -a xiaofu
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6380> select 1
OK
127.0.0.1:6380[1]> keys *
(empty list or set)
127.0.0.1:6380[1]>
我的redis版本是5.0.7
[fuhx@testmachine bin]$ ./redis-server --version
Redis server v=5.0.7 sha=00000000:0 malloc=libc bits=64 build=87b518f2fdf327d
一些操作方法可以参考django-redis
的官方文档。
在settings.py
的CACHES
中再添加一个配置my_redis_cache
,如下
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
'LOCATION': 'my_cache_table',
},
"my_redis_cache": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6380/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"PASSWORD": "xiaofu"
}
}
}
其中LOCATION
配置项的最后那个1
表示Redis中的第1号DB
配置好了两个待选的存储路径,就可以开始实际使用看看了。
Django帮我们封装了一个非常好用的装饰器cache_page
,只需要将准备加入cache的view函数使用该装饰器即可。该装饰器只需要一个参数就可以使用,就是以秒为单位的超时时间。同时还可以加入cache=xxx
的关键字参数来指定settings.py
中配置好的存储位置。
创建路由和view函数如下
path('test_cache/',views.test_cache,name='test_cache'),
path('test_cache_target/',views.test_cache_target,name='test_cache_target'),
def test_cache(request):
return HttpResponseRedirect(reverse('app:test_cache_target'))
def test_cache_target(request):
time.sleep(5)
return HttpResponse('5 seconds have passed by')
访问/app/test_cache/
的时候会跳转到/app/test_cache_target/
,这里模拟一个耗时计算,睡眠5秒,然后返回。
此时访问http://127.0.0.1:8000/app/test_cache/
效果如下
可以看到左上角转圈了5秒才显示目标页面的内容。
然后修改view函数如下
def test_cache(request):
return HttpResponseRedirect(reverse('app:test_cache_target'))
@cache_page(60 * 5, cache='default')
def test_cache_target(request):
time.sleep(5)
return HttpResponse('5 seconds have passed by')
加了一个装饰器,并且配置过期时间为5分钟,使用名为default
的cache配置,也就是数据库cache。之后再访问http://127.0.0.1:8000/app/test_cache/
,从第二次开始的效果如下
可以看到按下回车的一瞬间就显示了目标页面的内容。要从第二次开始,是因为第一次访问之后会将结果存储到数据库中,下一次生效。
查看数据库中的数据,发现my_cache_table
多了两条记录,过期时间是在约5分钟后
如果过期时间和本机时间对不上,修改一下settings.py
中的TIME_ZONE
到Asia/Shanghai
来解决。
休息5分钟后再过来尝试访问,又会出现转圈的现象,之后my_cache_table
中的过期时间往后推了5分钟
下次访问就又可以飞速跳转了。
修改上面的view函数如下
@cache_page(60 * 5, cache='my_redis_cache')
def test_cache_target(request):
time.sleep(5)
return HttpResponse('5 seconds have passed by')
只是将数据库cache换成了Redis。之后的访问表现和上面一样,在第一次转圈以后就可以飞速跳转,并且Redis中也多出来了两个key,内容都被加码了
[fuhx@testmachine bin]$ ./redis-cli -p 6380 -a xiaofu
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6380> select 1
OK
127.0.0.1:6380[1]> keys *
(empty list or set)
127.0.0.1:6380[1]> keys *
1) ":1:views.decorators.cache.cache_header..752bc302bf33f4d1293e77001ab27be9.en-us"
2) ":1:views.decorators.cache.cache_page..GET.752bc302bf33f4d1293e77001ab27be9.d41d8cd98f00b204e9800998ecf8427e.en-us"
127.0.0.1:6380[1]> get :1:views.decorators.cache.cache_page..GET.752bc302bf33f4d1293e77001ab27be9.d41d8cd98f00b204e9800998ecf8427e.en-us
"\x80\x04\x95\x89\x01\x00\x00\x00\x00\x00\x00\x8c\x14django.http.response\x94\x8c\x0cHttpResponse\x94\x93\x94)\x81\x94}\x94(\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\x1dWed, 08 Apr 2020 08:22:09 GMT\x94\x86\x94\x8c\rcache-control\x94\x8c\rCache-Control\x94\x8c\x0bmax-age=300\x94\x86\x94u\x8c\x11_closable_objects\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]\x94C\x185 seconds have passed by\x94aub."
127.0.0.1:6380[1]>
通过ttl可以查看两个key的过期时间,单位是秒,如果返回-2表示已经过期
127.0.0.1:6380[1]> ttl :1:views.decorators.cache.cache_page..GET.752bc302bf33f4d1293e77001ab27be9.d41d8cd98f00b204e9800998ecf8427e.en-us
(integer) 87
127.0.0.1:6380[1]> ttl :1:views.decorators.cache.cache_page..GET.752bc302bf33f4d1293e77001ab27be9.d41d8cd98f00b204e9800998ecf8427e.en-us
(integer) 86
127.0.0.1:6380[1]> ttl :1:views.decorators.cache.cache_page..GET.752bc302bf33f4d1293e77001ab27be9.d41d8cd98f00b204e9800998ecf8427e.en-us
(integer) 84
127.0.0.1:6380[1]> ttl :1:views.decorators.cache.cache_page..GET.752bc302bf33f4d1293e77001ab27be9.d41d8cd98f00b204e9800998ecf8427e.en-us
(integer) -2
上面这种对view函数加装饰器的方式对于静态页面还是很方便的,但是其弱点也很明显,就是对于动态页面会显得非常不灵活。最好是能够将一个页面中不经常变化以及计算很耗时的数据存到cache中,这个时候就需要利用到底层API的操作了。
首先通过from django.core.cache import caches
之后利用caches['xxx']
来获取一个cache对象。这其中的xxx
就是在settings.py
中配置的存储位置的名字。然后就可以用这个cache对象来进行API操作了。
几个常用的API操作如下:
cache.set(key, value, timeout)
设置一个cache的键值对,key是一个string类型,而value可以是python支持的数据类型,例如字典。timeout的单位是秒,如果赋值None表示永不过期,而赋值0则不会存储这个键值对
cache.get(key, default=None)
尝试去获取一个key,如果已经过期则返回None。或者赋值给default一个当key不存在时候的返回值
cache.delete(key)
删除一个key
还有一些操作可以查看官方文档
修改上面的view函数如下
def test_cache(request):
return HttpResponseRedirect(reverse('app:test_cache_target'))
# @cache_page(60 * 5, cache='my_redis_cache')
def test_cache_target(request):
rdm = random.randint(1,100)
# cache = caches['my_redis_cache']
cache = caches['default']
cached = cache.get('giant_data')
if cached == None:
time.sleep(5)
cache.set('giant_data','this is a giant data',60*5)
return HttpResponse('the result after calculation is {}'.format(str(rdm)))
每次访问会生成一个随机的0-100的整数,这个数字因为变化太快所以不用放进cache。 然后这里给cache实例添加了一个叫giant_data
的key,过期时间是5分钟,模拟一个复杂运算以及不经常变动的内容。
第一次访问在停顿了5秒后在数据库的my_cache_table
种多了一条记录
以后再次访问就没有停顿了,
如果要改为Redis也非常简单,只需要修改cache = caches['my_redis_cache']
即可,如下
def test_cache(request):
return HttpResponseRedirect(reverse('app:test_cache_target'))
# @cache_page(60 * 5, cache='my_redis_cache')
def test_cache_target(request):
rdm = random.randint(1,100)
cache = caches['my_redis_cache']
# cache = caches['default']
cached = cache.get('giant_data')
if cached == None:
time.sleep(5)
cache.set('giant_data','this is a giant data',60*5)
return HttpResponse('the result after calculation is {}'.format(str(rdm)))
之后的效果和之前一样,不过键值对存储在了Redis中如下
127.0.0.1:6380[1]> keys *
1) ":1:giant_data"
127.0.0.1:6380[1]> get :1:giant_data
"\x80\x04\x95\x18\x00\x00\x00\x00\x00\x00\x00\x8c\x14this is a giant data\x94."
127.0.0.1:6380[1]> ttl :1:giant_data
(integer) 270
127.0.0.1:6380[1]>
这一篇我们熟悉了多种实现cache的方法,如果要说哪一种最好,我个人最推荐Redis配合底层API的方式。生产环境的Redis通常是一个单独的集群,不仅速度快,而且便于共享,同时也也可以避免单点问题。