第 12 篇:加緩存爲接口提速

做者: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

  1. 文章列表:須要緩存,但若是有文章修改、新增或者刪除時應使緩存失效。
  2. 文章詳情:須要緩存,但若是文章內容修改或者刪除了應使緩存失效。
  3. 分類、標籤、歸檔日期:能夠緩存,但一樣要注意在相應的數據變化時使緩存失效。
  4. 評論列表:能夠緩存,新增或者刪除評論時應使緩存失效。
  5. 搜索接口:由於搜索的關鍵詞是多種多樣的,能夠緩存常見搜索關鍵詞的搜索結果,但如何肯定常見搜索關鍵詞是一個複雜的優化問題,這裏咱們不作任何緩存處理。

配置緩存

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 服務部分。

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
複製代碼

而後就能夠將服務發佈上線了。


關注公衆號加入交流羣
相關文章
相關標籤/搜索