高併發文章瀏覽量計數系統設計

最近由於我的網站的文章瀏覽量計數在Chrome瀏覽器下有BUG,因此打算從新實現這個功能。前端

本來的實現很簡單,每次點擊文章詳情頁的時候,前端會發送一個GET請求articles/id獲取一篇文章詳情。這個時候,會把這篇文章的瀏覽量+1,再存進數據庫裏。redis

這個實現本來能夠實現這個功能,可是後來我才發現,我犯了一個很致命的錯誤:在GET請求的業務邏輯裏進行了數據的寫操做!sql

原則來說,GET請求應該具備冪等性,即短期內同時兩個如出一轍的GET請求,返回的結果也應該是同樣的。而我本來的實現就破壞了GET請求的冪等性。數據庫

剛好,在Chrome瀏覽器裏,個人文章詳情頁會發送兩次GET請求。這疑似Chrome瀏覽器和nuxt服務端渲染之間的一個BUG,目前尚未定位到具體緣由。後端

但不管如何,後端應該是能夠避免這樣的BUG,即便某用戶短期內請求兩次或者屢次,也應該只增長一次瀏覽量計數。瀏覽器

因爲最近在學習高併發方面的知識,因此這裏也考慮一下,若是一個高併發的文章瀏覽量計數系統,應該如何設計?緩存

先來理一下需求。多線程

需求

  1. 用戶能夠是匿名的,不須要登陸
  2. 每當一個用戶點擊了一個文章的詳情頁面,這個文章的瀏覽量應該+1
  3. 用戶應該能當即看到本身點擊文章後瀏覽量+1的反饋
  4. 瀏覽量這個數據存在Mysql和ElasticSearch裏面,要最終一致(不要求強一致)
  5. 做者可能在後臺編輯文章,而後保存文章。若是在這期間有瀏覽量的增長,保存文章的時候不該該覆蓋掉這段時間的瀏覽量增量。
  6. 應該在服務端對用戶的請求去重,防止用戶不斷刷新或者使用爬蟲不斷請求某個API(建議經過IP)
  7. 要過濾掉百度和谷歌的爬蟲請求(根據User-Agent頭判斷,能夠先不作)
  8. 要高性能地實現「查看瀏覽最多文章列表」的功能。
  9. 儘量優化性能,知足多個用戶的高併發需求。

設計思路

若是要知足高併發,那首先考慮用異步和緩存。因此考慮使用多線程加Redis的解決方案。併發

請求流程:異步

  1. 用戶點擊某篇文章詳情頁
  2. 前端發送一個PUT請求/articles/{id:\\d+}/view
  3. 後端使用線程池執行一個異步任務,當即返回給前端200響應。
  4. 前端獲得200響應後,當即把當前文章的瀏覽量+1,知足需求3。

請求流程.png

後端主要邏輯:

後端的主要思路是暫時把增長的瀏覽量(假設某篇文章爲n)放進Redis裏,而後每隔一段時間刷新到Mysql數據庫和ElasticSearch存儲裏,讓這篇文章的瀏覽量在現有的基礎上加n,而後把Redis這篇文章的瀏覽量清零。

  1. 後端首先判斷redis裏時候有沒有當前ip對這篇文章的瀏覽記錄,這個key爲:isViewd:articleId:ip。若是有,就說明以前瀏覽過,就什麼也不作,直接返回。若是沒有,就加上這個key。時間能夠設置爲1小時過時,防止佔用過多內存。這裏使用Redis的string類型。
  2. 若是第5步的結果是沒有,那就在Redis裏給這篇文章的瀏覽量+1。Redis的這個支持原子操做,因此不用擔憂併發問題。key爲viewCount:articleId,value爲緩存的瀏覽量。完成後當前線程任務就結束了。這裏使用Redis的string類型。這些key應該沒有過時時間。
  3. 弄一個定時任務,好比每5分鐘,去Redis裏拿緩存的瀏覽量,拿到後就更新到數據庫和ElasticSearch裏,並把Redis的數據清零。爲了防止併發帶來的問題,這裏應該是拿到m,就在Redis裏減去m,而不是直接設置爲0。
  4. 爲了節約內存,應該刪除沒必要要的key,按照業務邏輯來看,若是一篇文章長時間沒有人瀏覽,可能這篇文章比較「舊」了,咱們能夠考慮刪除它在Redis裏面的key。因此咱們能夠在第6步,每次在Redis裏進行瀏覽量+1操做時,記錄下一個時間戳。因此Redis可使用hash類型,一個字段存最後操做時間,一個字段存瀏覽量。而在第7步裏,咱們能夠順便刪除掉最後操做時間小於十天前的key。
  5. 保存更新文章的時候,應該只更新其它字段,而不更新瀏覽量這個字段。或者執行一遍第7步的邏輯。因爲Redis加減操做的原子性,這裏不用擔憂併發問題。若是當前線程把一篇文章的瀏覽量在Redis裏減了m,那定時任務線程應該獲得的是減了m以後的結果,因此數據會是一致的。
  6. 關於需求8,在併發量不算特別大的時候,咱們仍是去取數據庫裏面的數據,根據數據庫裏面的瀏覽量來排序,只是能夠在應用裏面給它加一個緩存,緩存時間應該與第7步定時任務一致,這裏設置爲5分鐘。

若是併發量特別大,能夠考慮不把瀏覽量存在數據庫裏,而僅存在Redis裏,這樣能夠獲得近乎實時的瀏覽量存儲,並且需求8排序也是實時的(使用zset),但這樣可能會耗費大量的內存資源。

後端邏輯.png

後記

雖然最後權衡了併發量和複雜性,個人我的網站的文章瀏覽邏輯並無徹底按照上述設計思路來作,但上述思路是我對一個高併發文章瀏覽量計數系統設計的思考,之後若是有機會能夠寫一個開源的版本。

可能實現起來會更復雜,根據併發量的不一樣,代碼也會有一些差異,以上思路僅供參考。

相關文章
相關標籤/搜索