惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Threatpost
aimingoo的专栏
aimingoo的专栏
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
T
Tailwind CSS Blog
J
Java Code Geeks
博客园_首页
Google Online Security Blog
Google Online Security Blog
Hugging Face - Blog
Hugging Face - Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
I
Intezer
P
Palo Alto Networks Blog
V
Vulnerabilities – Threatpost
雷峰网
雷峰网
O
OpenAI News
SecWiki News
SecWiki News
小众软件
小众软件
酷 壳 – CoolShell
酷 壳 – CoolShell
美团技术团队
N
News | PayPal Newsroom
Project Zero
Project Zero
Forbes - Security
Forbes - Security
IT之家
IT之家
A
Arctic Wolf
WordPress大学
WordPress大学
Jina AI
Jina AI
T
Tor Project blog
博客园 - 三生石上(FineUI控件)
S
Secure Thoughts
Google DeepMind News
Google DeepMind News
Attack and Defense Labs
Attack and Defense Labs
博客园 - 聂微东
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
P
Privacy International News Feed
Cloudbric
Cloudbric
G
GRAHAM CLULEY
博客园 - 叶小钗
H
Hacker News: Front Page
腾讯CDC
量子位
Help Net Security
Help Net Security
人人都是产品经理
人人都是产品经理
C
Cyber Attacks, Cyber Crime and Cyber Security
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
宝玉的分享
宝玉的分享
爱范儿
爱范儿
L
Lohrmann on Cybersecurity
Hacker News - Newest:
Hacker News - Newest: "LLM"
Recorded Future
Recorded Future
C
CERT Recently Published Vulnerability Notes

追梦人物的博客

