AIGC标识 发布订阅模式(Pub/Sub Pattern)

发布订阅模式(Pub/Sub Pattern)

Go 特有/事件驱动模式

一、核心思想

发布订阅模式将消息的发送者(Publisher)接收者(Subscriber)解耦:发布者不需要知道谁在订阅,只管向主题(Topic)发送消息;订阅者只关心自己感兴趣的主题,不关心消息来自谁。

一对多通信的本质

  Publisher ──→ [ Topic ] ──→ Subscriber A
                     └────→ Subscriber B
                     └────→ Subscriber C

发布者发一条消息,所有订阅该主题的订阅者各自收到一份拷贝。这是与工作队列(一条消息只被一个 worker 消费)的本质区别。

生活中的类比

广播电台:电台只管发射信号,不关心有多少台收音机在接收。每台收音机调到同一频率都能收到同一节目。电台 ↔ 频率 ↔ 收音机 = Publisher ↔ Topic ↔ Subscriber。

二、观察者模式 vs 发布订阅模式

之前学过观察者模式,两者很像但有关键区别:

对比维度 观察者模式 发布订阅模式
通信方式 直接调用观察者的方法 通过中间件(Topic/EventBus)转发
耦合度 主题知道观察者存在 发布者订阅者互不感知
进程范围 通常是进程内 可进程内,也可跨进程(消息队列)
Go 典型实现 Subject 持有 []Observer EventBus map[topic][]chan

简单说:观察者是"打电话"(直接通知),发布订阅是"发广播"(中间人转发)。

三、Go 实现要点

1. 核心数据结构

type EventBus struct {
    mu          sync.RWMutex
    subscribers map[string][]chan Event  // topic → subscriber channels
}

2. 关键设计决策

决策点 选项 推荐
订阅者 channel 缓冲 无缓冲 / 有缓冲 有缓冲,防止慢消费者阻塞发布者
发布方式 同步发 / 异步发 异步 goroutine 发,避免慢消费者拖垮发布者
关闭策略 关闭所有订阅 channel / 仅停止接收 先停止接收新消息,再 close 所有 channel
取消订阅 删除 channel / 关闭 channel 从 slice 中删除 + close,防止 goroutine 泄漏
慢消费者处理 阻塞 / 丢弃 / 缓冲 视场景而定,推荐缓冲 + 丢弃策略

3. 发布时拷贝订阅者列表

发布消息时,不能直接遍历 subscribers[topic],因为遍历过程中其他 goroutine 可能修改列表。正确做法:先拷贝一份快照再遍历

四、Go 实现示例

完整的进程内 EventBus

package main

import (
	"fmt"
	"sync"
	"time"
)

// ============================================================
// Event 事件结构
// ============================================================

// Event 是总线上传递的事件
type Event struct {
	Topic   string
	Payload any
}

// ============================================================
// EventBus 事件总线
// ============================================================

// EventBus 管理所有主题和订阅者
type EventBus struct {
	mu          sync.RWMutex
	subscribers map[string][]chan Event
	closed      bool
}

// NewEventBus 创建事件总线
func NewEventBus() *EventBus {
	return &EventBus{
		subscribers: make(map[string][]chan Event),
	}
}

// Subscribe 订阅指定主题,返回一个接收 channel
// bufSize 为订阅者 channel 的缓冲大小
func (eb *EventBus) Subscribe(topic string, bufSize int) chan Event {
	eb.mu.Lock()
	defer eb.mu.Unlock()

	if eb.closed {
		ch := make(chan Event)
		close(ch)
		return ch
	}

	ch := make(chan Event, bufSize)
	eb.subscribers[topic] = append(eb.subscribers[topic], ch)
	return ch
}

// Unsubscribe 取消订阅并关闭 channel
func (eb *EventBus) Unsubscribe(topic string, ch chan Event) {
	eb.mu.Lock()
	defer eb.mu.Unlock()

	subs, ok := eb.subscribers[topic]
	if !ok {
		return
	}

	for i, sub := range subs {
		if sub == ch {
			// 从 slice 中删除
			eb.subscribers[topic] = append(subs[:i], subs[i+1:]...)
			close(ch)
			return
		}
	}
}

