文章目录

简介

发布订阅模型简写为 pub/sub 模型,消息生产者成为了发布者 publisher,消息消费者成为了订阅者 subscriber。传统生产者消费者模型是将消息发送到一个队列中,发布订阅模型是将消息发布给一个主题

与其把发布订阅者模式我更喜欢理解成发布订阅器模式,订阅者更像是一个订阅器,发布者中含有多个订阅器,每个订阅器有自己的一种订阅规则,同时订阅器中会存在多个不同的订阅信息,但是这些订阅信息都是满足该订阅器的订阅规则的

发布订阅模型

发布订阅支持包

package pubsub

import (
    "sync"
    "time"
)

/* ============== type ============== */

type (
	// subscriber 是一个管道,实际是存储了一批订阅信息的信道,即「一批订阅信息的合集」
    subscriber chan interface{}       
  	// topicFunc 是一个函数,实际就是一个「订阅规则」
    topicFunc  func(v interface{}) bool
)

/* ============== struct ============== */

// 发布者对象
type Publisher struct {
  	// 读写锁
    m           sync.RWMutex
	// 订阅器的订阅信息信道满载后的超时时延
    timeout     time.Duration
  
  	// 订阅器中可以容纳多少条订阅信息
    buffer      int                     
  	// 订阅器是一个键值对,key 是订阅信息信道,value 是订阅规则函数。发布者中含有无数个订阅器
    subscribers map[subscriber]topicFunc
}

/* ------------ 发布者新增一个订阅器 ------------ */

// 发布者新增一个新的订阅器,入参是新的订阅过滤器的过滤规则
func (p *Publisher) SubscribeTopic(topic topicFunc) chan interface{} {
    ch := make(chan interface{}, p.buffer)
	// 加读锁
    p.m.Lock()
  	// 这样一个订阅器就创建了。订阅器是一个键值对,key 为订阅信息信道,value 为订阅器规则
    p.subscribers[ch] = topic
    p.m.Unlock()
    return ch
}

// 发布者添加一个新的订阅器,新的订阅没有订阅规则
func (p *Publisher) Subscribe() chan interface{} {
	// nil 即没有订阅规则
    return p.SubscribeTopic(nil)
}

/* ------------ 发布者删除一个订阅器 ------------ */

// 删除一个订阅器
func (p *Publisher) Evict(sub chan interface{}) {
  	// 因为涉及修改发布者 p 的内容,所以加写锁
    p.m.Lock()
    defer p.m.Unlock()
  
	// 删除一个订阅器
    delete(p.subscribers, sub)
    close(sub)
}

/* ------------ 发布者发布信息 ------------ */
// 发布者发布一个信息去匹配发布者结构体中的每一个订阅器
func (p *Publisher) Publish(v interface{}) {
  	// 加写锁防止多协程其他发布者操作同样的一个订阅器发成异常
    p.m.RLock()
    defer p.m.RUnlock()

  	// sync.WaitGroup 保证当发布者内每一个订阅器都和订阅信息 v 做完匹配之后 Publish 发布函数才能结束,否则卡在 Wait 函数处
    var wg sync.WaitGroup
    for sub, topic := range p.subscribers {
        wg.Add(1)
      	// 每将一个订阅器和订阅信息 v 做匹配就开启一个新的协程
        go p.sendTopic(sub, topic, v, &wg)
    }
    wg.Wait()
}

// 订阅器匹配订阅信息的核心内容,订阅器的过滤规则筛掉了发布过来的订阅信息就直接 return,如果没有筛掉就会考虑给订阅器添加这个订阅信息,如果在添加时发现该订阅器的订阅信息信道被存满了,就会进行超时等待
func (p *Publisher) sendTopic(
    sub subscriber, topic topicFunc, v interface{}, wg *sync.WaitGroup,
) {
    defer wg.Done()
	// 如果订阅信息不满足订阅器的过滤器规则,即被过滤器筛掉了,就直接 return
    if topic != nil && !topic(v) {
        return
    }

  	// 如果订阅信息没有被过滤器筛掉
    select {
    // 将订阅信息 v 存入订阅器的订阅信息信道内
    case sub <- v:
    // 如果该订阅器的订阅信息信道已满,则使用超时等待
    case <-time.After(p.timeout):
    }
}

/* ------------ 发布者清空其中的每个订阅器 ------------ */
// 完全清空发布者中的每一个订阅器
func (p *Publisher) Close() {
    // 因为是修改(删除)订阅器,所以需要加写锁
    p.m.Lock()
    defer p.m.Unlock()

    for sub := range p.subscribers {
        delete(p.subscribers, sub)
        close(sub)
    }
}

/* ============== create ============== */
// 创建一个发布者,里头主要包含了发布者的读写锁以及无数个订阅器
func NewPublisher(publishTimeout time.Duration, buffer int) *Publisher {
    return &Publisher{
		// 订阅器中可以容纳多少条订阅信息
        buffer: buffer,
     	// 订阅器的订阅信息信道满载后的超时时延
        timeout: publishTimeout,
      	// 订阅器是一个键值对,key 是订阅信息信道,value 是订阅规则函数。发布者中含有无数个订阅器
        subscribers: make(map[subscriber]topicFunc),
    }
}

下面是发布订阅 main 主流程

import "pubsub"

func main() {
  	/* ============== 创建发布者 ============== */
  
  	// 创建发布者,其中规定了超时时延以及其中每一个订阅器的订阅信息满载数量
	p := pubsub.NewPublisher(100 * time.Millisecond, 10) 
  	// main 最后清空发布者 p 中所有的订阅器
	defer p.Close()
  
   	/* ============== 发布者新增订阅器 ============== */
  
	// 发布者 p 新增一个订阅器,并且该订阅器中没有任何过滤规则,返回的是该订阅器的订阅信息信道
  	allMsg := p.Subscribe()
  	// 发布者 p 新增一个订阅器,并且该订阅器中有一个需要匹配字符串“golang”的订阅规则,返回的是该订阅器的订阅信息信道
    golang := p.SubscribeTopic(func(v interface{}) bool {
        if s, ok := v.(string); ok {
			return strings.Contains(s, "golang")
    	}
    	return false
    })
	// todo: 后面还可以添加多个订阅器
  
   	/* ============== 发布者发布信息 ============== */
  
  	// 开启一个协程发布,发布者发布一个订阅信息,这个订阅信息会和发布者内的所有订阅器去做匹配
  	go p.Publish("hello,  world!")
	// 开启一个协程发布,发布者发布一个订阅信息,这个订阅信息会和发布者内的所有订阅器去做匹配
  	go p.Publish("hello, golang!")
	// todo: 后面还可以发布多个订阅信息
  
  	/* ============== 打印发布者中订阅器的所有订阅信息 ============== */
  	
  	// 从信道中取数据,避免信道满载,打印无过滤规则的订阅器中的所有订阅信息,
  	go func() {
        for  msg := range allMsg {
            fmt.Println("allMsg:", msg)
        }
    }()
  	// 从信道中取数据,避免信道满载,打印有过滤规则("golang")的订阅器中的所有订阅信息
  	go func() {
        for  msg := range golangMsg {
            fmt.Println("golangMsg:", msg)
        }
    }()
  
   	/* ============== 等待时常 ============== */
  
  	// 2s 延时
	time.Sleep(2 * time.Second)  
}