前言
看过很多go项目的源码,但是自己没真正写过。自己一直是做web项目,一般的web的项目java已经非常非常够用了并且非常非常好用。go感觉还是比较适合做中间件,但是自己的眼界有限,就不知道能写神马了,所以就一直看别人源码没自己去写过。然后现在试着做一个简单的消息队列,第一次写go,碰到很多问题,也有很多疑问想得到答案,希望巨佬们能帮忙解惑。
代码在这里
https://github.com/woshidajj/woshidajj-mq
需求
消息队列,差不多就是用户能订阅某个topic,服务给某个topic,发布一条消息,这个topic的所有用户都能收到。用户收到一条消息,他自己知道怎么去处理他。用户不想订阅了,就取消订阅。
设计
1.消息队列服务(msgqueue)维护很多个topic,每个topic应该对应一个订阅者列表(topiclist),订阅者列表里面都是订阅者
2.消息队列服务(msgqueue),提供订阅,取消订阅,发布消息的功能
3.每个订阅者列表(topiclist),都有一个publisher,消息队列服务(msgqueue)发布消息时,通过发送消息给publisher,进行发布
4.订阅者列表(topiclist)的publisher,会直接把消息发送给订阅者(subscriber),订阅者异步处理
5.这里的订阅者(subscriber),发布者(publisher),比较像actor模型里的actor,每个actor有自己的mailbox,消息队列服务(msgqueue),往publisher的mailbox发消息,publisher从他的mailbox里取出消息,然后给他的subscriber的mailbox发消息,然后subscribe会从mailbox里取消息自己处理
6.消息队列服务(msgqueue)也可以弄成一个actor,给它发消息,然后订阅,取消订阅吗?我觉得订阅是不行,因为订阅需要马上反馈给客户。然后取消订阅,我觉得可以,我想到一个场景,就是突然断网,会有大量订阅者取消订阅,造成处理压力,取消订阅因为要操作切片,一般都要锁,一个一个取消,如果取消订阅也用actor模型,先发送消息,后续处理,就不会有这方面的处理压力(但是我没实现)
代码
1.actor
(1)每个actor都有一个Mailbox,一个ActorWork,Mailbox是一个chan,发送消息就是给chan塞入消息
(2)每个actor维持着一个协程runActor,他会监听Mailbox的消息,然后取消息调用actorworker的work方法处理
type ActorWorker interface {
Work(interface{}) error
}
type actor struct {
id int
status int
Mailbox chan interface{}
ExitC chan struct{}
once sync.Once
worker ActorWorker
}
func NewActor(mailboxLen int, worker ActorWorker) *actor {
mailbox := make(chan interface{}, mailboxLen)
exitC := make(chan struct{})
a := &actor{Mailbox: mailbox, ExitC: exitC, worker: worker}
return a
}
func (a *actor) runActor() {
for {
select {
case msg := <-a.Mailbox:
a.worker.Work(msg)
case <-a.ExitC:
fmt.Printf("actor(%p) is closed \n", a)
return
}
}
}
func (a *actor) close() {
// 把状态设置为停止,不需要并发安全,重复关闭不会有问题
a.status = 1
// 只关闭一次
a.once.Do(func() {
close(a.ExitC)
})
}
2.一个简单的ActorWorker,HelloActorWorker
type HelloActorWorker struct {
}
func (w *HelloActorWorker) Work(m interface{}) error {
v, ok := m.(string)
if ok {
fmt.Printf("HELLO - %s \n", v)
} else {
fmt.Println("INVALID M")
}
return nil
}
3.测试一下给这个actor发送消息
func TestHelloActor(t *testing.T) {
a := NewActor(10, &HelloActorWorker{})
go a.runActor()
time.Sleep(time.Second * 1)
go func() {
for i := 0; i < 20; i++ {
a.Mailbox <- fmt.Sprintf("haha_%d", i)
}
}()
time.Sleep(time.Second * 5)
}
4.PublisherActorWorker
(1)发布者publisher的actorWorker,他接收PublishMsg消息,包含了一个切片subers,是订阅者的列表,msg是订阅者会接收到的消息。
(2)发布消息的时候,他会对每个订阅者单独开启一个协程,给他们的Mailbox发送消息,5秒超时(因为对方的Mailbox可能满了或者其他原因导致发送不了),超时取消操作
type PublishMsg struct {
msg interface{} // 消息
subers []*actor // 订阅者的列表
}
type PublisherActorWorker struct {
}
func (w *PublisherActorWorker) Work(m interface{}) error {
v, ok := m.(PublishMsg)
if ok {
msgHandleDuration := time.Second * 5
// 对每个订阅者,开启一个协程往他mailbox发送消息
for _, suber := range v.subers {
go func(s *actor, m interface{}) {
// 超时退出协程
idleDuration := msgHandleDuration
idleDelay := time.NewTimer(idleDuration)
defer idleDelay.Stop()
select {
case s.Mailbox <- m:
fmt.Printf("publish to : %p \n", s)
case <-idleDelay.C:
return
}
}(suber, v.msg)
}
} else {
fmt.Println("INVALID M")
}
return nil
}
5.测试一下用一个publisher处理消息
func TestPublishActor(t *testing.T) {
a1 := NewActor(10, &HelloActorWorker{})
go a1.runActor()
a2 := NewActor(10, &HelloActorWorker{})
go a2.runActor()
a3 := NewActor(10, &HelloActorWorker{})
go a3.runActor()
a4 := NewActor(10, &HelloActorWorker{})
go a4.runActor()
subers := []*actor{a1, a2, a3, a4}
p := NewActor(10, &PublisherActorWorker{})
go p.runActor()
time.Sleep(time.Second * 1)
go func() {
for i := 0; i < 20; i++ {
msg := PublishMsg{msg: fmt.Sprintf("haha_%d", i), subers: subers}
p.Mailbox <- msg
}
}()
time.Sleep(time.Second * 20)
}
6.toplist
(1)有一个切片subers是他的订阅者的actor
(2)publisher是这个toplist的一个发布消息的actor,上层的消息队列服务如果要向这个topic发布消息,会直接向publisher的Mailbox塞入消息
(3)订阅的时候要注意,如果是第一个订阅者,要开启publisher的协程
(4)取消订阅的时候要注意,如果是最后一个订阅者,要关闭publisher的协程
(5)这里的topiclist不会主动去关闭订阅者的协程,主要是考虑toplist应该负责关闭自己的资源
type topiclist struct {
topic string
subers []*actor // 订阅者的列表
sync.RWMutex // 订阅和取消订阅的时候应该锁住
publisher *actor // publisherActor
}
func newTopiclist(topic string) *topiclist {
subers := make([]*actor, 0)
tl := &topiclist{topic: topic, subers: subers}
return tl
}
// 订阅,只负责添加actor
func (tlist *topiclist) subscribe(suber *actor) {
tlist.Lock()
defer tlist.Unlock()
// 第一个订阅者,需要配备publisher
if len(tlist.subers) == 0 {
tlist.publisher = NewActor(10, &PublisherActorWorker{})
go tlist.publisher.runActor()
}
tlist.subers = append(tlist.subers, suber)
}
// 取消订阅,只负责从订阅列表移除,当订阅者列表为0,关闭publisher的协程
func (tlist *topiclist) unsubscribe(suber *actor) {
tlist.Lock()
defer tlist.Unlock()
// 先关闭协程
suber.close()
tempList := make([]*actor, 0)
found := false
for _, suberInList := range tlist.subers {
if suberInList == suber {
found = true
continue
}
tempList = append(tempList, suberInList)
}
if found {
tlist.subers = tempList
}
// 取消订阅以后列表为空,要关闭publisher的协程,下次再有订阅会重新new publisher然后启动协程
if len(tlist.subers) == 0 {
tlist.publisher.close()
}
}
7.msqueue
(1)订阅列表放在一个sync.Map里,key是topic,value的topiclist
(2)订阅的时候要调用topiclist的subscribe方法把订阅者的actor放到toplist里,然后要负责启动订阅者actor的协程,等待消息发送过来处理
(3)取消订阅二话不说先停止订阅者actor的协程(我很怕泄露,所以有很多关闭协程的地方,其实这么做好不好?还是应该怎么做),然后再从tooiclist移出这个actor
(4)发布消息比较简单,应该要同步反馈给用户发布成功还是不成功,就没用协程,超时5秒
const (
publishDuration = time.Second * 5
)
type MsgQueue struct {
exitC chan bool
topics sync.Map
}
func NewMsgQueue() (*MsgQueue, error) {
exitC := make(chan bool)
var topics sync.Map
mq := &MsgQueue{exitC: exitC, topics: topics}
return mq, nil
}
func (mq *MsgQueue) Subscribe(topic string, suber *actor) (*actor, error) {
select {
// wait for exit signal
case <-mq.exitC:
return nil, errors.New("MQ EXIT")
default:
}
fmt.Printf("(%p) subscirbe %s \n", suber, topic)
llist, _ := mq.topics.LoadOrStore(topic, newTopiclist(topic))
tlist := llist.(*topiclist)
tlist.subscribe(suber)
go suber.runActor()
return suber, nil
}
func (mq *MsgQueue) Unsubscribe(topic string, suber *actor) error {
if suber == nil {
return nil
}
// 先关闭订阅者,退出协程
suber.close()
select {
// wait for exit signal
case <-mq.exitC:
return errors.New("MQ EXIT")
default:
}
llist, ok := mq.topics.Load(topic)
if !ok {
return nil
}
tlist := llist.(*topiclist)
tlist.unsubscribe(suber)
return nil
}
func (mq *MsgQueue) Publish(topic string, msg interface{}) error {
select {
// wait for exit signal
case <-mq.exitC:
return errors.New("MQ EXIT")
default:
}
llist, ok := mq.topics.Load(topic)
if !ok {
return nil
}
tlist := llist.(*topiclist)
// 超时退出协程
idleDuration := publishDuration
idleDelay := time.NewTimer(idleDuration)
defer idleDelay.Stop()
msgP := PublishMsg{msg: msg, subers: tlist.subers}
select {
case tlist.publisher.Mailbox <- msgP:
case <-idleDelay.C:
return errors.New("MQ PUBLISH TIMEOUT")
}
return nil
}
func (mq *MsgQueue) Close() {
close(mq.exitC)
// 发布者和订阅者的协程
mq.topics.Range(func(key, value interface{}) bool {
tlist := value.(*topiclist)
tlist.publisher.close()
for _, suber := range tlist.subers {
suber.close()
}
return true
})
}
一些问题
1.我没go开发的经验,很多规范不是很熟悉,我很希望有人能分享是不是有不规范的问题
2.我比较在意的一个事情是,会不会协程泄露,我的方式是每个协程监听一个exitC的chan,但是我总是很怕关闭的时候panic或者其他原因,导致没成功关闭(例如,topiclist在取消订阅的时候,如果没订阅者了要关闭publisher的协程,如果没关成功,有新的订阅者订阅,会再开启协程,这样就会有残留一个。)。不知道我的代码有没有这方面的问题,或者平时生产是怎么解决这个问题,希望有人能提供解决的办法合思路