上一篇文章我们做了 channel 的基础设计,这一篇文章我们来完成消息队列两个必备的功能:完成确认和队列重入。

完成确认

我们都知道,在分布式系统的消息传递过程中有三种语义,也就是三种的质量保证:

  • At most once: 至多一次。消息在传递时,最多会被送达一次。换一个说法就是,没什么消息可靠性保证,允许丢消息。
  • At least once: 至少一次。消息在传递时,至少会被送达一次。也就是说,不允许丢消息,但是允许有少量重复消息出现。
  • Exactly once:恰好一次。消息在传递时,只会被送达一次,不允许丢失也不允许重复。

显然 Exactly once 语义是我们最希望系统能够提供的,但是它实现起来非常复杂,所以绝大部分消息队列系统提供的都是 At least once 语义,然后用幂等性来保证业务的正确性。而 At least once 语义最常见的实现方式就是接收方在接收到消息后进行回复确认,类似 TCP 握手中的 ACK。

要达到这个目的,我们首先要在 channel 中维护一个 map 用于存储已经发送的消息,以及一个管道来辅助写入:

type Channel struct {
    ...
    inFlightMessageChan chan *Message
    inFlightMessages    map[string]*Message
}

在发送消息的时候,我们也往 inFlightMessageChan 中写入,同时在事件处理函数 Router 中增加对该管道的监听,将接收到的消息添加到 inFlightMessages 中:

func (c *Channel) pushInFlightMessage(msg *Message) {
    c.inFlightMessages[util.UuidToStr(msg.Uuid())] = msg
}

func (c *Channel) popInFlightMessage(uuidStr string) (*Message, error) {
    msg, ok := c.inFlightMessages[uuidStr]
    if !ok {
        return nil, errors.New("UUID not in flight")
    }
    delete(c.inFlightMessages, uuidStr)
    return msg, nil
}

// Router handles the events of Channel
func (c *Channel) Router() {
    ...

    go c.RequeueRouter(closeChan)
    go c.MessagePump(closeChan)

    ...
}

func (c *Channel) RequeueRouter(closeChan chan struct{}) {
    for {
        select {
        case msg := <-c.inFlightMessageChan:
            c.pushInFlightMessage(msg)
        case <-closeChan:
            return
        }
    }
}

func (c *Channel) MessagePump(closeChan chan struct{}) {
    for {
        ...

        if msg != nil {
            c.inFlightMessageChan <- msg
        }

        c.clientMessageChan <- msg
    }
}

接下来编写完成确认相关逻辑,还是在 channel 结构体中添加一个管道 finishMessageChan,提供写入方法,并增加相关事件处理逻辑,代码如下:

type Channel struct {
    ...    
    finishMessageChan chan util.ChanReq
}

func (c *Channel) FinishMessage(uuidStr string) error {
    errChan := make(chan interface{})
    c.finishMessageChan <- util.ChanReq{
        Variable: uuidStr,
        RetChan:  errChan,
    }
    err, _ := (<-errChan).(error)
    return err
}

func (c *Channel) RequeueRouter(closeChan chan struct{}) {
    for {
        select {
        ...
        case finishReq := <-c.finishMessageChan:
            uuidStr := finishReq.Variable.(string)
            _, err := c.popInFlightMessage(uuidStr)
            if err != nil {
                log.Printf("ERROR: failed to finish message(%s) - %s", uuidStr, err.Error())
            }
            finishReq.RetChan <- err
        case <-closeChan:
            return
        }
    }
}

重入队列

消息重新入队也是很常见的功能,比如消费者想多次消费同一条消息的时候就需要用到,现在我们来实现一下。

重入和上面的完成确认逻辑很类似,就直接贴代码了:

type Channel struct {
    ...
    requeueMessageChan  chan util.ChanReq
}

func (c *Channel) RequeueMessage(uuidStr string) error {
    errChan := make(chan interface{})
    c.requeueMessageChan <- util.ChanReq{
        Variable: uuidStr,
        RetChan:  errChan,
    }
    err, _ := (<-errChan).(error)
    return err
}

func (c *Channel) RequeueRouter(closeChan chan struct{}) {
    for {
        select {
        ...
        case requeueReq := <-c.requeueMessageChan:
            uuidStr := requeueReq.Variable.(string)
            msg, err := c.popInFlightMessage(uuidStr)
            if err != nil {
                log.Printf("ERROR: failed to requeue message(%s) - %s", uuidStr, err.Error())
            } else {
                go func(msg *Message) {
                    c.PutMessage(msg)
                }(msg)
            }
            requeueReq.RetChan <- err
        case finishReq := <-c.finishMessageChan:
            ...
        }
    }
}

到这里功能就差不多已经全实现了,不过还有一个问题,就是如果消费者迟迟不确认完成的话,消息就会大量堆积在 inFlightMessages 中。我们可以添加这样一个逻辑:在限定的时间内如果消息没有确认完成的话,我们就将该消息自动重新入队。可以在监听 inFlightMessageChan 的 case 中加入以下代码:

func (c *Channel) RequeueRouter(closeChan chan struct{}) {
    for {
        select {
        case msg := <-c.inFlightMessageChan:
            ...
            go func(msg *Message) {
                select {
                case <-time.After(60 * time.Second):
                    log.Printf("CHANNEL(%s): auto requeue of message(%s)", c.name, util.UuidToStr(msg.Uuid()))
                }
                err := c.RequeueMessage(util.UuidToStr(msg.Uuid()))
                if err != nil {
                    log.Printf("ERROR: channel(%s) - %s", c.name, err.Error())
                }
            }(msg)
            ...
        }
    }
}

当然,当消息确认完成后我们也需要终止这个等待超时的逻辑,这里的解决方案是在消息 message 结构体中增加一个管道,等待入队的同时也监听这个管道,确认完成时则向这个管道发送消息。message 改动如下:

message.go

type Message struct {
    ...
    timerChan chan struct{}
}

...

func (m *Message) EndTimer() {
    select {
    case m.timerChan <- struct{}{}:
    default:

    }
}

channel 改动如下:

func (c *Channel) popInFlightMessage(uuidStr string) (*Message, error) {
    ...
    msg.EndTimer()
    return msg, nil
}

...

func (c *Channel) RequeueRouter(closeChan chan struct{}) {
    for {
        select {
        case msg := <-c.inFlightMessageChan:
            ...
            go func(msg *Message) {
                select {
                case <-time.After(60 * time.Second):
                    log.Printf("CHANNEL(%s): auto requeue of message(%s)", c.name, util.UuidToStr(msg.Uuid()))
                case <-msg.timerChan:
                    return
                }
                ...
            }(msg)
    }
}

channel 的完整代码:channel.go

到这里我们的 channel 就设计得差不多了,下一篇我们将介绍 topic 的实现。


项目地址:https://github.com/yhao1206/SMQ
相关阅读:


与昊
222 声望634 粉丝

IT民工,主要从事web方向,喜欢研究技术和投资之道