而這些不同的動作對應的數據其實是存在不同的表中,例如話題表、回帖表、評論表等等。
今天主要是介紹 OSChina 是如何將這些屬於不同范圍的數據匯總到用單一時間軸進行展示的動態。
動態表
首先要說明的是動態表,這個表在 OSChina 數據庫中對應的表名是 osc_opt_logs ,從這個名字能看出意思是“操作記錄表”,字段如下:
字段說明:
id 主鍵字段,動態記錄的唯一標識
user 某人的動態
obj_type/obj_id 由這兩個字段組合起來,表示對應不同類型的動作,例如是新聞、提問、回帖等,每一種動作都有唯一的 obj_type 對應,這些常量有:
parent_type/parent_id 這兩個字段用來表示具有一些層次關系的動態,例如回帖:parent_type 和 parent_id 就會填充上回帖對應帖子的編號和類型
shown 轉發標識,主要用於動彈的評論是否在空間中顯示與否
reply_count 如果此動態是一條動彈,那么此字段存放動彈的評論次數
memo 如果此動態是一條動彈,那么此字段存放動彈的內容
attachment 動彈對應的圖片
referer 保留用
appid 動彈的方式,可以是PC瀏覽器、手機版或者不同的手機客戶端
create_time 動作發生的時間
obj_type 取值常量表:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public
final
static
byte
TYPE_PROJECT =
0x01
;
public
final
static
byte
TYPE_QUESTION =
0x02
;
//問題
public
final
static
byte
TYPE_ANSWER =
0x11
;
//答案
public
final
static
byte
TYPE_BLOG =
0x03
;
public
final
static
byte
TYPE_NEWS =
0x04
;
public
final
static
byte
TYPE_CODE =
0x05
;
public
final
static
byte
TYPE_JOB =
0x06
;
//招聘職位
public
final
static
byte
TYPE_GROUP_BUY =
0x0F
;
//團購信息
public
final
static
byte
TYPE_NEWS_COMMENT =
0x10
;
//新聞評論
public
final
static
byte
TYPE_BLOG_COMMENT =
0x12
;
//博客評論
public
final
static
byte
TYPE_CODE_COMMENT =
0x13
;
//代碼評論
public
final
static
byte
TYPE_JOB_COMMENT =
0x14
;
//招聘職位評論
public
final
static
byte
TYPE_TWEET =
100
;
//動彈
public
final
static
byte
TYPE_TWEET_REPLY =
101
;
//動彈評論
|
假設某個動態是回帖,那么 obj_type 值為 TYPE_ANSWER,而 obj_id 的值則為對應回帖的唯一編號。
動彈和動彈評論
動彈和動彈評論是比較特殊的動態,它沒有對應到其他表中的關系。動彈對應的 obj_type 值為 TYPE_TWEET,動彈評論對應的 obj_type 值為 TYPE_TWEET_REPLY。而 memo 則存放了動彈或者評論的內容。
當我們進入某個用戶空間的時候,會列出這個用戶的所有動態,查詢很簡單:
1
|
SELECT
*
FROM
osc_opt_logs
WHERE
user
= ?
ORDER
BY
id
DESC
|
如果是查看某個類別的動態,例如我們只想看 蟋蟀哥哥 的動彈,只需要:
1
|
SELECT
*
FROM
osc_opt_logs
WHERE
user
= ?
AND
obj_type = 100
ORDER
BY
id
DESC
|
其中 100 就是 TYPE_TWEET 常量值。
這樣簡單的查詢,在任何數據庫上執行都是非常之快的。
唯一麻煩的在於,取出了動態列表后,如何加載它們對應的目標數據,如果是一個提問,那必須取出對應的帖子記錄。
這個的確是很麻煩,代碼量也比較大,只能簡單的介紹一下思路:
首先將獲取的動態列表按照 obj_type 進行分組,然后根據 obj_id 批量去 load 對應的數據,例如我們可組合從像下面這樣的 SQL 語句來 load 數據:
1
|
SELECT
*
FROM
osc_questions
WHERE
id
IN
(?,?,?,?,?,?)
|
這樣在首次進入某個用戶空間,光是動態這塊可能需要執行七八個SQL語句來獲取顯示完整的動態列表,好在這些 SQL 語句都是最基本的查詢,再結合緩存的使用,性能還是非常之好的。
查看自己的空間
以上都是關於看別人空間時的說明,但大家應該都發現了,在看自己的空間時,並不只是自己的動態,還會包含自己所關注的人的所有動態。
我自己的以及我關注的人的動態。這個查詢條件看似簡單,一般人會這樣寫 SQL 語句:
1
|
SELECT
l.*
FROM
osc_opt_logs l,osc_friends f
WHERE
l.
user
= ?
OR
(l.
user
=f.friend
AND
f.
user
= ?)
ORDER
BY
l.id
DESC
|
請注意這里用到了 OR 查詢,這個 OR 查詢讓你的所有優化都白做,索引也用不上。在你絞盡腦汁研究怎么改 SQL 語句的時候,不妨換一個角度來思考。
不就是差一個我自己的 id 嗎?如果在 osc_friends 表里也有我自己的記錄,那這個 SQL 不就可以簡化為:
1
|
SELECT
l.*
FROM
osc_opt_logs l,osc_friends f
WHERE
l.
user
=f.friend
AND
f.
user
= ?
ORDER
BY
l.id
DESC
|
這里沒有 OR 查詢,查詢速度快多了。唯一需要處理的就是每次用戶注冊的時候往 osc_friends 表中插入一條自己是自己好友的記錄,也就是 user=我的用戶id,friend=我的用戶id。
這還是我的偏好,通過冗余數據來提升查詢性能。是好是壞,大家自己掂量。
我只能說目前這種方案是適合 OSChina 目前的狀態,如果規模再大一點時候肯定還有改造,至於怎么改,到時候再說。涉及到用戶動態的其他方面的都不值一提。
全文完,希望對大家有所幫助。
最近在做類似的功能,遇到幾個問題:
1. 動態類型多樣性;
2. 數據模塊化存儲,各模塊間通過rest調用數據,造成拉取動態列表響應時間變長;
3. 數據層級復雜,編碼邏輯通用性差。例如:轉發一篇文章后,評論該轉發,然后回復該評論時的對象層級為: 回復->評論->文章。
求有相關經驗者分享一下經驗為謝!
以下是現有的設計:
動態的結構:
{ user_id: 動態創建者ID, action: 行為類型, object_type: 動態對象類型, object_id: 對象ID, object_user: 對象所有者, view_count: 0, created_at: 創建時間, deleted_at: 刪除時間, }
場景列表:
// A 發布 了 文章 xxx 'action' => NEW, 'user_id' => A的ID, 'object_id' => 文章ID, 'object_user' => A的ID, 'object_type' => ARTICLE, 'ext' => [], // A 發布 了 N張 圖片 'action' => NEW, 'user_id' => A的ID, 'object_id' => 圖片ID(數組,以逗號隔開), 'object_user' => A的ID, 'object_type' => PICTURE, 'ext' => [], // 4. A 提了 問題 xxxx 'action' => NEW, 'user_id' => A的ID, 'object_id' => 問題ID, 'object_user' => A的ID, 'object_type' => QUESTION, 'ext' => [], // 5. A 在 文章 中回復了 B 的 評論 'action' => REPLY, 'user_id' => A的ID, 'object_id' => 評論ID, 'object_user' => B的ID, 'object_type' => COMMENT, 'ext' => [ 'text' => $text, 'comment_target_id' => '文章ID', //評論所屬對象 'comment_target_type' => 'ARTICLE',//評論所屬對象類型 'reply_id' => 回復ID, ], // 6. A 評論 了 B的 文章 xxxx 'action' => COMMENT, 'user_id' => A的ID, 'object_id' => 文章ID, 'object_user' => B的ID, 'object_type' => 'ARTICLE', 'ext' => [ 'comment_id' => '評論ID', ], // 7. A 回答 了 B 的 提問 xxx 'action' => RESPOND, 'user_id' => A的ID, 'object_id' => 問題ID, 'object_user' => B的ID, 'object_type' => QUESTION, 'ext' => [ 'answer_id' => '答案ID', ],
最終我參考開源中國做了調整,以完成我們的需求:
動態的結構:
{ user_id:13, action: 行為, object_id: 對象ID, object_type: 對象類型, object_user_id: 對象用戶ID, parent_object_id: 對象父級ID, parent_object_type: 對象父級類型, parent_object_user_id: 對象父級用戶ID, reply_id: 回復ID, // action為回復時有用 parent_reply_id: 回復的父級回復ID, // action為回復時有用,回復了別人對評論的回復 text: '轉發或者分享時附加文字', view_count: 0, created_at: 創建時間, deleted_at: 刪除時間, }
說明:
1. object_*
只存儲主要模塊內容信息,不含評論;
2. parent_object_*
存儲有嵌套關系的對象,比如當object_*
為答案時,parent_object_*
為問題;
3. reply_id
用於直接回復評論時用到;
4. parent_reply_id
父回復ID;
5. 兩個回復ID,使用情況是:當回復了別人的回復時,根據comment_id
拉取評論與全部回復,在模板顯示時只顯示對話的兩個回復。
場景列表:
一級結構:
- 安正超 發布 了 文章
'action' => NEW, 'user_id' => 安正超ID, 'object_id' => 文章ID, 'object_user_id' => 安正超ID, 'object_type' => ARTICLE,
- 安正超 上傳 了 N張 圖片
'action' => NEW, 'user_id' => 安正超ID, 'object_id' => 圖片ID(數組,以逗號隔開), 'object_user_id' => 安正超ID, 'object_type' => PICTURE,
- 安正超 提了 問題 xxxx
'action' => NEW, 'user_id' => 安正超ID, 'object_id' => 問題ID, 'object_user_id' => 安正超ID, 'object_type' => QUESTION
二級結構:
- 安正超 評論 了 文章 xxxx(回答了通用)
展示:
文章: xxxxx
評論:xxxxx (李林評論的)
'action' => COMMENT, 'user_id' => 安正超ID, 'object_id' => 評論ID, 'object_type' => COMMENT, 'object_user_id' => 安正超ID 'parent_object_id' => 文章ID, 'parent_object_user_id' => 作者ID 'parent_object_type' => ARTICLE,
三級結構:
- 安正超 在 文章 中 回復 了 李林 的 評論
展示:
文章: xxxxx
評論:xxxxx (李林評論的)
回復:xxxx (安正超)
'action' => REPLY, 'user_id' => 安正超ID, 'object_id' => 評論ID, 'object_type' => COMMENT, 'object_user_id' => 李林ID 'parent_object_id' => 文章ID, 'parent_object_user_id' => 作者ID 'parent_object_type' => ARTICLE, 'reply_id' => 安正超的回復ID
四級結構:
- 安正超 回復了 李文凱 在 問題 “xxxx” 中 李林 的答案 下的 評論
說明:問題信息從答案接口取回
展示:
問題: xxxxx
答案1... 答案2... 答案3...(李林回答的) 評論:xxxxx (李文凱評論的) 回復:xxxx (安正超)
'action' => RESPOND, 'user_id' => 安正超ID, 'object_id' => 評論ID, 'object_type' => COMMENT, 'object_user_id' => 李文凱的ID 'parent_object_id' => 答案ID, 'parent_object_type' => ANSWER, 'parent_object_user_id' => 李林ID 'reply_id' => 安正超的回復ID
- 安正超 回復了 李文凱 在 問題 “xxxx” 中 李林 的答案 下的 回復
說明:問題信息從答案接口取回
展示:
問題: xxxxx
答案1... 答案2... 答案3...(李林回答的) 評論:xxxxx (A評論的) 李文凱 回復 A:xxxx 安正超 回復 李文凱:xxxx
'action' => RESPOND, 'user_id' => 安正超ID, 'object_id' => 評論ID, 'object_type' => COMMENT, 'object_user_id' => A的ID 'parent_object_id' => 答案ID, 'parent_object_type' => QUESTION, 'parent_object_user_id' => 李林ID, // 以下兩個回復只在模板中用到用以決定顯示哪兩個回復,因為根據comment_id帶着回復會全部拉回來 'parent_reply_id' => 李文凱的回復ID, 'reply_id' => 安正超的回復ID,