MQ
MQ的理解(自己的想法)
- 一种栈形式的数据结构?遵循先进先出的原则,跟栈非常的相像。
- 保存消息在传输过程中的一种容器,存储消息的中间件。多应用于分布式系统的通信。
MQ的优势(或是主要作用)
- 系统解耦:在一对多的系统中,负责调用不同函数的系统,存在顺序执行。所以要执行下一个动作则需要完成上一个动作,这样会导致阻塞影响性能。如果使用MQ则消息只需要发送给MQ,MQ来负责分配处理消息的对象,保证在消息阻塞的情况下,不影响别的功能的调用。
- 异步提速:MQ作为中间件,在操作时间,可同时多线程执行不同的请求,最后共同保存到DB中,对比顺序执行,减少了执行时间。
- 削峰填谷:如果当前系统可处理的1000个请求的情况下,突然有5000个请求同时到达,则会导致系统崩溃。而MQ可以通过每次获取1000个请求方式来保护系统,多余的请求可以存储在MQ中,当后续请求量到不了1000的时候再将存储的请求处理掉。保证系统的正常运行,减少系统崩溃的概率。
MQ的劣势
- 系统的可用性降低,MQ宕机会导致系统的不可用:如何保证MQ的高可用?
- 系统复杂度上升,要保证MQ异步调用的准确性,保证消息不被重复消费,保证信息不丢失,保证消费的顺序。
- 数据一致性问题,多系统操作,一个操作失败此前的操作如何做到回滚?
常见的MQ对比分析
RabbitMQ | RocketMQ | kafka |
---|---|---|
Rabbit | 阿里 | Apache |
并发量1w | 10w | 100w+ |
微秒延迟 | 毫秒延迟 | 毫秒以下 |
可靠性高 | 可靠性高 | 可靠性一般 |
RabbitMQ 相关介绍
RabbitMQ的工作模式:
- 简单模式,一个生产者对应一个MQ和一个消费者,是一个直线的顺序关系
- 工作队列模式,一个生产者对应一个MQ和多个消费者,消费者之间存在竞争关系
- 发布订阅模式(pub/sub),这是MQ最特别的模式,一个生产者对应一个交换机多个MQ和多个消费者。发布订阅模式是通过消费者订阅MQ,消息到达MQ后,由MQ对消费者进行消息推送的形式。
- 交换机只负责转发不具备存储能力
- 生产者负责将消息递交给交换机
- 交换机分为广播,路由,主题模式
- 广播,递交给所有的队列
- 路由,递交给指定的队列
- 主题,与路由相似,但是是使用通配符进行匹配的
RabbitMQ的高级特性
- 消息可靠性,RabbitMQ的消息可靠性主要通过confirm确认模式和Return退回模式来实现的。
- confirm callback :是生产者将消息传递给路由的时候,路由在接收消息成功后会产生一个成功回调通知生产者接收到消息
- return callback :是MQ接收路由信息的时候进行的失败回调,在接收信息不完整的情况下MQ会返回一个回调给路由,通知路由再发一边
- consumer ack
- 消费者确认机制,是消费者接收到信息后,进行消息处理,如果处理失败可以提示MQ重发消息,如果处理成功则通知MQ将存储的消息删除
- ack分为自动确认和手动确认两种,自动确认在收到信息后自动确认,消息就从队列中del了,如果处理失败也无法进行重发。
- 手动确认,在业务处理成功后调用basicack,手动签收,MQ才会删除信息。如果处理失败,可以调用basicNack拒收,MQ会重发消息。
- 持久化,宕机后可恢复消息
死信队列
创建时设置 x-dead-letter-exchange
死信队列储存的信息有
- consumer 拒收却且不放入原队列
- 超时未消费的消息
- 队列过长,加入新的消息,导致被挤掉的旧消息
幂等性问题
如何确保多条相同的消息,消费后得到只消费一次的效果
解决方案:
- insert 操作会先根据主键判断,存在则将insert修改为update
- 使用乐观锁 + 版本控制的形式来更新语句。例如:
update table set name = ? , version = version + 1 where ? and version = 1
保证消息消费顺序
一个queue有多个consumer消费,每个消息处理时间不同导致处理顺序不同。
一个queue对应一个consumer,但是consumer存在多线程操作。
解决方案
- 一对一的情况下不使用多线程
- 消费者不直接消费消息,consumer中进行内部排队,关关键字存在同一个队列,保证相同的操作顺序执行,不同的操作同步执行。
MQ消费者获取消息的两种模式
- pull,主动拉取信息。需要轮询,如果MQ长时间处理空置状态很容易造成cpu的浪费
- push,由MQ进行消息的推送,这是发布订阅模式的方法。但是这样会导致削峰力度的不够,要进行消费限流。
Redis实现MQ
redis实现MQ的方式有很多种,主要分析一下优劣
Redis实现MQ的缺点:
- 存储贵,内存空间的存储小且有限
- 存在数据丢失的风险
- 内存的存储本身就是容易丢失的
- 持久化是异步的,所以会有持久化但是添加新数据导致数据丢失问题
- 数据一直性弱
redisList
redisList,双向链表,符合先进先出的操作理念。
操作指令:
- lpush 推送信息到redis
- rpop 消费者消费消息,返回一个list元素
- brpop 推荐的消费指令
redisList作为MQ的消费流程
- list作为实现的模式是pull形式,要consumer轮询拉取进行消费
- 使用rpop拉取时,数据存在则返回消息,数据不存在则返回nil。consumer再度进行轮询
- 使用rpop的劣势
- nil的时候需要一直轮询,不会进行阻塞,这样一直轮询浪费cpu资源。而手动阻塞则会因为时间判断不准确而导致消费不积极,从而导致信息处理过慢。
- 使用brpop可以做到,有数据才返回响应,存在阻塞效果,可以减少轮询的次数
- brpop listname blockTime
List作为MQ的局限性
- 不支持发布订阅模式,redis数据是独立的只有一份
- 不支持ack回调机制
redis pub/sub 实现MQ
主要解决List无法实现发布订阅模式的问题。
- 消费方通过subcriber创建channel,每个消费方subcriber创建的都是独立的channel
- 消费到达MQ后,会复制多份,分别从channel中推送给消费者
- 生产方使用publisher 指定推送channel
pub/sub模式 中存储的是channel跟subscriber的映射关系。生产者通过publisher推送信息到channel的时候,redis会查询映射关系,然后将消息推送到对应的subecriber。
每个subscriber都存在着一个缓冲区buffer,主要是存储接收到还没来得及处理的消息。
pub/sub的劣势
- 缺少ack回调机制
- 不存储信息,pub/sub模式不存储信息,即停即走。所以不可能存在信息重复发送的情况。
Streams
以上两种形式的数据结构实现MQ都存在着明显的弊端。
而在redis5.0推出了一个新型的数据结构streams,而这个新功能就是为了redis实现MQ功能而量身定做的。
操作的具体流程
- 生产者通过XADD的形式向streams中新增消息,新增消息的时候默认会生成msgID,这是根据添加顺序来进行消息排序的。
- 消费者需要通过 Xgroup Create 的形式来创建分组联系
XGROUP CREATE topic group 0-0
- 同一个消息在一个消费组中只会被消费一次
- 后续通过XReadGroup的指令,以消费组的形式进行消费
- 消费的形式又分为两种,一种是直接消费streams中的消息
- 另一种消费是消费,超时未ACK的消息,及消息推送后未返回ack,则进行重复消费
// 正常消费
XREADGROUP GROUP groupID consumerID BLOCK 0 STREAMS
// BLOCK阻塞时间设置为0的时候,代表无限阻塞
// 消费未ack消息
XREADGROUP GROUP groupID consumerID STREAMS topic 0-0
// 0-0 表示读取未ack的消息
- 消息处理成功后需要返回ack,消息才会在redis中del
XACK topic groupID msgID
streams的优势分析
- 支持发布订阅模式
- 支持持久化存储
- 支持ack机制
- 支持消息缓存
对比list,pub/sub
mq实现方案 | 发布订阅 | ack | 缓存 | 丢失风险 |
---|---|---|---|---|
list | X | X | √ | X |
pub/sub | √ | X | X | √ |
streams | √ | √ | √ | X |
streams的劣势
对于redis这种基于内存存储的存储引擎,始终存在爆内存的风险,可以使用
XADD topic MAXLEN 1000 * key value
通过maxlen来指定缓存长度,1000表示存1000条数据,在redis超过1000条数据的情况,会将老的消息挤掉。存在数据丢失的风险。需要合理的计算设计MAXLEN
相较于kafka等MQ中间件来说,redis streams实现的mq虽然存在很多缺点。但是唯一的好处就是运行维护的成本偏低。多数分布式服务都存在redis缓存的情况。使用redis实现mq则减少了更多中间件的引入。
代码实现
具体参考了小徐先生的编程世界,可以移步查看原文,如果只是想使用的,也可以查看小徐老师的开源项目
实现架构分析
实现MQ主要是分别对生产者提交消息的方法封装。对消费者消费消息还有ack确认回调机制的封装。期间还存在着存储消息处理失败,不放回MQ队列的上文提到的死信队列的实现。
首先封装redis客户端
- 实现redis连接池pool,还有初始化方法。这个在redis实现分布式锁的时候有写过,可参考
- 封装生产者跟消费者的方法
- XADD 生产者递交消息的方法
- XReadGroup 消费者消费信息的方法(消费队列信息和过期未ACK信息)
- XReadGroupPending 过期信息
- XReadGroup 队列信息
- XGroupCreate 创建分组
- XACK 消息回调确认
代码如下
// XADD 执行XADD语句
func (c *Client) XADD(ctx context.Context, topic string, maxLen int, key, value string) (string, error) {
// topic 不能为空
if topic == "" {
return "", errors.New("redis XADD topic can't be empty")
}
// 从redisPool中获取连接
conn, err := c.getConn(ctx)
if err != nil {
return "", err
}
// 使用完毕关闭
defer conn.Close()
// 执行XADD指令
// return 唯一标识,跟redis返回的一样
return redis.String(conn.Do("XADD", topic, "MAXLEN", maxLen, "*", key, value))
}
// 判断发送请求,获取未确认请求
func (c *Client) XReadGroupPending(ctx context.Context, groupID, consumerID, topic string) ([]*MsgEntity, error) {
return c.xReadGroup(ctx, groupID, consumerID, topic, 0, true)
}
// 判断发送请求,获取队列中存在请求
func (c *Client) XReadGroup(ctx context.Context, groupID, consumerID, topic string, timeoutMiliSeconds int) ([]*MsgEntity, error) {
return c.xReadGroup(ctx, groupID, consumerID, topic, timeoutMiliSeconds, false)
}
func (c *Client) xReadGroup(ctx context.Context, groupId, consumerId, topic string, timeoutSeconds int, pending bool) ([]*MsgEntity, error) {
// 判断关键数据不能为nil
if groupId == "" || consumerId == "" || topic == "" {
return nil, errors.New("redis XREADGROUP groupID/consumerID/topic can't be empty")
}
// 获取conn进行操作
conn, err := c.getConn(ctx)
if err != nil {
return nil, err
}
// 关闭连接
defer conn.Close()
// rawReply 接收返回数据,数据内容很多,后续处理
/*
1) 1) "my_test_topic" rawMsg
2) 1) 1) "1712908579093-0" msgId
2) 1) "first_key" msgKey
2) "first_value" msgVal
*/
var rawReply interface{}
// 判断pending,是查看未ack,还是查看队列中的消息
if pending {
// pending 为true 查看未ack消息
rawReply, err = conn.Do("XREADGROUP", "GROUP", groupId, consumerId, "STREAMS", topic, "0-0")
} else {
rawReply, err = conn.Do("XREADGROUP", "GROUP", groupId, consumerId, "BLOCK", timeoutSeconds,
"STREAMS", topic, ">")
}
// 进行获取数据的判断
if err != nil {
return nil, err
}
//处理rawReply
reply, _ := rawReply.([]interface{})
if len(reply) == 0 {
// no msg received
return nil, ErrNoMsg
}
replyElement, _ := reply[0].([]interface{})
if len(replyElement) != 2 {
// 消息格式不对??
return nil, errors.New("invalid msg format")
}
var msgs []*MsgEntity
fmt.Printf("%t", replyElement[0])
rawMsgs, _ := replyElement[1].([]interface{})
for _, rawMsg := range rawMsgs {
_msg, _ := rawMsg.([]interface{})
if len(_msg) != 2 {
return nil, errors.New("invalid msg format")
}
msgID := gocast.ToString(_msg[0])
msgBody, _ := _msg[1].([]interface{})
if len(msgBody) != 2 {
return nil, errors.New("invalid msg format")
}
msgKey := gocast.ToString(msgBody[0])
msgVal := gocast.ToString(msgBody[1])
msgs = append(msgs, &MsgEntity{
// 1712912520368-0
msgID,
// first_key
msgKey,
// first_value
msgVal,
})
}
return msgs, nil
}
// XGroupCreate 创建group分组
func (c *Client) XGroupCreate(ctx context.Context, topic, group string) (string, error) {
// 获取连接
conn, err := c.getConn(ctx)
if err != nil {
return "", err
}
// 默认关闭连接
defer conn.Close()
// 执行分组语句
return redis.String(conn.Do("XGROUP", "CREATE", topic, group, "0-0"))
}
func (c *Client) XACK(ctx context.Context, topic, groupID, msgID string) error {
if topic == "" || groupID == "" || msgID == "" {
return errors.New("topic,groupId or msgId is not nil")
}
conn, err := c.getConn(ctx)
if err != nil {
return err
}
defer conn.Close()
_, err = conn.Do("XACK", topic, groupID, msgID)
return err
}
封装生产者对象
- 生产者需要内置client方便调用方法
- 生产者的唯一配置是 maxlen 即创建对队列的最大值
// --------------------------------------------ProducerOptions-----------------------------------------------------------
type ProducerOptions struct {
// topic 可以缓存的消息长度,单位:条. 当消息条数超过此数值时,会把老消息踢出队列
msgQueueLen int
}
type ProducerOption func(p *ProducerOptions)
func WithMsgQueueLen(len int) ProducerOption {
return func(opts *ProducerOptions) {
opts.msgQueueLen = len
}
}
// 默认为每个 topic 保留 500 条消息
func repairProducer(opts *ProducerOptions) {
if opts.msgQueueLen <= 0 {
opts.msgQueueLen = 500
}
}
type Producer struct {
// 内置Client
c *Client
// 用户自定义生产者配置文件
opts *ProducerOptions
}
func NewProducer(client *Client, opts ...ProducerOption) *Producer {
// 创建,并赋值Client
p := Producer{
c: client,
opts: &ProducerOptions{},
}
// 遍历配置
for _, opt := range opts {
opt(p.opts)
}
// 不合规的配置恢复默认
repairProducer(p.opts)
// 返回
return &p
}
// 投递信息
func (p *Producer) SendMsg(ctx context.Context, topic, key, val string) (string, error) {
return p.c.XADD(ctx, topic, p.opts.msgQueueLen, key, val)
}
封装消费者对象
- 消费者配置文件
- receiveTimeout 每轮接收信息的阻塞时长
- maxRetryLimit 消息的最大重试次数
- deadLetterMailbox 死信队列
- deadLetterDeliverTimeout 投递死信的超时阈值
- handleMsgsTimeout 处理消息流程的超时阈值
- 控制消费者的生命周期
- context.context
- context.CancelFunc
- 接收msg的回调函数 callbackFunc
- redis 客户端
- topic 消费的streams
- groupid 消费组
- consumerID 消费者
- failureCnts 消息累计处理失败kv
type封装(配置文件)和构造方法
// ------------------------------------------------consumerOptions------------------------------------------------------
type ConsumerOptions struct {
// 每轮接收消息超时时长
receiveTimeout time.Duration
// 处理消息最大重试次数,超过规定次数,递交到死信队列
maxRetryLimit int
// 死信队列,可以由使用方自定义实现
deadLetterMailbox DeadLetterMailbox
// 投递死信流程超时阈值
deadLetterDeliverTimeout time.Duration
// 处理消息流程超时阈值
handleMsgsTimeout time.Duration
}
type ConsumerOption func(opts *ConsumerOptions)
func WithReceiveTimeout(timeout time.Duration) ConsumerOption {
return func(opts *ConsumerOptions) {
opts.receiveTimeout = timeout
}
}
func WithMaxRetryLimit(maxRetryLimit int) ConsumerOption {
return func(opts *ConsumerOptions) {
opts.maxRetryLimit = maxRetryLimit
}
}
func WithDeadLetterMailbox(mailbox DeadLetterMailbox) ConsumerOption {
return func(opts *ConsumerOptions) {
opts.deadLetterMailbox = mailbox
}
}
func WithDeadLetterDeliverTimeout(timeout time.Duration) ConsumerOption {
return func(opts *ConsumerOptions) {
opts.deadLetterDeliverTimeout = timeout
}
}
func WithHandleMsgsTimeout(timeout time.Duration) ConsumerOption {
return func(opts *ConsumerOptions) {
opts.handleMsgsTimeout = timeout
}
}
func repairConsumer(opts *ConsumerOptions) {
if opts.receiveTimeout < 0 {
opts.receiveTimeout = 2 * time.Second
}
if opts.maxRetryLimit < 0 {
opts.maxRetryLimit = 3
}
if opts.deadLetterMailbox == nil {
opts.deadLetterMailbox = NewDeadLetterLogger()
}
if opts.deadLetterDeliverTimeout <= 0 {
opts.deadLetterDeliverTimeout = time.Second
}
if opts.handleMsgsTimeout <= 0 {
opts.handleMsgsTimeout = time.Second
}
}
// 消费者type封装
// consumer
type Consumer struct {
// consumer 生命周期
ctx context.Context
// 停止 consumer 控制器
stop context.CancelFunc
// 接收msg的回调函数
callbackFunc MsgCallback
// redis 客户端
client *Client
// 消费的topic
topic string
// 所属消费组
groupID string
// 当前消费者
consumerID string
// 消息累计失败次数
failureCnts map[MsgEntity]int
// 用户自定义配置
opts *ConsumerOptions
}
// consumer构造器
func NewConsumer(client *Client, topic, groupID, consumerID string, callbackFunc MsgCallback, opts ...ConsumerOption) (*Consumer, error) {
// 创建用于consumer停止的控制器
ctx, stop := context.WithCancel(context.Background())
c := Consumer{
client: client,
ctx: ctx,
topic: topic,
groupID: groupID,
consumerID: consumerID,
callbackFunc: callbackFunc,
stop: stop,
opts: &ConsumerOptions{},
// 消息累计失败次数
failureCnts: make(map[MsgEntity]int),
}
// 校验 consumer 中的参数,包括 topic、groupID、consumerID 都不能为空,且 callbackFunc 不能为空
if err := c.checkParam(); err != nil {
return nil, err
}
for _, opt := range opts {
opt(c.opts)
}
repairConsumer(c.opts)
c.client.XGroupCreate(c.ctx, c.topic, c.groupID)
// 多线程循环获取
go c.run()
return &c, nil
}
// 检查配置
func (c *Consumer) checkParam() error {
if c.callbackFunc == nil {
return errors.New("callback function can't be empty")
}
if c.client == nil {
return errors.New("redis client can't be empty")
}
if c.topic == "" || c.consumerID == "" || c.groupID == "" {
return errors.New("topic | group_id | consumer_id can't be empty")
}
return nil
}
// 接收到消息后执行的回调函数
type MsgCallback func(ctx context.Context, msg *MsgEntity) error
// 停止 consumer
func (c *Consumer) Stop() {
c.stop()
}
消费消息方法ACK方法封装
// 运行消费者
func (c *Consumer) run() {
for {
select {
case <-c.ctx.Done():
return
default:
}
// 新消息接收处理
msgs, err := c.receive()
/*
msgs = append(msgs, &MsgEntity{
// 1712912520368-0
msgID,
// first_key
msgKey,
// first_value
msgVal,
*/
if err != nil {
fmt.Printf("receive msg failed, err: %v", err)
continue
}
// 设置处理信息超时时间并处理信息
tctx, _ := context.WithTimeout(c.ctx, c.opts.handleMsgsTimeout)
c.handlerMsgs(tctx, msgs)
// 死信队列投递
tctx, _ = context.WithTimeout(c.ctx, c.opts.deadLetterDeliverTimeout)
c.deliverDeadLetter(tctx)
// pending 接收消息处理
pendingMsg, err := c.receivePending()
if err != nil {
fmt.Printf("pending msg received failed, err: %v", err)
}
tctx, _ = context.WithTimeout(c.ctx, c.opts.handleMsgsTimeout)
c.handlerMsgs(tctx, pendingMsg)
}
}
// 获取分组信息
func (c *Consumer) receive() ([]*MsgEntity, error) {
// ctx == 上下文
// group.topic.consumer
// receiveTimeout == 无信息时阻塞时间
msgs, err := c.client.XReadGroup(c.ctx, c.groupID, c.consumerID, c.topic, int(c.opts.receiveTimeout.Milliseconds()))
// 超过阻塞时间依旧无信息返回
if err != nil && errors.Is(err, ErrNoMsg) {
return nil, err
}
return msgs, nil
}
// 获取未ack信息
func (c *Consumer) receivePending() ([]*MsgEntity, error) {
// 调用 client 的阅读未确认msg的方法
msg, err := c.client.XReadGroupPending(c.ctx, c.groupID, c.consumerID, c.topic)
if err != nil && errors.Is(err, ErrNoMsg) {
return nil, err
}
return msg, nil
}
// 执行成功进行ack操作
func (c *Consumer) handlerMsgs(ctx context.Context, msgs []*MsgEntity) {
for _, msg := range msgs {
if err := c.callbackFunc(ctx, msg); err != nil {
c.failureCnts[*msg]++
}
// callback执行成功,进行ack
if err := c.client.XACK(ctx, c.topic, c.groupID, msg.MsgID); err != nil {
fmt.Printf("msg ack failed, msg id: %s, err: %v", msg.MsgID, err)
continue
}
delete(c.failureCnts, *msg)
}
}
死信队列操作
// 死信队列操作
func (c *Consumer) deliverDeadLetter(ctx context.Context) {
for msg, failureCnt := range c.failureCnts {
if failureCnt < c.opts.maxRetryLimit {
continue
}
// 投递死信
if err := c.opts.deadLetterMailbox.Deliver(&msg); err != nil {
fmt.Printf("dead letter deliver failed, msg id: %s, err: %v", msg.MsgID, err)
}
// 执行 ack 响应
if err := c.client.XACK(ctx, c.topic, c.groupID, msg.MsgID); err != nil {
fmt.Printf("msg ack failed, msg id: %s, err: %v", msg.MsgID, err)
continue
}
// 对于 ack 成功的消息,将其从 failure map 中删除
delete(c.failureCnts, msg)
}
}
redisMQ使用方法
// 启动生产者
import (
"context"
"github.com/xiaoxuxiansheng/redmq"
)
func main(){
// ...
producer := redmq.NewProducer(redisClient, redmq.WithMsgQueueLen(10))
ctx := context.Background()
msgID, err := producer.SendMsg(ctx, topic, "test_kk", "test_vv")
}
// 启动消费者
import (
"github.com/xiaoxuxiansheng/redmq"
)
func main(){
// ...
// 构造并启动消费者
consumer, _ := redmq.NewConsumer(redisClient, topic, consumerGroup, consumerID, callbackFunc,
// 每条消息最多重试 2 次
redmq.WithMaxRetryLimit(2),
// 每轮接收消息的超时时间为 2 s
redmq.WithReceiveTimeout(2*time.Second),
// 注入自定义实现的死信队列
redmq.WithDeadLetterMailbox(demoDeadLetterMailbox))
defer consumer.Stop()
}
// 生产者投递流程
import (
"context"
"testing"
"github.com/xiaoxuxiansheng/redmq"
"github.com/xiaoxuxiansheng/redmq/redis"
)
const (
network = "tcp"
address = "请输入 redis 地址"
password = "请输入 redis 密码"
topic = "请输入 topic 名称"
)
func Test_Producer(t *testing.T) {
client := redis.NewClient(network, address, password)
// 最多保留十条消息
producer := redmq.NewProducer(client, redmq.WithMsgQueueLen(10))
ctx := context.Background()
msgID, err := producer.SendMsg(ctx, topic, "test_k", "test_v")
if err != nil {
t.Error(err)
return
}
t.Log(msgID)
}
// 消费者消费流程
import (
"context"
"testing"
"time"
"github.com/xiaoxuxiansheng/redmq"
"github.com/xiaoxuxiansheng/redmq/redis"
)
const (
network = "tcp"
address = "请输入 redis 地址"
password = "请输入 redis 密码"
topic = "请输入 topic 名称"
consumerGroup = "请输入消费者组名称"
consumerID = "请输入消费者名称"
)
// 自定义实现的死信队列
type DemoDeadLetterMailbox struct {
do func(msg *redis.MsgEntity)
}
func NewDemoDeadLetterMailbox(do func(msg *redis.MsgEntity)) *DemoDeadLetterMailbox {
return &DemoDeadLetterMailbox{
do: do,
}
}
// 死信队列接收消息的处理方法
func (d *DemoDeadLetterMailbox) Deliver(ctx context.Context, msg *redis.MsgEntity) error {
d.do(msg)
return nil
}
func Test_Consumer(t *testing.T) {
client := redis.NewClient(network, address, password)
// 接收到消息后的处理函数
callbackFunc := func(ctx context.Context, msg *redis.MsgEntity) error {
t.Logf("receive msg, msg id: %s, msg key: %s, msg val: %s", msg.MsgID, msg.Key, msg.Val)
return nil
}
// 自定义实现的死信队列
demoDeadLetterMailbox := NewDemoDeadLetterMailbox(func(msg *redis.MsgEntity) {
t.Logf("receive dead letter, msg id: %s, msg key: %s, msg val: %s", msg.MsgID, msg.Key, msg.Val)
})
// 构造并启动消费者
consumer, err := redmq.NewConsumer(client, topic, consumerGroup, consumerID, callbackFunc,
// 每条消息最多重试 2 次
redmq.WithMaxRetryLimit(2),
// 每轮接收消息的超时时间为 2 s
redmq.WithReceiveTimeout(2*time.Second),
// 注入自定义实现的死信队列
redmq.WithDeadLetterMailbox(demoDeadLetterMailbox))
if err != nil {
t.Error(err)
return
}
defer consumer.Stop()
// 十秒后退出单测程序
<-time.After(10 * time.Second)
}