項目源代碼已託管在 Github,歡迎 Star、Fork。
QA 是一個基於 B/S 架構而設計開發的社區網站。html
主要爲用戶提供如下服務:前端
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 適合放一些頻繁使用、比較熱的數據。由於數據放在了內存中,讀寫性能卓越。
Redis 類型 | 數據結構 | 應用場景 |
---|---|---|
List | 雙向列表 | 最新列表、關注列表 |
Set | 無序集合 | 贊踩、抽獎、已讀、共同好友 |
SortedSet | 優先隊列 | 排行榜 |
Hash | 哈希表 | 不定長屬性數 |
KV | 單一數值 | 驗證碼、PV、緩存 |
除了用戶內容贊踩,在本項目中,Redis 還應用於如下場景:
本小節討論用戶內容贊踩服務。
用戶對某一實體點贊,會將"LIKE:ENTITY_TYPE:ENTITY_ID"做爲 key ,用戶 id 做爲 value ,存入 like 集合中。同時移除 unlike 集合中該 key 對應的用戶 id。點踩服務反之。
最後將點贊數響應給頁面。
本項目涉及到多種異步事件的處理。如:
這些動做並非單一的,它們會觸發一些後續的操做:
用戶評論了某個問題,系統除了處理「評論」這個動做外,還須要給該問題對應的用戶發送一條消息,通知說「xx評論你的問題」,或者還須要給用戶增長積分/經驗...
事件觸發者並不關心這些後續的任務,系統處理完某個動做後就能夠將結果返回給觸發者,然後續的任務交給系統進行異步處理便可。
所以,設計一個異步事件處理框架尤其重要。
本項目的異步框架以下圖所示:
業務觸發一個異步事件,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; }
與評論功能相似,對於關注功能,系統一樣創建了一個統一的關注服務中心,用戶能夠關注不一樣的實體(問題/用戶),只須要經過 EntityType 和 EntityId 識別便可。
在數據存儲方面,採用 Redis 的 zset 完成,緣由有如下幾個:
一個用戶,系統存儲兩個集合:
①保存用戶關注的實體;②保存關注用戶的人。
即 A 是 B 的粉絲,B 是 A 的關注對象。 [參考資料 ]
用戶關注了一個問題,須要發生兩個動做:
這兩個動做必須同時發生,所以,這裏用到了 Redis 事務保證原子性和數據的一致性。
另外,對於關注功能,如前面所說,會觸發異步事件,將消息通知被關注的實體 / 實體 Owner。
本系統未採用排名算法。若要了解相關算法,能夠參考以下資料:
當用戶更新動態時,該用戶全部粉絲均可以在必定時間內收到新的動態(也稱爲新鮮事、feed),能夠由 「推拉模式」 實現。
模式 | 定義 | 優缺點 |
---|---|---|
推 | 事件觸發後廣播給全部粉絲。 | 對於粉絲數過多的事件,後臺壓力較大,浪費存儲空間; 流程清晰,開發難度低,關注新用戶須要同步更新 feed 流。 |
拉 | 登陸打開頁面時,根據關注的實體動態生成 Timeline 內容。 | 讀取壓力大,存儲佔用小,緩存最新讀取的 feed,根據時間分區拉取。 |
推拉 | 活躍/在線用戶推,其餘用戶拉。 | 下降存儲空間,又知足大部分用戶的讀取需求。 |
具體來講,推模式就是:事件觸發後產生 feed,觸發事件的用戶下全部粉絲的 Timeline(redis list 實現)中都存入該 feed 的 id。而拉模式,就是當前用戶去拉取本身關注的人的 feed。
更多推拉模式相關,能夠參考 微博 feed 系統推拉模式。
因爲系統初始數據較少,爲了豐富網站內容,本項目採用 pyspider 實現對 V2EX 網站的數據爬取,存儲到後臺數據庫,並展現在前端頁面上。
安裝 pyspider:
pip install pyspider
啓動 pyspider:
pyspider
本項目在全文搜索服務上採用 Solr 框架,中文分詞采用 Solr 自帶的中文分詞插件 solr_cnAnalyzer 。