專案背景
當初出於留存的考慮,產品同事在app內設計了類似微博的feed功能。從功能上看,我們的feed服務更像是微博和微信朋友圈的結合體。既有微博熱門的場景,也有微信朋友圈的影子。
功能列表
- feed資料頁
類似微信朋友圈的相簿功能,可以看到使用者曾經發布的feed動態。
- feed新鮮事頁
類似微信朋友圈功能,可以看到自己及好友(關注的人)釋出的feed動態。
- feed廣場頁
類似微博的推薦或熱門功能,為使用者做個性化的推薦。
基本思路
專案思考
1. 資料儲存
在feed動態主要儲存兩種資訊,一是動態內容,二是不同維度的動態索引。
feed資料結構
feed資訊除了基本的內容外還需要儲存額外的資訊,並且額外資訊可能還會面臨擴充套件的情況。所以feed資料結構基本定義如下。在資料庫裡面儲存的是FeedInfo資訊序列化後的資料,這樣後續可以支援擴充套件,而不需要修改資料庫欄位。
message FeedItem {
string feed_id = 1; //動態id
int64 create_time = 2; //釋出時間
User from_user = 3; //釋出者
int32 feed_type = 4; //對應FeedType
bytes attachment = 5; //附件資訊
...
}
message FeedInfo {
FeedItem feed_item = 1;
bool deleted = 2; //是否刪除
AuditType audit_type = 3; // 稽核型別
VisibleStatus visible_status = 4; // 動態可見性
...
}
複製程式碼
內容儲存
feedId => feed內容
動態內容可以抽象成KV形式的鍵值儲存,幾乎所有的場景都是根據動態ID來獲取動態內容,然後進行後續處理。因此選用了HBase作為優先的內容儲存服務,再以Mysql和Redis為輔,作為降級方案。畢竟是首次將HBase應用到線上服務。以最終的效能資料來看,HBase的效能還是不錯的。
索引資料儲存
其它維度的索引關係還是使用Mysql儲存,畢竟可能會涉及到複雜的查詢。
- 資料頁feed
類似微信朋友圈相簿功能,需要儲存使用者所釋出的所有動態,按使用者維度進行分表。基本資訊如下:
屬性 | 備註 |
---|---|
user_id | 釋出者ID |
feed_id | 動態ID |
feed_create_time | 動態建立時間 |
visible_status | 動態可見性 |
- 新鮮事feed
類似微信朋友圈功能,需要儲存所有關注人及自己的動態,按使用者維度分表。基本資訊如下:
屬性 | 備註 |
---|---|
user_id | 使用者ID |
feed_id | 動態ID |
from_user_id | 釋出者ID |
feed_create_time | 動態建立時間 |
- 廣場推薦feed
類似微博的熱門推薦功能,需要根據時間線儲存所有使用者釋出的feed。因此廣場feed根據日期進行分表。1個月有31天,這裡一共分成31個表,不同日期的feed儲存到不同表。基本資訊和新鮮事feed基本一致,只是儲存的資料及資料維度有所不同。
屬性 | 備註 |
---|---|
user_id | 使用者ID |
feed_id | 動態ID |
from_user_id | 釋出者ID |
feed_create_time | 動態建立時間 |
這裡按天分表有以下考慮:按當前預估釋出量,31個分表應該足夠,不需要再細分。按天分表基本能保證資料的連續性,比較符合查詢習慣。
2. 資料擴散
- 擴散方式
資料擴散有寫擴散和讀擴散兩種方式。 讀擴散是在讀取的時候再進行擴散,這樣一次讀請求可能會涉及好幾個地方的讀取,讀取耗時不可控。寫擴散一般是儲存多份資料,雖然冗餘儲存了很多資料資訊,但是讀取的效率可以提高不少,在當下磁碟等硬體資源並不緊缺的情況下寫擴散應該是更為合適的處理方式。
- 資料流向
在feed服務裡面使用者釋出的feed會先在本人的資料頁及新鮮事頁可見,優先保證釋出者的體驗。然後釋出的feed才會擴散到其粉絲好友和廣場頁進行曝光。後面的流程使用者基本對延遲不感知的,因此這裡通過訊息佇列進行非同步化的寫擴散處理。基本處理流程如下:
3.稽核機制
使用者釋出動態需要經過稽核,為了避免違規的內容推送給使用者。稽核機制一般有先審後發和先發後審兩種。
- 先審後發
使用者釋出的feed必須先通過稽核後才可以擴散給其他使用者。因此可以在feed寫入釋出者資料頁和新鮮事頁之後,寫入訊息佇列進行擴散之前進行攔截。只有當feed稽核通過後才寫入訊息佇列進行寫擴散,這樣釋出者和其他使用者也不會有明顯的感知。不過對於釋出者來說,可能會感覺到互動的延遲。
- 先發後審
使用者釋出的feed可以先進行擴散曝光,當feed稽核不通過時才進行刪除操作。這種情況下使用者的體驗會更好,不過會增加平臺的一些風險。
4.點贊設計
- 使用者維度點贊列表
使用者點贊資料會儲存在資料庫和redis快取。由於使用者點贊行為不確定,部分使用者可能會頻繁點贊。因此使用redis的zset結構快取使用者部分點贊資料,field為feedId,score也是feedId,根據feedId倒序排列,只保留最新的N條feed點贊資料。
使用者點贊列表為什麼不以點贊時間排序?因為使用者點贊時間是不確定的,使用者很有可能點讚了很久之前的feed。這樣就意味著當快取裡沒找到feed點贊資料時,無法確定是快取缺失還是使用者沒點贊,最後都得在資料庫再次查詢做確認。
使用者點贊列表按feedId排序的情況下,若feedId不在列表中,且feedId大於點贊快取中最小的feedId,就能確定使用者的確沒有點贊,不需要再從資料庫進行查詢。
- feed維度點贊計數
在我們的場景裡,feed維度點贊計數是重要的資料。一開始設計的時候,使用redis的count來同步計數,可是會存在漏計數或者重複計數的問題,沒辦法保證資料的準確性。後來考慮到點讚的頻率不會很高,調整成從資料庫獲取count計數,將計數再同步到redis快取。
- 拆分點贊流程
在設計裡面優先保證使用者端體驗,因此在儲存使用者維度的資料後會快速返回,將本次點贊請求寫入訊息佇列。然後再處理feed維度點贊資料的維護,包括調整feed點贊計數和給feed釋出者傳送點贊訊息等。根據重要程度拆分流程,優先保證使用者的體驗。
5.trade off策略
- 廣場推薦頁保底策略
廣場推薦feed一般是推薦服務根據使用者特徵返回其更感興趣的feed列表。當推薦服務不可用時,需要有個保底策略以確保使用者能正常拉取到feed資訊,避免影響使用者體驗。這裡就實現了個保底策略,利用redis的zset結構維護所有使用者釋出的最新的N條feed。當推薦服務不可用時直接返回該列表的資訊。
- 被關注者最新的N條feed
當使用者新關注其他使用者時,被關注者的feed應當出現在使用者的新鮮事頁面。考慮到資訊的實時性,我們只選擇被關注者最新的N條feed插入到使用者的新鮮事,而不是被關注者所有的feed列表。這樣處理基本也不影響使用者體驗,處理上相對簡單一點。
結語
這是第一次思考feed服務的設計並加以實現,基本涵蓋了主要的場景。在這過程中也不斷地進行了一些小優化,也有不小的收穫,其中還有很多可以再優化的地方。