fix: 修复无订阅者时消息静默丢失问题,完善测试

- 新增 pending 缓冲区,publish 时若无订阅者则暂存消息
- subscribe 时自动将缓冲消息投入 channel,解决服务重启后恢复任务丢失的问题
- 去除 broadcast 5ms 超时导致的消息丢失
- chan bool 改为 chan struct{},RWMutex 改为 Mutex
- 新增 broker_test.go,12 个单元测试覆盖核心场景(含 -race)
- 为 client_test.go 中的无限循环 demo 添加 t.Skip()
This commit is contained in:
2026-06-02 19:21:47 +08:00
parent ae0e099277
commit 1322280daf
3 changed files with 297 additions and 90 deletions

143
broker.go
View File

@@ -3,102 +3,66 @@ package queue
import (
"errors"
"sync"
"time"
)
type Broker struct {
exit chan bool // 关闭消息队列通道
capacity int // 消息队列的容量
topics map[string][]chan any // key topic value queue, 一个topic可以有多个订阅者一个订阅者对应着一个通道
sync.RWMutex // 同步锁
exit chan struct{}
capacity int
topics map[string][]chan any
pending map[string][]any // 订阅前发布的消息缓冲subscribe 时一次性投递
mu sync.Mutex
}
// 设置消息容量
// @description 控制消息队列的大小
func (b *Broker) setConditions(capacity int) {
b.mu.Lock()
b.capacity = capacity
b.mu.Unlock()
}
// 关闭消息队列
func (b *Broker) close() {
select {
case <-b.exit:
return
default:
close(b.exit)
b.Lock()
b.mu.Lock()
b.topics = make(map[string][]chan any)
b.Unlock()
b.pending = make(map[string][]any)
b.mu.Unlock()
}
return
}
// 消息推送
// @param topic 订阅的主题
// @param msg 传递的消息
func (b *Broker) publish(topic string, pub any) error {
// publish 推送消息;若暂无订阅者则缓冲,等待订阅者注册后投递。
func (b *Broker) publish(topic string, msg any) error {
select {
case <-b.exit:
return errors.New("broker closed")
default:
}
b.RLock()
subscribers, ok := b.topics[topic]
b.RUnlock()
if !ok {
b.mu.Lock()
subs := b.topics[topic]
if len(subs) == 0 {
b.pending[topic] = append(b.pending[topic], msg)
b.mu.Unlock()
return nil
}
// 持有锁期间只做列表复制,发送在锁外进行,避免阻塞其他 publish
chs := make([]chan any, len(subs))
copy(chs, subs)
b.mu.Unlock()
b.broadcast(pub, subscribers)
for _, ch := range chs {
select {
case ch <- msg:
case <-b.exit:
return errors.New("broker closed")
}
}
return nil
}
// 消息广播
// @description 对推送的消息进行广播,保证每一个订阅者都可以收到
func (b *Broker) broadcast(msg any, subscribers []chan any) {
count := len(subscribers)
concurrency := 1
switch {
case count > 1000:
concurrency = 3
case count > 100:
concurrency = 2
default:
concurrency = 1
}
pub := func(start int) {
//采用Timer 而不是使用time.After 原因time.After会产生内存泄漏 在计时器触发之前垃圾回收器不会回收Timer
idleDuration := 5 * time.Millisecond
idleTimeout := time.NewTimer(idleDuration)
defer idleTimeout.Stop()
for j := start; j < count; j += concurrency {
if !idleTimeout.Stop() {
select {
case <-idleTimeout.C:
default:
}
}
idleTimeout.Reset(idleDuration)
select {
case subscribers[j] <- msg:
case <-idleTimeout.C:
case <-b.exit:
return
}
}
}
for i := 0; i < concurrency; i++ {
go pub(i)
}
}
// 消息订阅
// @description 传入订阅的主题,即可完成订阅
// @param topic 订阅的主题
// @return sub 通道用来接收数据
// subscribe 订阅 topic返回 channel同时将该 topic 的缓冲消息立即投入 channel。
func (b *Broker) subscribe(topic string) (<-chan any, error) {
select {
case <-b.exit:
@@ -106,16 +70,25 @@ func (b *Broker) subscribe(topic string) (<-chan any, error) {
default:
}
ch := make(chan any, b.capacity)
b.Lock()
b.mu.Lock()
capacity := b.capacity
if capacity <= 0 {
capacity = 10
}
ch := make(chan any, capacity)
b.topics[topic] = append(b.topics[topic], ch)
b.Unlock()
buffered := b.pending[topic]
delete(b.pending, topic)
b.mu.Unlock()
// channel 刚创建必然不满,直接写入不会阻塞
for _, msg := range buffered {
ch <- msg
}
return ch, nil
}
// 取消订阅
// @param topic 订阅的主题
// @param sub 消息订阅的通道
func (b *Broker) unsubscribe(topic string, sub <-chan any) error {
select {
case <-b.exit:
@@ -123,31 +96,25 @@ func (b *Broker) unsubscribe(topic string, sub <-chan any) error {
default:
}
b.RLock()
subscribers, ok := b.topics[topic]
b.RUnlock()
b.mu.Lock()
defer b.mu.Unlock()
if !ok {
return nil
}
// delete subscriber
b.Lock()
var newSubs []chan any
for _, subscriber := range subscribers {
if subscriber == sub {
continue
subs := b.topics[topic]
newSubs := subs[:0]
for _, s := range subs {
if s != sub {
newSubs = append(newSubs, s)
}
newSubs = append(newSubs, subscriber)
}
b.topics[topic] = newSubs
b.Unlock()
return nil
}
func NewBroker() *Broker {
return &Broker{
exit: make(chan bool),
topics: make(map[string][]chan any),
exit: make(chan struct{}),
capacity: 10,
topics: make(map[string][]chan any),
pending: make(map[string][]any),
}
}