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


文章轉自“荒野七叔  ”  鏈接  https://juejin.im/post/5c3aa3c86fb9a04a0e2d6c9f

來源平台 掘金

 

最近因為個人網站的文章瀏覽量計數在Chrome瀏覽器下有BUG,所以打算重新實現這個功能。

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

這個實現原本可以實現這個功能,但是后來我才發現,我犯了一個很致命的錯誤:在GET請求的業務邏輯里進行了數據的寫操作!

原則來講,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),但這樣可能會耗費大量的內存資源。
 
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM