1 什么是點贊系統(tǒng)
點贊是互聯(lián)網(wǎng)中常見的交互方式,系統(tǒng)根據(jù)用戶的點贊操作來跟蹤用戶的行為议忽,并對用戶的喜好進行分析懒闷。
點贊,在互聯(lián)網(wǎng)中是一個比較簡單的操作栈幸。用戶看到自己喜歡的信息愤估,點擊“贊”按鈕,點亮“贊”操作速址,再次點擊玩焰,取消之前的“贊”操作。是不是很 easy壳繁?
但震捣,一個明星微博的點贊數(shù)可能高達幾十萬,甚至上百萬闹炉。一個熱點新聞蒿赢,可能有數(shù)千、數(shù)萬渣触、甚至數(shù)十萬用戶同時點贊羡棵,如何處理這些極端情況呢?
2 系統(tǒng)設(shè)計要點
如果要構(gòu)建一套通用點贊系統(tǒng)嗅钻,首先需要對系統(tǒng)所涉及角色皂冰、主功能進行梳理。
2.1 系統(tǒng)角色
系統(tǒng)角色主要涉及 點贊發(fā)起者 和 點贊目標(biāo)對象养篓。
這兩個角色秃流,本質(zhì)上是對其他對象的一種引用。從領(lǐng)域設(shè)計角度柳弄,應(yīng)該是對其他限界上下文中聚合的引用舶胀。兩個角色,本身沒有唯一標(biāo)識,并根據(jù)屬性確定其相等性嚣伐。因此糖赔,符合值對象建模規(guī)范,應(yīng)該作為值對象處理轩端。
通常情況下放典,點贊發(fā)起者對應(yīng)系統(tǒng)的用戶(有的系統(tǒng)會有多個用戶系統(tǒng))。但基茵,點贊的目標(biāo)對象可能會有很多奋构,如新聞、評論耿导、帖子等等声怔。
系統(tǒng)角色如下:
角色 | 含義 | 建模方式 |
---|---|---|
Owner | 點贊發(fā)起者 | 值對象 |
Target | 點贊目標(biāo)對象 | 值對象 |
2.2 系統(tǒng)功能用例
系統(tǒng)功能,主要圍繞系統(tǒng)角色展開舱呻。
點贊發(fā)起者 Owner醋火,可以選擇一個點贊對象 Target 進行點擊操作。當(dāng)顯示目標(biāo)對象 Target 時箱吕,需要判斷該 Target 是否已經(jīng)點過贊芥驳。
對于點贊對象 Target,只有一個應(yīng)用場景茬高,就是在顯示時兆旬,獲取總的點贊數(shù)量。
詳細用例如下:
用例 | 含義 |
---|---|
點擊 | Owner 對特定 Target 進行點擊怎栽。如果沒有點贊丽猬,則點贊;如果已經(jīng)點贊熏瞄,則取消點贊 |
是否點贊 | 判斷特定 Owner 對 特定 Target 是否已經(jīng)點贊 |
獲取點贊數(shù)量 | 獲取特定 Target 總的點贊數(shù)量 |
2.3 贊功能設(shè)計
點贊發(fā)起者點擊“贊”按鈕脚祟,點亮“贊”操作,再次點擊强饮,取消之前的“贊”操作由桌。
2.3.1 識別建模類型
Like 可以接收點擊操作,并更新內(nèi)部狀態(tài)邮丰。同一個點贊發(fā)起者對同一個點贊目標(biāo)進行“贊”和“取消”操作時行您,針對的應(yīng)該是同一個 “Like” 實例,需要唯一標(biāo)識對操作進行跟蹤剪廉。綜上分析 “Like” 是一個實體娃循。
2.3.2 實體建模
首先,需要明確實體的名稱斗蒋,在這里淮野,我們簡單命名為 Like捧书。
行為建模
分析下 Like 的業(yè)務(wù)行為,Like 對外操作只提供一個 click 方法骤星,當(dāng)觸發(fā) click 操作時,Like 在 Submitted 和 Cancelled 之間進行切換爆哑。當(dāng) Like 狀態(tài)發(fā)生變化時洞难,需要發(fā)布對應(yīng)的內(nèi)部事件。
Like 所涉及的業(yè)務(wù)方法如下:
業(yè)務(wù)方法 | 含義 | 事件 | 業(yè)務(wù)規(guī)則 |
---|---|---|---|
click | 用戶點擊行為 | 無 | 無 |
submit | 點贊 | LikeSubmittedEvent | 當(dāng)用戶未點贊時觸發(fā) |
cancel | 取消點贊 | LikeCancelledEvent | 當(dāng)用戶已經(jīng)點贊時觸發(fā) |
為了避免 Like 的臃腫揭朝,我們將 Like 的狀態(tài)進行單獨建模队贱。構(gòu)建一個單獨的值對象,并將狀態(tài)相關(guān)的操作下推到該值對象中潭袱。我們稱為 LikeStatus柱嫌。
屬性建模
Like 所關(guān)聯(lián)的對象,包括 Target屯换、Owner 和 LikeStatus编丘,并且三者都是值對象。
屬性 | 類型 | 含義 |
---|---|---|
owner | Owner | 點贊發(fā)起者 |
target | Target | 點贊目標(biāo)對象 |
status | LikeStatus | 點贊狀態(tài) |
創(chuàng)建方式建模
Like 的創(chuàng)建方式比較簡單彤悔,沒有太復(fù)雜的業(yè)務(wù)邏輯嘉抓,因此,采用靜態(tài)方法對其進行創(chuàng)建晕窑。
2.3.3 小結(jié)
Like 是一個比較簡單的聚合抑片,具體結(jié)構(gòu)如下:
贊功能所涉及的對象見下表杨赤。
對象 | 含義 | 建模方式 |
---|---|---|
Like | 贊 | 實體&聚合根 |
LikeStatus | 贊狀態(tài) | 值對象 |
LikeSubmittedEvent | 點贊事件 | 內(nèi)部領(lǐng)域事件 |
LikeCancelledEvent | 取消贊事件 | 內(nèi)部領(lǐng)域事件 |
2.4 日志功能設(shè)計
Like 代表的是當(dāng)前點贊狀態(tài)敞斋,對于多次點擊植捎,只會記錄最后的結(jié)果,而中間的過程數(shù)據(jù)丟失了说敏。
日志鸥跟,本身不屬于業(yè)務(wù)功能,但對用戶行為分析非常重要盔沫,我們應(yīng)該將用戶的所有操作保存下來医咨。我們稱這些過程數(shù)據(jù)為 LikeLogger。
2.4.1 識別建模類型
日志主要用于記錄誰(Owner)對什么(Target)進行哪個操作(Action)架诞,在創(chuàng)建后就不在改變拟淮。基本符合值對象建模條件谴忧,但很泊,我們?nèi)绾螌ζ溥M行持久化呢角虫?
一般情況下,值對象的持久化依賴于包含它的實體委造,值對象會隨著實體的持久化而持久化戳鹅。但,Logger 是個整體概念昏兆,本身不屬于任何實體枫虏。在這種情況下,我們可以將其建模成一個不變實體爬虱,一來借助實體進行持久化隶债,二來避免對實體的修改。
2.4.2 不變實體建模
LikeLogger 為不變實體跑筝,內(nèi)部所包含的屬性死讹,不允許修改。
屬性建模
LikeLogger 所包含屬性如下:
屬性 | 類型 | 含義 |
---|---|---|
owner | Owner | 點贊發(fā)起者 |
target | Target | 點贊目標(biāo)對象 |
actionType | ActionType | 操作類型 |
創(chuàng)建方式建模
LikeLogger 支持 Like 和 Cancel 兩種類型的日志曲梗,可以根據(jù) ActionType 構(gòu)建靜態(tài)方法赞警,以完成各自的創(chuàng)建。
方法 | 含義 |
---|---|
createLikeAction | 創(chuàng)建點贊日志 |
createCancelAction | 創(chuàng)建取消點贊日志 |
2.4.3 小結(jié)
LikeLogger 所涉及對象包括:
對象 | 含義 | 建模方式 |
---|---|---|
LikeLogger | 贊日志 | 不變實體 |
ActionType | 操作類型 | 值對象 |
2.5 計數(shù)功能設(shè)計
最簡單的計數(shù)功能稀并,便是通過 SQL 對 Like 進行 “count group by” 來完成仅颇,但在高并發(fā)系統(tǒng)中,group by 是一大忌諱碘举。
從單一職責(zé)原則角度忘瓦,Like 承載了過多的責(zé)任,將統(tǒng)計功能強加到服務(wù)于業(yè)務(wù)的 Like 也非常不合適引颈。因此耕皮,我們對計數(shù)功能進行獨立的業(yè)務(wù)建模。 我們稱為 TargetCount蝙场。
2.5.1 識別建模類型
TargetCount 需要根據(jù)點贊和取消點贊對計數(shù)進行增減操作凌停。對于同一個 Target,需要持續(xù)跟蹤其數(shù)量變化售滤》D猓可見,TargetCount 是一個實體完箩。
2.5.2 實體建模
行為建模
TargetCount 的操作赐俗,主要有 incr 和 decr 兩個業(yè)務(wù)操作。在進行 count 更新時弊知,存在一個業(yè)務(wù)規(guī)則阻逮,及 count 不能小于零。
業(yè)務(wù)方法 | 含義 | 事件 | 業(yè)務(wù)規(guī)則 |
---|---|---|---|
incr | 增加點贊數(shù) | 無 | 無 |
decr | 減少點贊數(shù) | 無 | count 不能小于零 |
屬性建模
TargetCount 的屬性包括:
屬性 | 類型 | 含義 |
---|---|---|
target | Target | 點贊目標(biāo)對象 |
count | Long | 總的點贊數(shù) |
創(chuàng)建方式建模
TargetCount 的創(chuàng)建方式比較簡單秩彤,因此叔扼,采用靜態(tài)方法對其進行創(chuàng)建事哭。
2.5.3 小結(jié)
計數(shù)功能所涉及對象包括:
對象 | 含義 | 建模方式 |
---|---|---|
TargetCount | 點贊目標(biāo)計數(shù) | 實體 |
2.6 用例走查
用例走查,主要從用例角度瓜富,驗證當(dāng)前設(shè)計是否滿足業(yè)務(wù)需要鳍咱。
用例 | 支持方式 |
---|---|
點擊 | 由 Like 聚合的 click 方法進行支持 |
是否點贊 | 由 Like 聚合的 LikeStatus 進行支持 |
獲取點贊數(shù)量 | 由 TargetCount 的計數(shù)進行支持 |
LikeLogger 不直接服務(wù)于業(yè)務(wù),仍舊有很大意義食呻。
2.7 架構(gòu)設(shè)計
到現(xiàn)在流炕,整個系統(tǒng)的核心組件就設(shè)計完成了,接下來仅胞,我們需要將其組裝起來乳幸,以形成一個可用系統(tǒng)拭嫁。
這設(shè)計架構(gòu)前站楚,有幾個非功能性需求需要考慮逻淌。
- 點擊行為的高并發(fā)
- 獲取計數(shù)的高并發(fā)
- Like 與 Logger兆沙、 Count 的數(shù)據(jù)一致性
2.7.1 點擊行為的高并發(fā)
點擊行為是典型的寫操作材鹦,需要對寫操作進行優(yōu)化蔗喂。
對于寫操作優(yōu)化再姑,常見的策略包括:
- 數(shù)據(jù)散列胳岂。也就是我們常說的分庫分表编整,將寫操作分散到多個數(shù)據(jù)庫實例中,從而提升系統(tǒng)的整體吞吐乳丰。
- 先入隊列掌测,后臺消費。將用戶請求添加到隊列产园,啟動后臺線程汞斧,從隊列中獲取請求,并挨個消費什燕。這種策略的最大特點就是可以起到消峰的作用粘勒,將瞬間巨大的請求緩存起來,不會對后臺服務(wù)造成很大沖擊屎即。
對于一致性要求高的業(yè)務(wù)場景(比如支付)庙睡,數(shù)據(jù)散列是唯一解決方案;對于一致性要求不高的業(yè)務(wù)場景(比如咱們的點贊系統(tǒng))技俐,隊列方案是最佳解決方案乘陪。
在此,我們使用隊列方案來應(yīng)對點擊行為的高并發(fā)虽另。即用戶提交點擊請求并不會直接調(diào)用業(yè)務(wù)方法暂刘,而是將請求放入消息隊列;后臺消費線程從消息隊列中獲取請求捂刺,并調(diào)用業(yè)務(wù)方法執(zhí)行業(yè)務(wù)邏輯谣拣。
2.7.2 獲取計數(shù)的高并發(fā)
獲取計數(shù)是典型的讀操作募寨,需要對讀操作進行優(yōu)化。
對讀操作的優(yōu)化森缠,主要是使用緩存進行訪問加速拔鹰。我們使用 Redis 來加速訪問。
具體的操作如下:
- 首先從 Redis 中獲取計數(shù)信息贵涵,如果命中列肢,直接返回
- 如果 Redis 未命中,從數(shù)據(jù)庫中獲取計數(shù)宾茂,將結(jié)果添加到 Redis 中瓷马,然后返回
- 當(dāng)計數(shù)發(fā)生變化時,清理 Redis 的過期數(shù)據(jù)
2.7.3 Like 與 Logger跨晴、 Count 的數(shù)據(jù)一致性
在系統(tǒng)中 Like欧聘、Logger、Count 是三個聚合根端盆,我們需要保證三者的數(shù)據(jù)一致性怀骤。
系統(tǒng)的操作入口只有 Like 聚合,當(dāng) Like 發(fā)生變化時焕妙,Logger 和 Count 都需要跟著聯(lián)動起來蒋伦。Like 與 Count、Logger 具有很強的因果關(guān)系焚鹊,這也是領(lǐng)域事件建模的重要信號痕届。
在常規(guī)操作中,我們會在操作 Like 后寺旺,直接調(diào)用 Logger爷抓、Count 相關(guān)接口進行業(yè)務(wù)操作。但在 DDD 中阻塑,是絕對不允許的蓝撇,一個操作只能對一個聚合根進行處理,聚合根之間的同步只能基于事件通過最終一致性解決陈莽。
我們可以基于內(nèi)存總線和內(nèi)部事件渤昌,通過訂閱 Like 相關(guān)事件在內(nèi)存中完成與 Logger、Count 的業(yè)務(wù)同步走搁;也可以使用專用消息隊列和外部事件独柑,完成多個系統(tǒng)間的數(shù)據(jù)同步。
考慮到系統(tǒng)讀寫的擴展性私植,在此忌栅,我們使用消息隊列和外部事件完成數(shù)據(jù)一致性保障。
Like 在執(zhí)行完業(yè)務(wù)操作后曲稼,將內(nèi)部領(lǐng)域事件直接發(fā)布到內(nèi)存總線(EventBus)索绪,Exporter 組件從內(nèi)存總線中獲取領(lǐng)域事件湖员,將其轉(zhuǎn)換為外部事件,并發(fā)送到消息隊列中瑞驱。Consumer 組件負責(zé)從消息隊列中獲取事件娘摔,調(diào)用 Logger、Count 相關(guān)業(yè)務(wù)接口以完成業(yè)務(wù)操作唤反。
2.7.4 小結(jié)
綜上分析凳寺,我們的最終架構(gòu)如下: