🎓 學習心得分享
本文是我在學習 UDEMY 後端工程課程後的心得整理,結合自己在遊戲業的實戰經驗,用更貼近遊戲開發的角度來解釋發布訂閱模式。

還記得 YouTube 上傳影片的神奇過程嗎?你上傳一個影片,幾分鐘後就有 1080p、720p、4K 等各種畫質版本。這背後其實隱藏著一個超強大的架構模式:發布訂閱(Pub/Sub)!今天我們來看看這個讓大型系統能夠優雅擴展的核心技術。

📋 文章目錄


什麼是發布訂閱模式

發布訂閱模式就像現實世界的報社和訂戶關係:

傳統做法(Request-Response): 你想要新聞,就得親自跑去報社問「有新聞嗎?」拿到報紙後,你還得告訴朋友們。如果 100 個人都想要新聞,報社就得回答 100 次。

Pub/Sub 做法

  • 報社(Publisher)寫好新聞就「發布」到中央郵局
  • 想看新聞的人(Subscriber)向郵局「訂閱」
  • 有新聞時,郵局自動送到每個訂戶家裡
  • 報社不用知道有誰在看,讀者也不用知道是誰寫的

這個「中央郵局」就是 Message Broker(訊息代理)!

Request-Response 的擴展性問題

讓我們用 YouTube 影片處理來理解這個問題。

🎬 YouTube 的複雜流程

當你上傳一部影片時,背後需要:

  1. 上傳服務:接收影片檔案
  2. 壓縮服務:壓縮影片減少檔案大小
  3. 格式轉換服務:產生 480p、720p、1080p、4K 版本
  4. 通知服務:通知訂閱者有新影片
  5. 版權檢查服務:檢查是否有版權問題

😰 用 Request-Response 的災難

如果用傳統的 Request-Response 架構:

用戶上傳 → 上傳服務 → 壓縮服務 → 格式轉換 → 通知服務 → 版權檢查 → 完成

這樣會造成:

  • 用戶要等很久:所有步驟都完成才能回應
  • 一個環節掛了全部停止:版權檢查服務壞了,整個流程卡住
  • 難以擴展:想加新功能(比如自動字幕)就得修改整個流程
  • 高度耦合:每個服務都要知道下一個服務是誰

想像這就像工廠的生產線,如果某個工位停工,整條線都得停!

Pub/Sub 如何解救複雜系統

🎯 解耦合的魔法

Pub/Sub 把複雜的「生產線」變成了「市集模式」:

// 上傳完成後,發布事件
uploadService.publish('video_uploaded', {
    videoId: 'abc123',
    originalFile: 'video.mp4',
    userId: 'user456'
});

// 各個服務獨立訂閱感興趣的事件
compressionService.subscribe('video_uploaded', compressVideo);
formatService.subscribe('video_uploaded', convertToMultipleFormats);
notificationService.subscribe('video_uploaded', notifySubscribers);
copyrightService.subscribe('video_uploaded', checkCopyright);

🏪 市集 vs 生產線

生產線模式(Request-Response)

  • A 做完 → B 做 → C 做 → D 做
  • 任何一個環節出問題,全部停擺
  • 要加新步驟,得重新設計整條線

市集模式(Pub/Sub)

  • 有人喊「新貨到了!」
  • 各個攤販自己決定要不要處理
  • 新攤販隨時可以加入市集
  • 某個攤販休息不會影響其他人

遊戲中的 Pub/Sub 應用

🎮 玩家升級事件系統

想像玩家在遊戲中升級了:

// 玩家升級時發布事件
player.levelUp();
eventBus.publish('player_level_up', {
    playerId: player.id,
    newLevel: player.level,
    oldLevel: player.level - 1
});

很多系統都對這個事件感興趣:

成就系統: 「咦,玩家升到 50 級了,給他『半百英雄』成就!」

通知系統: 「推送恭喜訊息給玩家朋友們」

排行榜系統: 「更新等級排行榜」

商城系統: 「解鎖新的裝備商店」

統計系統: 「記錄玩家升級數據」

每個系統都獨立運作,不會互相影響。新增功能也很簡單,只要訂閱事件就好!

🏆 公會戰事件系統

公會戰開始時:

eventBus.publish('guild_war_started', {
    guildA: 'DragonSlayers',
    guildB: 'IceWarriors',
    battleId: 'war_123'
});

各種系統立即響應:

  • 直播系統:開始錄製戰鬥過程
  • 押注系統:開放玩家下注
  • 通知系統:推送給所有公會成員
  • 統計系統:開始收集戰鬥數據

🎁 遊戲內購買事件

玩家買了新裝備:

eventBus.publish('item_purchased', {
    playerId: 'player123',
    itemId: 'legendary_sword',
    price: 999,
    currency: 'gems'
});

觸發連鎖反應:

  • 庫存系統:扣除物品數量
  • 錢包系統:扣除玩家貨幣
  • 成就系統:檢查購買相關成就
  • 推薦系統:更新玩家偏好資料
  • 客服系統:記錄交易日誌

實戰案例:遊戲事件系統

🎯 簡單的事件匯流排

class GameEventBus {
    constructor() {
        this.subscribers = {};
    }
    
    // 訂閱事件
    subscribe(eventType, callback) {
        if (!this.subscribers[eventType]) {
            this.subscribers[eventType] = [];
        }
        this.subscribers[eventType].push(callback);
    }
    
    // 發布事件
    publish(eventType, data) {
        if (this.subscribers[eventType]) {
            this.subscribers[eventType].forEach(callback => {
                // 異步執行,避免阻塞
                setTimeout(() => callback(data), 0);
            });
        }
    }
}

// 建立全域事件匯流排
const gameEvents = new GameEventBus();

🏹 戰鬥系統範例

