布-訂閱(Pub-Sub)系統(tǒng)設(shè)計(jì):從需求到 Go 源碼級(jí)實(shí)現(xiàn))
示例工程【免費(fèi)下載鏈接】awesome-low-level-designLearn Low Level Design (LLD) and prepare for interviews using free resources.項(xiàng)目地址https://gitcode.com/GitHub_Trending/aw/awesome-low-level-design點(diǎn)擊查看免費(fèi)下載導(dǎo)讀本文以 awesome-low-level-design 倉庫中 solutions/golang/pubsubsystem 的實(shí)現(xiàn)為主線完整講解發(fā)布-訂閱Publisher-Subscriber系統(tǒng)的核心需求、類/接口設(shè)計(jì)以及 Go 語言落地實(shí)現(xiàn)。你將掌握 Topic、Subscriber、Publisher 的角色劃分理解如何用sync.RWMutex保障并發(fā)安全并能運(yùn)行倉庫自帶的 pubsub_system_demo.go 演示多發(fā)布者、多訂閱者的實(shí)時(shí)消息投遞場(chǎng)景。本文適用于準(zhǔn)備系統(tǒng)設(shè)計(jì)/低層設(shè)計(jì)LLD面試、或需要快速搭建進(jìn)程內(nèi)消息總線的開發(fā)者。一、系統(tǒng)需求Requirements關(guān)聯(lián)文檔 README.md 定義了本系統(tǒng)需要滿足的 6 條核心需求它們是后續(xù)所有類設(shè)計(jì)的出發(fā)點(diǎn)面向主題發(fā)布系統(tǒng)應(yīng)允許發(fā)布者Publisher將消息發(fā)布到指定的主題Topic。按主題訂閱訂閱者Subscriber可以訂閱感興趣的主題并接收發(fā)布到這些主題上的消息。多對(duì)多支持系統(tǒng)應(yīng)支持多個(gè)發(fā)布者和多個(gè)訂閱者。實(shí)時(shí)投遞消息應(yīng)實(shí)時(shí)投遞給主題的所有訂閱者。并發(fā)安全系統(tǒng)應(yīng)處理并發(fā)訪問并確保線程安全。可擴(kuò)展與高效系統(tǒng)在消息投遞方面應(yīng)具備可擴(kuò)展性和高效性。這 6 條需求定義了一個(gè)進(jìn)程內(nèi)、基于主題解耦的廣播模型發(fā)布者與訂閱者互不感知只通過 Topic 這一中間媒介建立聯(lián)系。二、核心類、接口與枚舉設(shè)計(jì)關(guān)聯(lián)文檔給出了 7 個(gè)核心設(shè)計(jì)元素逐一展開如下Message消息表示一條可被發(fā)布、可被訂閱者接收的消息內(nèi)容為消息正文。對(duì)應(yīng) Go 實(shí)現(xiàn)見 message.go。Topic主題消息發(fā)布的目標(biāo)。維護(hù)一組訂閱者集合提供添加/移除訂閱者以及向所有訂閱者發(fā)布消息的方法。對(duì)應(yīng) topic.go。Subscriber訂閱者接口定義訂閱者的契約聲明onMessage方法該方法在訂閱者收到消息時(shí)被調(diào)用。對(duì)應(yīng) subscriber.go。PrintSubscriber打印訂閱者Subscriber接口的具體實(shí)現(xiàn)接收消息并打印到控制臺(tái)。對(duì)應(yīng) print_subscriber.go。Publisher發(fā)布者向指定主題發(fā)布消息。對(duì)應(yīng) publisher.go。PubSubSystem系統(tǒng)主類管理主題、訂閱者與消息發(fā)布。按文檔描述它使用ConcurrentHashMap存儲(chǔ)主題、用ExecutorService處理并發(fā)消息發(fā)布——這是 Java 版 PubSubService.java 的設(shè)計(jì)對(duì)應(yīng)倉庫內(nèi) UML 類圖 pubsubsystem-class-diagram.png而 Go 版本采用“Topic 自持讀寫鎖 按主題同步廣播”的等價(jià)方案將并發(fā)控制下沉到每個(gè) Topic。PubSubDemo演示類通過創(chuàng)建主題、訂閱者、發(fā)布者并發(fā)布消息來演示系統(tǒng)用法。Go 版對(duì)應(yīng) pubsub_system_demo.go 中的Run()入口。三、Go 源碼級(jí)實(shí)現(xiàn)剖析3.1 Message輕量消息載體type Message struct { Content string } func NewMessage(content string) *Message { return Message{Content: content} }message.go 僅保留Content字段并通過NewMessage構(gòu)造器統(tǒng)一創(chuàng)建。實(shí)際業(yè)務(wù)場(chǎng)景中可在此基礎(chǔ)上擴(kuò)展Timestamp、Topic、Headers等元數(shù)據(jù)Java 類圖中的Message即攜帶timestamp: Instant與payload: String兩個(gè)字段見 pubsubsystem-class-diagram.png。3.2 Subscriber 接口與 PrintSubscriber 實(shí)現(xiàn)type Subscriber interface { OnMessage(message *Message) }subscriber.go 定義了訂閱者的唯一契約OnMessage。得益于 Go 接口的鴨子類型任何實(shí)現(xiàn)該方法的類型都能成為訂閱者。倉庫提供的默認(rèn)實(shí)現(xiàn) print_subscriber.gotype PrintSubscriber struct { Name string } func NewPrintSubscriber(name string) *PrintSubscriber { return PrintSubscriber{Name: name} } func (ps *PrintSubscriber) OnMessage(message *Message) { fmt.Printf(Subscriber %s received message: %s\n, ps.Name, message.Content) }每個(gè)訂閱者通過Name區(qū)分身份OnMessage將消息打印到控制臺(tái)——這也正是需求 4“消息實(shí)時(shí)投遞給所有訂閱者”的可觀察落點(diǎn)。若需接入真實(shí)業(yè)務(wù)只需實(shí)現(xiàn)新的OnMessage邏輯如寫入隊(duì)列、調(diào)用下游 API。3.3 Topic訂閱注冊(cè)表 廣播中樞type Topic struct { Name string Subscribers map[Subscriber]struct{} mu sync.RWMutex } func NewTopic(name string) *Topic { return Topic{ Name: name, Subscribers: make(map[Subscriber]struct{}), } } func (t *Topic) AddSubscriber(subscriber Subscriber) { t.mu.Lock() defer t.mu.Unlock() t.Subscribers[subscriber] struct{}{} } func (t *Topic) RemoveSubscriber(subscriber Subscriber) { t.mu.Lock() defer t.mu.Unlock() delete(t.Subscribers, subscriber) } func (t *Topic) Publish(message *Message) { t.mu.RLock() defer t.mu.RUnlock() for subscriber : range t.Subscribers { subscriber.OnMessage(message) } }topic.go 是整套系統(tǒng)的核心包含三個(gè)設(shè)計(jì)要點(diǎn)訂閱集合用map[Subscriber]struct{}以接口值作鍵天然去重同一訂閱者重復(fù)AddSubscriber不會(huì)產(chǎn)生重復(fù)投遞struct{}作為空值占位零內(nèi)存開銷。讀寫鎖sync.RWMutex保證并發(fā)安全對(duì)應(yīng)需求 5AddSubscriber/RemoveSubscriber寫操作加寫鎖LockPublish讀操作加讀鎖RLock允許多個(gè)發(fā)布者并發(fā)廣播、同時(shí)阻塞寫入期間的集合變更。廣播采用“讀鎖快照式遍歷”Publish在持有 RLock 期間遍歷訂閱者并同步調(diào)用OnMessage保證發(fā)布瞬間的訂閱集合一致性代價(jià)是投遞是同步的、按調(diào)用者線程串行完成。3.4 Publisher受控發(fā)布type Publisher struct { Topics map[*Topic]struct{} } func NewPublisher() *Publisher { return Publisher{Topics: make(map[*Topic]struct{})} } func (p *Publisher) RegisterTopic(topic *Topic) { p.Topics[topic] struct{}{} } func (p *Publisher) Publish(topic *Topic, message *Message) { if _, exists : p.Topics[topic]; !exists { fmt.Printf(This publisher cant publish to topic: %s\n, topic.Name) return } topic.Publish(message) }publisher.go 引入了一個(gè)文檔中未展開、但源碼里明確實(shí)現(xiàn)的發(fā)布權(quán)限控制機(jī)制發(fā)布者必須先RegisterTopic(topic)登記主題才能調(diào)用Publish未登記的主題會(huì)被拒絕并打印提示。這一設(shè)計(jì)讓“發(fā)布者只允許向授權(quán)主題發(fā)布”成為顯式約束是對(duì)需求 1 的工程化加強(qiáng)。3.5 PubSubDemo完整的端到端演示func Run() { // Create topics topic1 : NewTopic(Topic1) topic2 : NewTopic(Topic2) // Create publishers publisher1 : NewPublisher() publisher2 : NewPublisher() // Create subscribers subscriber1 : NewPrintSubscriber(Subscriber1) subscriber2 : NewPrintSubscriber(Subscriber2) subscriber3 : NewPrintSubscriber(Subscriber3) publisher1.RegisterTopic(topic1) publisher2.RegisterTopic(topic2) // Subscribe to topics topic1.AddSubscriber(subscriber1) topic1.AddSubscriber(subscriber2) topic2.AddSubscriber(subscriber2) topic2.AddSubscriber(subscriber3) // Publish messages publisher1.Publish(topic1, NewMessage(Message1 for Topic1)) publisher1.Publish(topic1, NewMessage(Message2 for Topic1)) publisher2.Publish(topic2, NewMessage(Message1 for Topic2)) // Unsubscribe from a topic topic1.RemoveSubscriber(subscriber2) // Publish more messages publisher1.Publish(topic1, NewMessage(Message3 for Topic1)) publisher2.Publish(topic2, NewMessage(Message2 for Topic2)) }pubsub_system_demo.go 完整覆蓋了文檔演示類描述的所有動(dòng)作且特意構(gòu)造了跨主題訂閱subscriber2同時(shí)訂閱 Topic1 與 Topic2和中途退訂兩個(gè)邊界場(chǎng)景步驟動(dòng)作預(yù)期效果創(chuàng)建2 個(gè) Topic、2 個(gè) Publisher、3 個(gè) Subscriber多發(fā)布者/多訂閱者拓?fù)渚途w訂閱Topic1←{S1,S2}Topic2←{S2,S3}Subscriber2 同時(shí)訂閱兩個(gè)主題發(fā)布P1 發(fā) 2 條到 Topic1P2 發(fā) 1 條到 Topic2各主題訂閱者分別收到消息退訂RemoveSubscriber(subscriber2)Subscriber2 不再收到 Topic1 后續(xù)消息再發(fā)布P1 發(fā) Message3P2 發(fā) Message2驗(yàn)證退訂生效S2 只收到 Topic2 的新消息四、運(yùn)行方式Go 實(shí)現(xiàn)位于倉庫的 solutions/golang 模塊下模塊聲明見 go.modgo 1.23.2。運(yùn)行有兩種方式方式一通過統(tǒng)一入口運(yùn)行。倉庫根入口 solutions/golang/main.go 預(yù)留了所有項(xiàng)目的Run()調(diào)用點(diǎn)取消對(duì)應(yīng)行的注釋并執(zhí)行g(shù)o run .方式二直接在 pubsubsystem 包內(nèi)驗(yàn)證。將Run()改為臨時(shí)main()后執(zhí)行g(shù)o run pubsub_system_demo.go運(yùn)行后控制臺(tái)將依次輸出類似Subscriber Subscriber1 received message: Message1 for Topic1的日志可用于直接驗(yàn)證“實(shí)時(shí)投遞”“多訂閱者廣播”“退訂后不再接收”等需求是否成立。五、并發(fā)安全與擴(kuò)展性分析對(duì)照需求 5 與需求 6從源碼結(jié)構(gòu)可以做出如下分析并發(fā)安全Go 版以Topic.mu sync.RWMutex作為唯一同步原語覆蓋了訂閱集合的讀寫全部路徑無裸露的共享可變狀態(tài)Publisher.Topics在演示中僅在初始化階段寫入屬于線程啟動(dòng)前的配置數(shù)據(jù)。這是比文檔描述的 Java 方案ConcurrentHashMapExecutorService見 PubSubService.java更簡(jiǎn)潔的等價(jià)實(shí)現(xiàn)——Go 通過RWMutex把“讀多寫少”的廣播場(chǎng)景優(yōu)化為并發(fā)讀。可擴(kuò)展性當(dāng)前Publish是同步串行廣播訂閱者數(shù)量增加時(shí)投遞時(shí)延隨之線性增長(zhǎng)這是該實(shí)現(xiàn)的主要瓶頸點(diǎn)。如需擴(kuò)展可參考文檔所述 Java 方案的思路——將OnMessage調(diào)用提交到ExecutorService異步執(zhí)行、或?yàn)槊總€(gè) Topic 分配獨(dú)立 goroutine 隊(duì)列這些屬于基于原文檔設(shè)計(jì)意圖的演進(jìn)方向倉庫當(dāng)前 Go 實(shí)現(xiàn)并未包含。六、總結(jié)發(fā)布-訂閱系統(tǒng)的本質(zhì)是用 Topic 解耦“誰生產(chǎn)”與“誰消費(fèi)”發(fā)布者只認(rèn)主題訂閱者只收回調(diào)。本倉庫的 Go 實(shí)現(xiàn)用 6 個(gè)文件、約 130 行代碼以sync.RWMutex精確滿足了文檔列出的全部 6 條需求并通過RegisterTopic的登記制與RemoveSubscriber的退訂能力提供了超出需求清單的工程細(xì)節(jié)。閱讀源碼時(shí)建議按Message → Subscriber → Topic → Publisher → Demo的順序推進(jìn)即可完整復(fù)現(xiàn)一次“設(shè)計(jì)需求 → 類設(shè)計(jì) → 并發(fā)落地 → 端到端驗(yàn)證”的 LLD 實(shí)戰(zhàn)閉環(huán)。其他語言Java/C/C#/Python的對(duì)照實(shí)現(xiàn)可參考倉庫 problems/pub-sub-system.md 中列出的各語言目錄以及全局 UML 類圖 pubsubsystem-class-diagram.png。贊分享示例工程【免費(fèi)下載鏈接】awesome-low-level-designLearn Low Level Design (LLD) and prepare for interviews using free resources.項(xiàng)目地址https://gitcode.com/GitHub_Trending/aw/awesome-low-level-design點(diǎn)擊查看免費(fèi)下載相關(guān)推薦發(fā)布-訂閱Pub-Sub系統(tǒng)低層設(shè)計(jì)LLD實(shí)戰(zhàn)從需求到并發(fā)安全的 Java/Go 多語言實(shí)現(xiàn)發(fā)布 訂閱Pub Sub系統(tǒng)低層設(shè)計(jì)LLD實(shí)戰(zhàn)從需求到并發(fā)安全的 Java/Go 多語言實(shí)現(xiàn) 導(dǎo)讀 本文以 problems/pub sub syst示例工程基于 C 與 Java 雙實(shí)現(xiàn)剖析線程安全的發(fā)布-訂閱Pub-Sub系統(tǒng)設(shè)計(jì)基于 C 與 Java 雙實(shí)現(xiàn)剖析線程安全的發(fā)布 訂閱Pub Sub系統(tǒng)設(shè)計(jì) 發(fā)布 訂閱Pub Sub模式是解耦消息生產(chǎn)者與消費(fèi)者的核心架構(gòu)范式在實(shí)示例工程yuzu 模擬器Switch 游戲上 PC 的 30 分鐘上手與調(diào)優(yōu)指南yuzu 模擬器Switch 游戲上 PC 的 30 分鐘上手與調(diào)優(yōu)指南 yuzu 是一款用 C 編寫的開源 Switch 模擬器由 Citra 開發(fā)團(tuán)虛擬化桌面應(yīng)用圖形學(xué)上一篇XML Notepad智能編輯工作流突破XML處理效率瓶頸的全棧解決方案下一篇3個(gè)維度開源工具WarcraftHelper實(shí)現(xiàn)魔獸爭(zhēng)霸3兼容性優(yōu)化全指南創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考