【Django 021】Django2.2利用装饰器和cache底层API两种方式实现Redis缓存原理和操作详解

cache,也就是缓存,对于网页应用的快速响应是非常必须的,毕竟没有谁愿意一直等着服务器返回数据。那么缓存在前面学习的MTV模型中处于哪一环节?究竟有多少种缓存方式?又该如何将数据放入缓存呢?这一节我们一起来看看这些问题。

我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。

操作环境

先总结下我的操作环境:

  • Centos 7
  • Python 3.7
  • Pycharm 2019.3
  • Django 2.2

因为Django长期支持版本2.2LTS和前一个长期支持版本1.11LTS有许多地方不一样,需要小心区分。

环境准备

离开之前的项目,新建一个DjangoCache项目。

步骤和之前是一样的,这里就不详细演示每一步的操作了:

  1. virtualenv创建虚拟环境,这里可以直接用之前的虚拟环境。记得activate环境
  2. django-admin startproject DjangoCache新建项目
  3. pycharm打开项目,注意是在manage.py的上一级打开
  4. 配置解释器为虚拟环境内的python,前一个项目的第三方包都会自动识别
  5. python manage.py startapp App新建一个应用,并在settings中注册
  6. mysql创好数据库DjangoCache,每次都用新的库,不然会有问题
  7. 修改配置文件settings.py,包括数据库连接信息,全局templates文件路径等等
  8. 全局__init__.py中对pymysql进行伪装pymysql.install_as_MySQLdb()
  9. 默认数据迁移python manage.py migrate
  10. App应用新建urls.py并include到项目的urls.py中,设置好namespace

cache的原理

cache就是缓存的意思,这里直接翻译一下官方文档的介绍。

对一个东西进行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函数进行返回。

Django中的cache

流程图

按照上面的伪代码,我自己画了一个简单的流程图来对比一下加了cache前后的流程变化
1-Django_cache.jpg
没有加cache的时候,流程从①到⑧按照顺序;有了cache以后,当view.py中的view函数配置了cache,程序直接去cache中进行查找,有则直接⑨到⑩然后到⑦;如果cache已经过期则⑨到⑩再去③重复一遍没cache的流程并⑥以后除了返回结果给⑦,还要把结果放进cache以便下一次查询。

Django内置的cache种类

  • 基于Memcached缓存
    Memcached是一种内存数据库,所有数据保存在内存中,所以速度非常快,而且Django原生支持。但是因为Redis的走红,Memcached有被逐步取代的趋势。不过因为Redis没有被Django原生支持,所以要额外安装第三方模块再使用。建议使用Redis而不是Memcached。

  • 基于数据库缓存
    和普通数据模型差不多,关联一张表专门用来存储cache。因为只是单张表,没有额外关联,所以速度相对会快很多。

  • 基于文件系统缓存
    将数据保存在磁盘中,因为磁盘和内存的速率相差几万倍,这种方式速度并不快,所以不做推荐

  • 基于本地内存缓存
    如果系统无法支持Memcached或者Redis这种内存数据库,直接存在本地内存中也是可以。但是这样别的设备就无法共享cache内容了。也不做推荐

  • 自定义缓存
    例如安装第三方的Redis模块来使用。

Django中cache设计宗旨

按照官方文档的说明,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.

  • API的一致性,不管后面使用什么cache实现,调用的API都要统一。类似于ORM

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

如果有机会,后面我们再一起看看一些源码来更深入的理解。

添加cache配置信息

虽说Django支持多种cache种类,但是通常以数据库和Redis来实现。这里分别来看看如何在Django中对这两种方式配置存储位置信息

基于数据库的cache

  1. 配置cache数据库信息

settings.py中模仿DATABASES的方式添加CACHES,如下

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
        'LOCATION': 'my_cache_table',
    }
}

这里的LOCATION是数据库中的表名,下一步中进行创建。

  1. 创建cache表

跑一下下面的命令创建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表非常像
2-table.png

基于Redis的cache

  1. 安装第三方库

因为Redis不是Django原生的,所以首先要安装第三方库,一共有两个:

  • django-redis
  • django-redis-cache

可以通过pip install来安装,或者是在设置中的解释器图形界面安装,如下
3-redis-module.png

  1. 运行Redis

在本地起一个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
  1. 配置cache数据库信息

一些操作方法可以参考django-redis官方文档

settings.pyCACHES中再添加一个配置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/效果如下
4-before-cache.gif
可以看到左上角转圈了5秒才显示目标页面的内容

数据库cache

然后修改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/从第二次开始的效果如下
5-after-cache.gif
可以看到按下回车的一瞬间就显示了目标页面的内容。要从第二次开始,是因为第一次访问之后会将结果存储到数据库中,下一次生效。

查看数据库中的数据,发现my_cache_table多了两条记录,过期时间是在约5分钟后
6-database-cache.png
如果过期时间和本机时间对不上,修改一下settings.py中的TIME_ZONEAsia/Shanghai来解决。

休息5分钟后再过来尝试访问,又会出现转圈的现象,之后my_cache_table中的过期时间往后推了5分钟
7-update-expire-time.png
下次访问就又可以飞速跳转了。

Redis

修改上面的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

基于底层API操作的实现

上面这种对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

还有一些操作可以查看官方文档

数据库cache

修改上面的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种多了一条记录
8-api-database.png
以后再次访问就没有停顿了,

Redis

如果要改为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通常是一个单独的集群,不仅速度快,而且便于共享,同时也也可以避免单点问题。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值