Spring Boot項目實踐之問答社區

項目源代碼已託管在 Github,歡迎 Star、Fork。

Q & A 問答社區

QA 是一個基於 B/S 架構而設計開發的社區網站。html

Build StatusJava VersionFramework

主要爲用戶提供如下服務:前端

  • 問題發佈
  • 評論
  • 用戶私信
  • 關注
  • 站內全文搜索

技術選型

Spring Boot + MyBatis + MySQL + Redis + FreeMarkerjava

功能描述

註冊登陸

爲了保證用戶信息安全,系統對用戶密碼採用「salt + md5」方式進行加密。用戶註冊/登陸成功後,系統會生成一個 ticket ,將 ticket 與用戶 id 相關聯,並將此信息插入到數據庫表 login_ticket 中,同時將 ticket 響應給客戶端。node

用戶每次請求頁面的時候,都須要先通過 PassportInterceptor 攔截器,攔截器判斷此 ticket 是否真實有效,如果,根據 ticket 對應的用戶 id ,查出相應用戶信息,並添加至頁面上下文中。git

用戶內容發佈

  • 問題發佈
  • 評論發佈
  • 私信發佈

在以上 UGC (User Generated Content, 用戶產生的內容)中,系統都會進行 HTML 標籤及敏感詞過濾,這在必定程度上防止網站被注入腳本或者充斥着不良信息。github

若沒有對 HTML 標籤進行處理,當用戶發佈的內容含有如 <script>alert("hahah");</script>時,網站頁面每次加載此內容時都會彈出消息框。

對於敏感詞過濾,按照常規的思惟,也是最簡單的方式,就是:對於每一個敏感詞,都在文本中查找該敏感詞是否出現,出現則進行替換。這種方式,每一個敏感詞都要在一段文本中進行遍歷查找,複雜度很是高。redis

本項目採用「前綴樹」方式實現敏感詞過濾,空間換時間,效率較高。前綴樹結點結構以下:算法

class TrieNode {
    // 標記是否爲敏感詞結尾
    boolean end;
    
    // 該結點的全部直接子結點
    Map<Character, TrieNode> subNodes = new HashMap<>();
    
    // 添加一個子結點
    void addSubNode(Character key, TrieNode node) {
        subNodes.put(key, node);
    }
    
    // 根據key獲取子結點
    TrieNode getSubNode(Character key) {
        return subNodes.get(key);
    }
}

後臺從敏感詞文件 SensitiveWords.txt 順序讀取每一行創建前綴樹。進行過濾時,遍歷須要過濾的文本,用星號替換髮現的敏感詞。假設文本長度爲 len,前綴樹的最大高度爲 h,那麼此算法的最壞時間複雜度爲 O(len*h)。spring

算法比較
假如敏感詞平均長度爲10,數量爲100000,文本長度爲 len。
常規方式,複雜度O(100000 (len + 10));前綴樹算法複雜度O(10 len)。

對於評論功能,系統創建的是一個統一的評論服務中心,經過 EntityType 與 EntityId 識別所評論的實體。用戶對於問題/評論的回覆,均可以應用此服務。查詢某實體下的評論時,一樣根據 EntityType 和 EntityId 查詢便可。數據庫

用戶內容贊踩

贊踩功能採用「Redis」做爲數據存儲。Why Redis?

比較一下 Redis 和 MySQL:

  • Redis: key-value數據庫,數據放在內存
  • MySQL: 關係型數據庫,數據放在磁盤

Redis 適合放一些頻繁使用、比較熱的數據。由於數據放在了內存中,讀寫性能卓越。

Redis 類型 數據結構 應用場景
List 雙向列表 最新列表、關注列表
Set 無序集合 贊踩、抽獎、已讀、共同好友
SortedSet 優先隊列 排行榜
Hash 哈希表 不定長屬性數
KV 單一數值 驗證碼、PV、緩存

除了用戶內容贊踩,在本項目中,Redis 還應用於如下場景:

  • 異步事件處理
  • 關注服務
  • Timeline

本小節討論用戶內容贊踩服務。

用戶對某一實體點贊,會將"LIKE:ENTITY_TYPE:ENTITY_ID"做爲 key ,用戶 id 做爲 value ,存入 like 集合中。同時移除 unlike 集合中該 key 對應的用戶 id。點踩服務反之。
最後將點贊數響應給頁面。

