做者: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
複製代碼
而後就能夠將服務發佈上線了。