// Publish 异步发布事件到指定主题的所有订阅者
func (eb *EventBus) Publish(topic string, payload any) {
	eb.mu.RLock()
	defer eb.mu.RUnlock()

	if eb.closed {
		return
	}

	// 拷贝订阅者列表快照,避免遍历时被修改
	subs, ok := eb.subscribers[topic]
	if !ok {
		return
	}
	snapshot := make([]chan Event, len(subs))
	copy(snapshot, subs)

	evt := Event{Topic: topic, Payload: payload}

	// 异步发送给每个订阅者
	for _, ch := range snapshot {
		go func(c chan Event) {
			// 非阻塞发送:如果 channel 满了则丢弃
			// 生产环境可根据需求改为阻塞或重试
			select {
			case c <- evt:
			default:
				fmt.Printf("  ⚠ 订阅者缓冲已满,事件被丢弃: topic=%s\n", topic)
			}
		}(ch)
	}
}

// Close 关闭事件总线,关闭所有订阅者 channel
func (eb *EventBus) Close() {
	eb.mu.Lock()
	defer eb.mu.Unlock()

	if eb.closed {
		return
	}
	eb.closed = true

	for topic, subs := range eb.subscribers {
		for _, ch := range subs {
			close(ch)
		}
		delete(eb.subscribers, topic)
	}
}

// ============================================================
// 使用示例
// ============================================================

func main() {
	bus := NewEventBus()

	// 订阅 "user.created" 主题 —— 日志服务
	logCh := bus.Subscribe("user.created", 10)
	go func() {
		for evt := range logCh {
			fmt.Printf("[日志服务] 收到事件: %+v\n", evt.Payload)
		}
		fmt.Println("[日志服务] channel 已关闭,停止消费")
	}()

	// 订阅 "user.created" 主题 —— 邮件服务
	mailCh := bus.Subscribe("user.created", 10)
	go func() {
		for evt := range mailCh {
			fmt.Printf("[邮件服务] 发送欢迎邮件: %+v\n", evt.Payload)
		}
		fmt.Println("[邮件服务] channel 已关闭,停止消费")
	}()

	// 订阅 "order.paid" 主题 —— 库存服务
	inventoryCh := bus.Subscribe("order.paid", 10)
	go func() {
		for evt := range inventoryCh {
			fmt.Printf("[库存服务] 扣减库存: %+v\n", evt.Payload)
		}
		fmt.Println("[库存服务] channel 已关闭,停止消费")
	}()

	// 等待订阅者就绪
	time.Sleep(100 * time.Millisecond)

	// 发布事件
	fmt.Println("=== 发布 user.created 事件 ===")
	bus.Publish("user.created", map[string]any{
		"userID": 1,
		"name":   "张三",
		"email":  "zhangsan@example.com",
	})

	time.Sleep(200 * time.Millisecond)
	fmt.Println("\n=== 发布 order.paid 事件 ===")
	bus.Publish("order.paid", map[string]any{
		"orderID":  "ORD-2026-001",
		"amount":   199.00,
		"products": []string{"Go语言圣经", "设计模式"},
	})

	time.Sleep(200 * time.Millisecond)
	fmt.Println("\n=== 取消邮件服务订阅 ===")
	bus.Unsubscribe("user.created", mailCh)

	time.Sleep(100 * time.Millisecond)
	fmt.Println("\n=== 再次发布 user.created 事件 ===")
	bus.Publish("user.created", map[string]any{
		"userID": 2,
		"name":   "李四",
		"email":   "lisi@example.com",
	})

	time.Sleep(200 * time.Millisecond)
	fmt.Println("\n=== 关闭事件总线 ===")
	bus.Close()

	time.Sleep(200 * time.Millisecond)
	fmt.Println("\n程序结束")
}