異步事件處理

本項目涉及到多種異步事件的處理。如:

  • 用戶評論了某個問題
  • 用戶點讚了某條評論
  • 用戶關注了另外一個實體

這些動做並非單一的,它們會觸發一些後續的操做:

用戶評論了某個問題,系統除了處理「評論」這個動做外,還須要給該問題對應的用戶發送一條消息,通知說「xx評論你的問題」,或者還須要給用戶增長積分/經驗...

事件觸發者並不關心這些後續的任務,系統處理完某個動做後就能夠將結果返回給觸發者,然後續的任務交給系統進行異步處理便可。

所以,設計一個異步事件處理框架尤其重要。
本項目的異步框架以下圖所示:

Async Event

業務觸發一個異步事件,EventProducer 將該事件(EventModel)序列化並存入隊列(Redis List)中,EventConsumer 開啓線程循環從隊列中取出事件,識別該事件的類型,找出該類型對應的一系列 EventHandler,交由這些 Handler 去處理。

EventModel 的設計以下:

class EventModel {
    // 事件類型
    EventType type;
    
    // 事件觸發者
    int actorId;
    
    // 事件對應的實體
    int entityType;
    int entityId;
    
    // 事件對應的實體的Owner
    int entityOwnerId;
    
    // 一些擴展字段
    Map<String, String> exts;
}

SNS 關注服務

與評論功能相似,對於關注功能,系統一樣創建了一個統一的關注服務中心,用戶能夠關注不一樣的實體(問題/用戶),只須要經過 EntityType 和 EntityId 識別便可。
在數據存儲方面,採用 Redis 的 zset 完成,緣由有如下幾個:

  • zset 有序,系統能夠根據用戶關注實體的時間倒序排列,獲取最新的關注列表;
  • zset 去重,用戶不能重複關注同一個實體;
  • zset 能夠獲取兩用戶之間的共同關注。

一個用戶,系統存儲兩個集合:

①保存用戶關注的實體;②保存關注用戶的人。

即 A 是 B 的粉絲,B 是 A 的關注對象。 [參考資料 ]

用戶關注了一個問題,須要發生兩個動做:

  • 將問題存入①中
  • 在②中存入用戶 id

這兩個動做必須同時發生,所以,這裏用到了 Redis 事務保證原子性和數據的一致性。

另外,對於關注功能,如前面所說,會觸發異步事件,將消息通知被關注的實體 / 實體 Owner。

用戶內容排名

本系統未採用排名算法。若要了解相關算法,能夠參考以下資料:

Timeline Feed 流服務

當用戶更新動態時,該用戶全部粉絲均可以在必定時間內收到新的動態(也稱爲新鮮事、feed),能夠由 「推拉模式」 實現。

模式 定義 優缺點
事件觸發後廣播給全部粉絲。 對於粉絲數過多的事件,後臺壓力較大,浪費存儲空間;
流程清晰,開發難度低,關注新用戶須要同步更新 feed 流。
登陸打開頁面時,根據關注的實體動態生成 Timeline 內容。 讀取壓力大,存儲佔用小,緩存最新讀取的 feed,根據時間分區拉取。
推拉 活躍/在線用戶推,其餘用戶拉。 下降存儲空間,又知足大部分用戶的讀取需求。

具體來講,推模式就是:事件觸發後產生 feed,觸發事件的用戶下全部粉絲的 Timeline(redis list 實現)中都存入該 feed 的 id。而拉模式,就是當前用戶去拉取本身關注的人的 feed。

更多推拉模式相關,能夠參考 微博 feed 系統推拉模式

Python 爬蟲

因爲系統初始數據較少,爲了豐富網站內容,本項目採用 pyspider 實現對 V2EX 網站的數據爬取,存儲到後臺數據庫,並展現在前端頁面上。

安裝 pyspider:

pip install pyspider

啓動 pyspider:

pyspider

站內全文搜索服務

本項目在全文搜索服務上採用 Solr 框架,中文分詞采用 Solr 自帶的中文分詞插件 solr_cnAnalyzer 。

相關文章
相關標籤/搜索