0x05:Merkle Tree & Patricia Trie 0x04:ECDSA - 以太坊设计与实现 LeetCode 105 从前序与中序遍历构造二叉树 迭代算法原理 + 完整证明 0x03:Address - 以太坊设计与实现 0x02:Secp256k1 - 以太坊设计与实现 0x00:专栏开篇 - 以太坊设计与实现 0x01:RLP 编码 - 以太坊设计与实现 Bellman-Ford 算法原理及其在 DeFi 套利中的应用 - 追梦人物的博客 迪杰斯特拉(Dijkstra)最短路径算法原理、实现与证明 - 追梦人物的博客 实战:CEX-DEX 稳定币套利监控程序开发 - 追梦人物的博客 CEX-DEX 稳定币套利模型 - 追梦人物的博客 Uniswap 手续费和协议费机制剖析 - 追梦人物的博客 Uniswap 流动性机制及相关数学原理分析 - 追梦人物的博客 uv 替代 pyenv + pipx + poetry 环境管理实践 Rust 项目从创建到发布 - 追梦人物的博客 VS Code 调试 Python - 追梦人物的博客 比特币跨市场套利的数学模型 - 追梦人物的博客 ERC-20 相关知识点总结 - 追梦人物的博客 Django 老项目如何从 SQLite 迁到 PostgreSQL 自动生成接口文档 - HelloDjango - django REST framework 教程 单元测试 - HelloDjango - django REST framework 教程 限制接口访问频率 - HelloDjango - django REST framework 教程 API 版本管理 - HelloDjango - django REST framework 教程 如何在 Windows 下搭建高效的 django 开发环境 拓展Python Markdown - 追梦人物的博客 基于 drf-haystack 实现文章搜索接口 - HelloDjango 评论接口 - HelloDjango - django REST framework 教程 实现分类、标签、归档日期接口 - HelloDjango - django REST framework 教程 在接口返回Markdown解析后的内容 - HelloDjango - django REST framework 教程 文章详情接口 - HelloDjango - django REST framework 教程 分页 - HelloDjango - django REST framework 教程 使用视图集简化代码 - HelloDjango - django REST framework 教程 用类视图实现首页 API - HelloDjango - django REST framework 教程 实现博客首页文章列表 API - HelloDjango - django REST framework 教程 初始化 RESTful API 风格的博客系统 - HelloDjango django-rest-framework 是什么鬼? - HelloDjango - django REST framework 教程 结束 or 开始? - HelloDjango Coverage.py 统计测试覆盖率 - HelloDjango - Django博客教程(第二版) 单元测试:测试评论应用 - HelloDjango - Django博客教程(第二版) 单元测试:测试 blog 应用 - HelloDjango Django Haystack 全文检索与关键词高亮 - HelloDjango Django 博客实现简单的全文搜索 - HelloDjango - Django博客教程(第二版) 开启 Django 博客的 RSS 功能 - HelloDjango 统计各个分类和标签下的文章数 - HelloDjango - Django博客教程(第二版) 稳定易用的 Django 分页库,完善分页功能 - HelloDjango 通过 Django Pagination 实现简单分页 - HelloDjango 在脚本中使用 ORM:Faker 批量生成测试数据 - HelloDjango Django 官方推荐的姿势:类视图 - HelloDjango - Django博客教程(第二版) Django 使用 union 合并不同模型(Model) 的查询集(QuerySet) 开发博客文章阅读量统计功能 - HelloDjango - Django博客教程(第二版) 使用 Docker 让部署 Django 项目更加轻松 - HelloDjango 使用 Certbot 向 Let's Encrypt 免费申请 HTTPS 证书 - HelloDjango 使用 Fabric 自动化部署 - HelloDjango 博客代码开源啦 - 追梦人物的博客 (赠书)推荐一本django书籍:Django企业开发实战 - 追梦人物的博客 Nginx+Gunicorn+Supervisor 部署 Django 博客应用 - HelloDjango 优化博客功能细节,提升使用体验 - HelloDjango - Django博客教程(第二版) 交流的桥梁:评论功能 - HelloDjango - Django博客教程(第二版) 分类、归档和标签页 - HelloDjango - Django博客教程(第二版) 页面侧边栏:使用自定义模板标签 - HelloDjango - Django博客教程(第二版) 自动生成文章摘要 - HelloDjango - Django博客教程(第二版) Markdown 文章自动生成目录,提升阅读体验 - HelloDjango - Django博客教程(第二版) 让博客支持 Markdown 语法和代码高亮 - HelloDjango 开发博客文章详情页 - HelloDjango - Django博客教程(第二版) 创作后台开启,请开始你的表演 - HelloDjango - Django博客教程(第二版) 博客从“裸奔”到“有皮肤” - HelloDjango - Django博客教程(第二版) Django 的接客之道 - HelloDjango - Django博客教程(第二版) Django 迁移、操作数据库 - HelloDjango - Django博客教程(第二版) 创建 Django 博客的数据库模型 - HelloDjango 开始进入 django 开发之旅 - HelloDjango "空空如也"的博客应用 - HelloDjango - Django博客教程(第二版) 一种自顶而下的Python装饰器设计方法 - 追梦人物的博客 Python提取支付宝和微信支付二维码 - 追梦人物的博客 2018 - 我的学生生涯最后一年回顾 - 追梦人物的博客 批量清除todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 筛选练习参考答案 - Vue 2.x Todo 教程练习参考答案 删除todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 编辑todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 添加todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 标为完成练习参考答案 - Vue 2.x Todo 教程练习参考答案 入门仪式_Hello_Vue练习参考答案 - Vue 2.x Todo 教程练习参考答案 组件化todo应用 - Vue 2.x Todo 教程 批量清除todo - Vue 2.x Todo 教程 本地存储 - Vue 2.x Todo 教程 筛选 - Vue 2.x Todo 教程 还剩多少todo未完成 - Vue 2.x Todo 教程 自定义指令实现自动聚焦 - Vue 2.x Todo 教程 全部标为完成 - Vue 2.x Todo 教程 删除todo - Vue 2.x Todo 教程 编辑todo - Vue 2.x Todo 教程 添加todo - Vue 2.x Todo 教程 标为完成 - Vue 2.x Todo 教程 UI - Vue 2.x Todo 教程 显示todo列表 - Vue 2.x Todo 教程 入门仪式:Hello Vue - Vue 2.x Todo 教程 在学习django-rest-framework时收集的学习资料推荐 - 追梦人物的博客 区块链理论与应用研究小组成员招募书 - 追梦人物的博客 Python界网红,豆瓣工程师董伟明加了我的QQ后 - 追梦人物的博客 招募Django学习小组项目组核心成员 - 追梦人物的博客
加缓存为接口提速 - HelloDjango - django REST framework 教程
2020-07-19 · via 追梦人物的博客

