目前,使用者對於介面的操作基本都需要查詢資料庫。獲取文章列表需要從資料庫查詢,獲取單篇文章需要從資料庫查詢,獲取評論列表也需要查詢資料。但是,對於部落格中的很多資源來說,在某個時間段內,他們的內容幾乎都不會發生更新。例如文章詳情,文章發表後,除非對其內容做了修改,否則內容就不會變化。還有評論列表,如果沒人釋出新評論,評論列表也不會變化。
要知道查詢資料庫的操作相對而言是比較緩慢的,而直接從記憶體中直接讀取資料就會快很多,因此快取系統應運而生。將那些變化不那麼頻繁的資料快取到記憶體中,記憶體中的資料相當於資料庫中的一個副本,使用者查詢資料時,不從資料庫查詢而是直接從快取中讀取,資料庫的資料發生了變化時再更新快取,這樣,資料查詢的效能就大大提升了。
當然資料庫效能也沒有說的那麼不堪,對於大部分訪問量不大的個人部落格而言,任何關係型資料庫都足以應付。但是我們學習 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
然後就可以將服務釋出上線了。
關注公眾號加入我們