运行结果

=== 发布 user.created 事件 ===
[邮件服务] 发送欢迎邮件: map[...
[日志服务] 收到事件: map[...

=== 发布 order.paid 事件 ===
[库存服务] 扣减库存: map[...

=== 取消邮件服务订阅 ===

=== 再次发布 user.created 事件 ===
[日志服务] 收到事件: map[...
[邮件服务] channel 已关闭,停止消费

=== 关闭事件总线 ===
[日志服务] channel 已关闭,停止消费
[库存服务] channel 已关闭,停止消费

程序结束

五、关键设计分析

1. 为什么发布时用 goroutine?

如果同步遍历发送,一个慢消费者会阻塞整个发布:

// ❌ 同步发送:慢消费者拖垮发布者
for _, ch := range subs {
    ch <- evt  // 如果某个 ch 缓冲满了,整个 Publish 阻塞
}

// ✅ 异步发送:互不影响
for _, ch := range subs {
    go func(c chan Event) {
        select { case c <- evt: default: /* 丢弃 */ }
    }(ch)
}

2. 为什么用 select + default(非阻塞发送)?

如果消费者处理太慢,channel 缓冲会满。阻塞发送会导致 goroutine 堆积甚至死锁。select + default 实现了背压降级——满则丢弃,保证发布者不被阻塞。

生产环境可选策略:

  • 丢弃:日志、监控等允许丢失的场景
  • 阻塞:必须保证不丢消息的核心业务
  • 更大的缓冲:突发流量场景

3. 为什么拷贝订阅者列表?

// ✅ 安全:先拷贝快照
snapshot := make([]chan Event, len(subs))
copy(snapshot, subs)
// 然后释放锁,遍历快照

// ❌ 危险:直接遍历 + goroutine 发送
// goroutine 执行时锁已释放,subs 可能已被修改
// 可能向已关闭的 channel 发送 → panic
for _, ch := range subs {
    go func() { ch <- evt }()
}

4. Unsubscribe 时必须 close channel

// ✅ 正确:删除 + close,让消费者的 range 循环退出
eb.subscribers[topic] = append(subs[:i], subs[i+1:]...)
close(ch)

// ❌ 只删不关:消费者的 for range 永远不退出 → goroutine 泄漏
eb.subscribers[topic] = append(subs[:i], subs[i+1:]...)

六、Go 标准库/生态中的 Pub/Sub

实现 特点 适用场景
sync.Map + channel 手写最简版 小型应用
container/list + goroutine 标准库链表 进程内事件
github.com/asaskevich/EventBus 第三方库 中型应用
Redis Pub/Sub 跨进程 分布式系统
Kafka / RabbitMQ 持久化 + 分区 大规模分布式
Go channels(原生) 最轻量 单进程简单场景

七、与观察者模式的关系回顾

本系列第 12 天学习了观察者模式,两者结构相似但解耦程度不同:

观察者模式: Subject ────直接调用───→ Observer.Update()
发布订阅:   Publisher ──→ EventBus ──→ Subscriber channel

观察者 = 打电话(双方知道对方)
发布订阅 = 发广播(通过中间人,双方互不认识)

Go 中由于 channel 天然支持"管道"通信,发布订阅模式的实现比 Java/C++ 更自然——channel 就是最好的"主题"中间件。

八、学习小结

发布订阅模式是构建松耦合系统的核心模式。核心要点:

  • EventBus 管理主题→订阅者映射,是解耦的关键
  • 异步发布 + 非阻塞发送 防止慢消费者拖垮发布者
  • 拷贝订阅者列表 再发布,避免并发修改导致的 panic
  • Unsubscribe 必须 close channel,否则 goroutine 泄漏
  • Go 的 channel 天然适合实现 Pub/Sub,比传统 OOP 语言更简洁
posted @ 2026-08-11 09:09  FfHUCisI  阅读(4)  评论(0)    收藏  举报