目前,用戶對於接口的操作基本都需要查詢數據庫。獲取文章列表需要從數據庫查詢,獲取單篇文章需要從數據庫查詢,獲取評論列表也需要查詢數據。但是,對於博客中的很多資源來說,在某個時間段內,他們的內容幾乎都不會發生更新。例如文章詳情,文章發表后,除非對其內容做了修改,否則內容就不會變化。還有評論列表,如果沒人發布新評論,評論列表也不會變化。
要知道查詢數據庫的操作相對而言是比較緩慢的,而直接從內存中直接讀取數據就會快很多,因此緩存系統應運而生。將那些變化不那么頻繁的數據緩存到內存中,內存中的數據相當於數據庫中的一個副本,用戶查詢數據時,不從數據庫查詢而是直接從緩存中讀取,數據庫的數據發生了變化時再更新緩存,這樣,數據查詢的性能就大大提升了。
當然數據庫性能也沒有說的那么不堪,對於大部分訪問量不大的個人博客而言,任何關系型數據庫都足以應付。但是我們學習 django-rest-framework 不僅僅是為了寫博客,也許你在工作中,面對的是流量非常大的系統,這時候緩存就不可或缺。
確定需緩存的接口
先來整理一下我們已有的接口,看看哪些接口是需要緩存的:
接口名 | URL | 需緩存 |
---|---|---|
文章列表 | /api/posts/ | 是 |
文章詳情 | /api/posts/:id/ | 是 |
分類列表 | /categories/ | 是 |
標簽列表 | /tags/ | 是 |
歸檔日期列表 | /posts/archive/dates/ | 是 |
評論列表 | /api/posts/:id/comments/ | 是 |
文章搜索結果 | /api/search/ | 否 |
補充說明
- 文章列表:需要緩存,但如果有文章修改、新增或者刪除時應使緩存失效。
- 文章詳情:需要緩存,但如果文章內容修改或者刪除了應使緩存失效。
- 分類、標簽、歸檔日期:可以緩存,但同樣要注意在相應的數據變化時使緩存失效。
- 評論列表:可以緩存,新增或者刪除評論時應使緩存失效。
- 搜索接口:因為搜索的關鍵詞是多種多樣的,可以緩存常見搜索關鍵詞的搜索結果,但如何確定常見搜索關鍵詞是一個復雜的優化問題,這里我們不做任何緩存處理。
配置緩存
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:
- 接口調用的視圖方法的 id,例如 blog.views. PostViewSet.list。
- 客戶端請求的接口返回的數據格式,例如 json、xml。
- 客戶端請求的語言類型。
另外我們還添加了 3 條自定義的緩存 key 的 KeyBit:
- 執行數據庫查詢的 sql 查詢語句
- 分頁請求的查詢參數
- 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,信號發出后該方法將被調用,往緩存中寫入文章資源的更新時間。
整理一下請求被緩存的邏輯:
- 請求文章列表接口
- 根據
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 服務
本地內存緩存服務配置簡單,適合在開發環境使用,但無法適應多線程和多進程適的環境,線上環境我們使用 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
然后就可以將服務發布上線了。
關注公眾號加入我們