// 戰鬥系統發布攻擊事件
function playerAttack(attacker, target, damage) {
    // 處理攻擊邏輯
    target.hp -= damage;
    
    // 發布事件,讓其他系統知道
    gameEvents.publish('player_attacked', {
        attackerId: attacker.id,
        targetId: target.id,
        damage: damage,
        targetRemainingHp: target.hp
    });
}

// UI 系統訂閱事件更新血條
gameEvents.subscribe('player_attacked', (data) => {
    updateHealthBar(data.targetId, data.targetRemainingHp);
    showDamageNumber(data.damage);
});

// 音效系統訂閱事件播放聲音
gameEvents.subscribe('player_attacked', (data) => {
    playAttackSound();
    if (data.targetRemainingHp <= 0) {
        playDeathSound();
    }
});

// 成就系統訂閱事件檢查成就
gameEvents.subscribe('player_attacked', (data) => {
    checkCombatAchievements(data.attackerId, data.damage);
});

訊息佇列的實際應用

🐰 RabbitMQ 範例

在實際的遊戲後端中,通常會使用專業的訊息佇列系統:

// 發布玩家註冊事件
function publishPlayerRegistered(playerData) {
    rabbitMQ.publish('player.registered', playerData);
}

// 各個服務訂閱事件
emailService.subscribe('player.registered', sendWelcomeEmail);
analyticsService.subscribe('player.registered', trackNewUser);
rewardService.subscribe('player.registered', giveNewPlayerReward);

📊 事件的可靠傳遞

專業的訊息佇列提供:

  • 持久化:事件不會因為服務重啟而遺失
  • 重試機制:處理失敗時自動重試
  • 負載均衡:多個服務實例分擔工作
  • 監控:追蹤事件處理狀況

優勢與挑戰

✅ Pub/Sub 的強大優勢

解耦合: 各個服務不需要知道彼此的存在,只要知道事件格式就好。就像你訂閱 Netflix,不需要知道製作團隊是誰。

易於擴展: 想加新功能?只要訂閱相關事件就好!想像餐廳要加外送服務,只需要「訂閱」新訂單事件。

容錯性強: 某個服務掛了不會影響其他服務。就像某個 YouTube 用戶的裝置壞了,不會影響其他人看影片。

異步處理: 發布者發布事件後立即返回,不用等待所有處理完成。

❌ 需要面對的挑戰

複雜性增加: 系統變得更複雜,需要管理訊息代理、監控事件流等。

除錯困難: 出問題時很難追蹤事件的流向。就像郵件系統,你不知道信件在哪個環節出了問題。

順序問題: 事件可能不按順序到達。想像同時發布「玩家升級」和「玩家裝備武器」,處理順序可能會錯亂。

重複處理: 同一個事件可能被處理多次。需要設計「冪等性」確保重複執行不會出問題。

技術選擇指南

🎯 何時使用 Pub/Sub

系統複雜度高: 多個服務需要對同一事件做出反應

需要高擴展性: 經常需要加新功能或服務

異步處理需求: 不需要立即回應,可以背景處理

解耦合需求: 希望各服務獨立開發和部署

🛑 何時不適合 Pub/Sub

簡單系統: 只有少數幾個服務的小型系統

需要立即回應: 像遊戲戰鬥這種需要即時反饋的功能

強順序要求: 必須按照特定順序處理的業務邏輯

團隊經驗不足: 團隊不熟悉分散式系統和異步處理

漸進式導入策略

📈 從簡單開始

第一階段:內部事件匯流排 在單一應用內使用事件模式,熟悉概念

第二階段:簡單訊息佇列 引入 Redis Pub/Sub 或類似的輕量級解決方案

第三階段:專業訊息系統 根據需求選擇 RabbitMQ、Apache Kafka 等

第四階段:微服務架構 完全基於事件驅動的分散式系統

小結

發布訂閱模式就像是軟體世界的「郵政系統」,讓複雜的系統能夠優雅地協作。雖然它增加了一些複雜性,但對於需要擴展的遊戲系統來說,這是一個強大的武器。

記住:不是所有問題都需要 Pub/Sub 來解決,但當你的系統開始變得複雜時,它會是你最好的朋友!

🚀 想深入學習微服務和事件驅動架構?

這門 UDEMY 後端課程在 Pub/Sub 和微服務架構方面講得很深入。從基礎概念到實際的 RabbitMQ、Kafka 應用,還有大型系統的設計模式。

特別是關於訊息可靠性、事件溯源等進階主題,對想要設計可擴展遊戲架構的工程師來說非常有價值。課程中的實戰案例幫我解決了很多分散式系統設計的難題!

👉 點此查看完整課程內容

下次我們來聊多工與連線池,看看如何讓遊戲伺服器效能飆升!


💭 你有遇過因為系統耦合太緊密而難以擴展的問題嗎?

🛠️ 動手試試:設計一個簡單的遊戲事件系統,體驗 Pub/Sub 的威力!


⚠️ 免責聲明:本文為個人學習心得分享,內容基於課程學習後的理解和實務經驗整理。如需獲得完整且準確的技術知識,建議參考原始課程內容。

標籤:#發布訂閱 #PubSub #事件驅動架構 #微服務 #遊戲後端 #後端架構 #訊息佇列 #RabbitMQ #解耦合 #可擴展架構 #事件系統 #遊戲開發 #後端開發 #分散式系統 #異步處理 #UDEMY課程心得 #後端工程師 #系統設計 #架構模式 #遊戲架構

創作者介紹
創作者 傑克淺談遊戲邏輯 的頭像
傑克的遊戲宇宙

傑克淺談遊戲邏輯

傑克的遊戲宇宙 發表在 痞客邦 留言(0) 人氣( 10 )