做者:HelloGitHub——追梦人物web
目前,用户对于接口的操做基本都须要查询数据库。获取文章列表须要从数据库查询,获取单篇文章须要从数据库查询,获取评论列表也须要查询数据。可是,对于博客中的不少资源来讲,在某个时间段内,他们的内容几乎都不会发生更新。例如文章详情,文章发表后,除非对其内容作了修改,不然内容就不会变化。还有评论列表,若是没人发布新评论,评论列表也不会变化。redis
要知道查询数据库的操做相对而言是比较缓慢的,而直接从内存中直接读取数据就会快不少,所以缓存系统应运而生。将那些变化不那么频繁的数据缓存到内存中,内存中的数据至关于数据库中的一个副本,用户查询数据时,不从数据库查询而是直接从缓存中读取,数据库的数据发生了变化时再更新缓存,这样,数据查询的性能就大大提高了。sql
固然数据库性能也没有说的那么不堪,对于大部分访问量不大的我的博客而言,任何关系型数据库都足以应付。可是咱们学习 django-rest-framework 不只仅是为了写博客,也许你在工做中,面对的是流量很是大的系统,这时候缓存就不可或缺。数据库
先来整理一下咱们已有的接口,看看哪些接口是须要缓存的:django
接口名 | URL | 需缓存 |
---|---|---|
文章列表 | /api/posts/ | 是 |
文章详情 | /api/posts/:id/ | 是 |
分类列表 | /categories/ | 是 |
标签列表 | /tags/ | 是 |
归档日期列表 | /posts/archive/dates/ | 是 |
评论列表 | /api/posts/:id/comments/ | 是 |
文章搜索结果 | /api/search/ | 否 |
补充说明json
django 为咱们提供了一套开箱即用的缓存框架,缓存框架对缓存的操做作了抽象,提供了统一的读写缓存的接口。不管底层使用什么样的缓存服务(例如经常使用的 Redis、Memcached、文件系统等),对上层应用来讲,操做逻辑和调用的接口都是同样的。api
配置 django 缓存,最重要的就是选择一个缓存服务,即缓存结果存储和读取的地方。本项目中咱们决定开发环境使用本地内存(Local Memory)缓存服务,线上环境使用 Redis 缓存。缓存
在开发环境的配置文件 settings/local.py 中加入如下的配置项即开启本地内存缓存服务。多线程
CACHES = {
'default': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', } } 复制代码
线上环境使用到 Redis 缓存服务,django 并未内置 Redis 缓存服务的支持,不过对于 Redis 来讲固然不缺少第三方库的支持,咱们选择 django-redis-cache,先来安装它:框架
$ pipenv install django-redis-cache
复制代码
而后在项目的线上环境配置文件 settings/production.py 中加入如下配置:
CACHES = {
"default": { "BACKEND": "redis_cache.RedisCache", "LOCATION": "redis://:UJaoRZlNrH40BDaWU6fi@redis:6379/0", "OPTIONS": { "CONNECTION_POOL_CLASS": "redis.BlockingConnectionPool", "CONNECTION_POOL_CLASS_KWARGS": {"max_connections": 50, "timeout": 20}, "MAX_CONNECTIONS": 1000, "PICKLE_VERSION": -1, }, }, } 复制代码
这样,django 的缓存功能就启用了。至于如何启动 Redis 服务,请参考教程最后的 Redis 服务部分。
django 的缓存框架比较底层,drf-extensions 在 django 缓存框架的基础上,针对 django-rest-framework 封装了更多缓存相关的辅助函数和类,咱们将借助这个第三方库来大大简化缓存逻辑的实现。
首先安装它:
$ pipenv install drf-extensions
复制代码
那么 drf-extensions 对缓存提供了哪些辅助函数和类呢?咱们须要用到的主要有这些:
KeyConstructor
能够理解为缓存键生成类。咱们先来看看 API 接口缓存的逻辑,伪代码是这样的:
给定一个 URL, 尝试从缓存中查找这个 URL 接口的响应结果
if 结果在缓存中: return 缓存中的结果 else: 生成响应结果 将响应结果存入缓存 (以便下一次查询) return 生成的响应结果 复制代码
缓存结果是以 key-value 的键值对形式存储的,这里关键的地方在于存储或者查询缓存结果时,须要生成相应的 key。例如咱们能够把 API 请求的 URL 做为缓存的 key,这样同一个接口请求将返回相同的缓存内容。可是在更为复杂的场景下,不能简单使用 URL 做为 key,好比即便是同一个 API 请求,已认证和未认证的用户调用接口获得的结果是不同的,因此 drf-extensions 使用 KeyConstructor 辅助基类来提供灵活的 key 生成方式。
KeyBit
能够理解为 KeyConstructor 定义的 key 生成规则中的某一项规则定义。例如,同一个 API 请求,已认证和未认证的用户将获得不一样的响应结果,咱们能够定义 key 的生成规则为请求的 URL + 用户的认证 id。那么 URL 能够当作一个 KeyBit,用户 id 是另外一个 KeyBit。
cache_response 装饰器
这个装饰器用来装饰 django-rest-framework 的视图(单个视图函数、视图集中的 action 等),被装饰的视图将具有缓存功能。
咱们首先来使用 cache_response 装饰器缓存文章列表接口,代码以下:
blog/views.py
from rest_framework_extensions.cache.decorators import cache_response class PostViewSet( mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet ): # ... @cache_response(timeout=5 * 60, key_func=PostListKeyConstructor()) def list(self, request, *args, **kwargs): return super().list(request, *args, **kwargs) @cache_response(timeout=5 * 60, key_func=PostObjectKeyConstructor()) def retrieve(self, request, *args, **kwargs): return super().retrieve(request, *args, **kwargs) 复制代码
这里咱们分别装饰了 list(获取文章列表的 action)和 retrieve(获取单篇文章),timeout
参数用于指定缓存失效时间, key_func
指定缓存 key 的生成类(即 KeyConstructor),固然 PostListKeyConstructor
、和 PostObjectKeyConstructor
还未定义,接下来咱们就来定义这两个缓存 key 生成类:
blog/views.py
from rest_framework_extensions.key_constructor.bits import ( ListSqlQueryKeyBit, PaginationKeyBit, RetrieveSqlQueryKeyBit, ) from rest_framework_extensions.key_constructor.constructors import DefaultKeyConstructor class PostListKeyConstructor(DefaultKeyConstructor): list_sql = ListSqlQueryKeyBit() pagination = PaginationKeyBit() updated_at = PostUpdatedAtKeyBit() class PostObjectKeyConstructor(DefaultKeyConstructor): retrieve_sql = RetrieveSqlQueryKeyBit() updated_at = PostUpdatedAtKeyBit() 复制代码
PostListKeyConstructor
用于文章列表接口缓存 key 的生成,它继承自 DefaultKeyConstructor
,这个基类中定义了 3 条缓存 key 的 KeyBit:
另外咱们还添加了 3 条自定义的缓存 key 的 KeyBit:
以上 6 条分别对应一个 KeyBit,KeyBit 将提供生成缓存键所须要的值,若是任何一个 KeyBit 提供的值发生了变化,生成的缓存 key 就会不一样,查询到的缓存结果也就不同,这个方式为咱们提供了一种有效的缓存失效机制。例如 PostUpdatedAtKeyBit
是咱们自定义的一个 KeyBit,它提供 Post 资源最近一次的更新时间,若是资源发生了更新,返回的值就会发生变化,生成的缓存 key 就会不一样,从而不会让接口读到旧的缓存值。PostUpdatedAtKeyBit
的代码以下:
blog/views.py
from .utils import UpdatedAtKeyBit class PostUpdatedAtKeyBit(UpdatedAtKeyBit): key = "post_updated_at" 复制代码
由于资源更新时间的 KeyBit 是比较通用的(后面咱们还会用于评论资源),因此咱们定义了一个基类 UpdatedAtKeyBit
,代码以下:
blog/utils.py
from datetime import datetime from django.core.cache import cache from rest_framework_extensions.key_constructor.bits import KeyBitBase class UpdatedAtKeyBit(KeyBitBase): key = "updated_at" def get_data(self, **kwargs): value = cache.get(self.key, None) if not value: value = datetime.utcnow() cache.set(self.key, value=value) return str(value) 复制代码
get_data
方法返回这个 KeyBit 对应的值,UpdatedAtKeyBit
首先根据设置的 key 从缓存中读取资源最近更新的时间,若是读不到就将资源最近更新的时间设为当前时间,而后返回这个时间。
固然,咱们须要自动维护缓存中记录的资源更新时间,这能够经过 django 的 signal 来完成:
blog/models.py
from django.db.models.signals import post_delete, post_save def change_post_updated_at(sender=None, instance=None, *args, **kwargs): cache.set("post_updated_at", datetime.utcnow()) post_save.connect(receiver=change_post_updated_at, sender=Post) post_delete.connect(receiver=change_post_updated_at, sender=Post) 复制代码
每当有文章(Post)被新增、修改或者删除时,django 会发出 post_save 或者 post_delete 信号,post_save.connect 和 post_delete.connect 设置了这两个信号的接收器为 change_post_updated_at,信号发出后该方法将被调用,往缓存中写入文章资源的更新时间。
整理一下请求被缓存的逻辑:
PostListKeyConstructor
生成缓存 key,若是使用这个 key 读取到了缓存结果,就直接返回读取到的结果,不然从数据库查询结果,并把查询的结果写入缓存。
PostListKeyConstructor
将生成一样的缓存 key,这时就能够直接从缓存中读到结果并返回了。
缓存更新的逻辑:
post_delete
,
post_save
信号,文章资源的更新时间将被修改。
PostListKeyConstructor
将生成不一样的缓存 key,这个新的 key 不在缓存中,所以将从数据库查询最新结果,并把查询的结果写入缓存。
PostListKeyConstructor
将生成一样的缓存 key,这时就能够直接从缓存中读到结果并返回了。
PostObjectKeyConstructor
用于文章详情接口缓存 key 的生成,逻辑和 PostListKeyConstructor
是彻底同样。
有了文章列表的缓存,评论列表的缓存只须要依葫芦画瓢。
KeyBit 定义:
blog/views.py
class CommentUpdatedAtKeyBit(UpdatedAtKeyBit): key = "comment_updated_at" 复制代码
KeyConstructor 定义:
blog/views.py
class CommentListKeyConstructor(DefaultKeyConstructor): list_sql = ListSqlQueryKeyBit() pagination = PaginationKeyBit() updated_at = CommentUpdatedAtKeyBit() 复制代码
视图集:
@cache_response(timeout=5 * 60, key_func=CommentListKeyConstructor())
@action( methods=["GET"], detail=True, url_path="comments", url_name="comment", pagination_class=LimitOffsetPagination, serializer_class=CommentSerializer, ) def list_comments(self, request, *args, **kwargs): # ... 复制代码
其它接口的缓存你们能够根据上述介绍的方法来完成,就留做练习了。
本地内存缓存服务配置简单,适合在开发环境使用,但没法适应多线程和多进程适的环境,线上环境咱们使用 Redis 作缓存。有了 Docker,启动一个 Redis 服务就是一件很是简单的事。
在线上环境的容器编排文件 production.yml 中加入一个 Redis 服务:
version: '3'
volumes: static: database: esdata: redis_data: services: hellodjango.rest.framework.tutorial: ... depends_on: - elasticsearch - redis redis: image: 'bitnami/redis:5.0' container_name: hellodjango_rest_framework_tutorial_redis ports: - '6379:6379' volumes: - 'redis_data:/bitnami/redis/data' env_file: - .envs/.production 复制代码
而后在 .envs/.production 文件中添加以下的环境变量,这个值将做为 redis 链接的密码:
REDIS_PASSWORD=055EDy65AAhLgBxMp1u1
复制代码
而后就能够将服务发布上线了。