为什么要使用缓存

目前,用户对于接口的操作基本都需要查询数据库。获取文章列表需要从数据库查询,获取单篇文章需要从数据库查询,获取评论列表也需要查询数据。但是,对于博客中的很多资源来说,在某个时间段内,他们的内容几乎都不会发生更新。例如文章详情,文章发表后,除非对其内容做了修改,否则内容就不会变化。还有评论列表,如果没人发布新评论,评论列表也不会变化。

要知道查询数据库的操作相对而言是比较缓慢的,而直接从内存中直接读取数据就会快很多,因此缓存系统应运而生。将那些变化不那么频繁的数据缓存到内存中,内存中的数据相当于数据库中的一个副本,用户查询数据时,不从数据库查询而是直接从缓存中读取,数据库的数据发生了变化时再更新缓存,这样,数据查询的性能就大大提升了。

当然数据库性能也没有说的那么不堪,对于大部分访问量不大的个人博客而言,任何关系型数据库都足以应付。但是我们学习 django-rest-framework 不仅仅是为了写博客,也许你在工作中,面对的是流量非常大的系统,这时候缓存就不可或缺。

确定需缓存的接口

先来整理一下我们已有的接口,看看哪些接口是需要缓存的:

接口名 URL 需缓存
文章列表 /api/posts/
文章详情 /api/posts/:id/
分类列表 /categories/
标签列表 /tags/
归档日期列表 /posts/archive/dates/
评论列表 /api/posts/:id/comments/
文章搜索结果 /api/search/

补充说明

  1. 文章列表:需要缓存,但如果有文章修改、新增或者删除时应使缓存失效。
  2. 文章详情:需要缓存,但如果文章内容修改或者删除了应使缓存失效。
  3. 分类、标签、归档日期:可以缓存,但同样要注意在相应的数据变化时使缓存失效。
  4. 评论列表:可以缓存,新增或者删除评论时应使缓存失效。
  5. 搜索接口:因为搜索的关键词是多种多样的,可以缓存常见搜索关键词的搜索结果,但如何确定常见搜索关键词是一个复杂的优化问题,这里我们不做任何缓存处理。

配置缓存

django 为我们提供了一套开箱即用的缓存框架,缓存框架对缓存的操作做了抽象,提供了统一的读写缓存的接口。无论底层使用什么样的缓存服务(例如常用的 Redis、Memcached、文件系统等),对上层应用来说,操作逻辑和调用的接口都是一样的。

配置 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 服务部分。

drf-extensions Cache

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:

  1. 接口调用的视图方法的 id,例如 blog.views. PostViewSet.list。
  2. 客户端请求的接口返回的数据格式,例如 json、xml。
  3. 客户端请求的语言类型。

另外我们还添加了 3 条自定义的缓存 key 的 KeyBit:

  1. 执行数据库查询的 sql 查询语句
  2. 分页请求的查询参数
  3. Post 资源的最新更新时间

以上 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,信号发出后该方法将被调用,往缓存中写入文章资源的更新时间。

整理一下请求被缓存的逻辑:

  1. 请求文章列表接口
  2. 根据 PostListKeyConstructor 生成缓存 key,如果使用这个 key 读取到了缓存结果,就直接返回读取到的结果,否则从数据库查询结果,并把查询的结果写入缓存。
  3. 再次请求文章列表接口,PostListKeyConstructor 将生成同样的缓存 key,这时就可以直接从缓存中读到结果并返回了。

缓存更新的逻辑:

  1. 新增、修改或者删除文章,触发 post_delete, post_save 信号,文章资源的更新时间将被修改。
  2. 再次请求文章列表接口,PostListKeyConstructor 将生成不同的缓存 key,这个新的 key 不在缓存中,因此将从数据库查询最新结果,并把查询的结果写入缓存。
  3. 再次请求文章列表接口,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 服务

本地内存缓存服务配置简单,适合在开发环境使用,但无法适应多线程和多进程适的环境,线上环境我们使用 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

然后就可以将服务发布上线了。

-- EOF --