GoLang学习-Redis实现MQ功能

MQ

MQ的理解(自己的想法)

  1. 一种栈形式的数据结构?遵循先进先出的原则,跟栈非常的相像。
  2. 保存消息在传输过程中的一种容器,存储消息的中间件。多应用于分布式系统的通信。

MQ的优势(或是主要作用)

  1. 系统解耦:在一对多的系统中,负责调用不同函数的系统,存在顺序执行。所以要执行下一个动作则需要完成上一个动作,这样会导致阻塞影响性能。如果使用MQ则消息只需要发送给MQ,MQ来负责分配处理消息的对象,保证在消息阻塞的情况下,不影响别的功能的调用。
  2. 异步提速:MQ作为中间件,在操作时间,可同时多线程执行不同的请求,最后共同保存到DB中,对比顺序执行,减少了执行时间。
  3. 削峰填谷:如果当前系统可处理的1000个请求的情况下,突然有5000个请求同时到达,则会导致系统崩溃。而MQ可以通过每次获取1000个请求方式来保护系统,多余的请求可以存储在MQ中,当后续请求量到不了1000的时候再将存储的请求处理掉。保证系统的正常运行,减少系统崩溃的概率。

MQ的劣势

  1. 系统的可用性降低,MQ宕机会导致系统的不可用:如何保证MQ的高可用?
  2. 系统复杂度上升,要保证MQ异步调用的准确性,保证消息不被重复消费,保证信息不丢失,保证消费的顺序。
  3. 数据一致性问题,多系统操作,一个操作失败此前的操作如何做到回滚?

常见的MQ对比分析

RabbitMQRocketMQkafka
Rabbit阿里Apache
并发量1w10w100w+
微秒延迟毫秒延迟毫秒以下
可靠性高可靠性高可靠性一般

RabbitMQ 相关介绍

RabbitMQ的工作模式:

  1. 简单模式,一个生产者对应一个MQ和一个消费者,是一个直线的顺序关系
  2. 工作队列模式,一个生产者对应一个MQ和多个消费者,消费者之间存在竞争关系
  3. 发布订阅模式(pub/sub),这是MQ最特别的模式,一个生产者对应一个交换机多个MQ和多个消费者。发布订阅模式是通过消费者订阅MQ,消息到达MQ后,由MQ对消费者进行消息推送的形式。
    1. 交换机只负责转发不具备存储能力
    2. 生产者负责将消息递交给交换机
    3. 交换机分为广播,路由,主题模式
      1. 广播,递交给所有的队列
      2. 路由,递交给指定的队列
      3. 主题,与路由相似,但是是使用通配符进行匹配的

RabbitMQ的高级特性

  1. 消息可靠性,RabbitMQ的消息可靠性主要通过confirm确认模式和Return退回模式来实现的。
    1. confirm callback :是生产者将消息传递给路由的时候,路由在接收消息成功后会产生一个成功回调通知生产者接收到消息
    2. return callback :是MQ接收路由信息的时候进行的失败回调,在接收信息不完整的情况下MQ会返回一个回调给路由,通知路由再发一边
  2. consumer ack
    1. 消费者确认机制,是消费者接收到信息后,进行消息处理,如果处理失败可以提示MQ重发消息,如果处理成功则通知MQ将存储的消息删除
    2. ack分为自动确认和手动确认两种,自动确认在收到信息后自动确认,消息就从队列中del了,如果处理失败也无法进行重发。
    3. 手动确认,在业务处理成功后调用basicack,手动签收,MQ才会删除信息。如果处理失败,可以调用basicNack拒收,MQ会重发消息。
  3. 持久化,宕机后可恢复消息
死信队列

创建时设置 x-dead-letter-exchange
死信队列储存的信息有

  1. consumer 拒收却且不放入原队列
  2. 超时未消费的消息
  3. 队列过长,加入新的消息,导致被挤掉的旧消息
幂等性问题

如何确保多条相同的消息,消费后得到只消费一次的效果
解决方案:

  1. insert 操作会先根据主键判断,存在则将insert修改为update
  2. 使用乐观锁 + 版本控制的形式来更新语句。例如:
update table set name = ? , version = version + 1 where ? and version = 1
保证消息消费顺序

一个queue有多个consumer消费,每个消息处理时间不同导致处理顺序不同。
一个queue对应一个consumer,但是consumer存在多线程操作。
解决方案

  1. 一对一的情况下不使用多线程
  2. 消费者不直接消费消息,consumer中进行内部排队,关关键字存在同一个队列,保证相同的操作顺序执行,不同的操作同步执行。

MQ消费者获取消息的两种模式

  1. pull,主动拉取信息。需要轮询,如果MQ长时间处理空置状态很容易造成cpu的浪费
  2. push,由MQ进行消息的推送,这是发布订阅模式的方法。但是这样会导致削峰力度的不够,要进行消费限流。

Redis实现MQ

redis实现MQ的方式有很多种,主要分析一下优劣

Redis实现MQ的缺点:

  1. 存储贵,内存空间的存储小且有限
  2. 存在数据丢失的风险
    1. 内存的存储本身就是容易丢失的
    2. 持久化是异步的,所以会有持久化但是添加新数据导致数据丢失问题
    3. 数据一直性弱
redisList

redisList,双向链表,符合先进先出的操作理念。
操作指令:

  1. lpush 推送信息到redis
  2. rpop 消费者消费消息,返回一个list元素
  3. brpop 推荐的消费指令
redisList作为MQ的消费流程
  1. list作为实现的模式是pull形式,要consumer轮询拉取进行消费
  2. 使用rpop拉取时,数据存在则返回消息,数据不存在则返回nil。consumer再度进行轮询
  3. 使用rpop的劣势
    1. nil的时候需要一直轮询,不会进行阻塞,这样一直轮询浪费cpu资源。而手动阻塞则会因为时间判断不准确而导致消费不积极,从而导致信息处理过慢。
    2. 使用brpop可以做到,有数据才返回响应,存在阻塞效果,可以减少轮询的次数
    3. brpop listname blockTime
List作为MQ的局限性
  • 不支持发布订阅模式,redis数据是独立的只有一份
  • 不支持ack回调机制
redis pub/sub 实现MQ

主要解决List无法实现发布订阅模式的问题。

  1. 消费方通过subcriber创建channel,每个消费方subcriber创建的都是独立的channel
  2. 消费到达MQ后,会复制多份,分别从channel中推送给消费者
  3. 生产方使用publisher 指定推送channel

pub/sub模式 中存储的是channel跟subscriber的映射关系。生产者通过publisher推送信息到channel的时候,redis会查询映射关系,然后将消息推送到对应的subecriber。
每个subscriber都存在着一个缓冲区buffer,主要是存储接收到还没来得及处理的消息。

pub/sub的劣势
  1. 缺少ack回调机制
  2. 不存储信息,pub/sub模式不存储信息,即停即走。所以不可能存在信息重复发送的情况。

Streams

以上两种形式的数据结构实现MQ都存在着明显的弊端。
而在redis5.0推出了一个新型的数据结构streams,而这个新功能就是为了redis实现MQ功能而量身定做的。

操作的具体流程
  1. 生产者通过XADD的形式向streams中新增消息,新增消息的时候默认会生成msgID,这是根据添加顺序来进行消息排序的。
  2. 消费者需要通过 Xgroup Create 的形式来创建分组联系
XGROUP CREATE topic group 0-0
  1. 同一个消息在一个消费组中只会被消费一次
  2. 后续通过XReadGroup的指令,以消费组的形式进行消费
    1. 消费的形式又分为两种,一种是直接消费streams中的消息
    2. 另一种消费是消费,超时未ACK的消息,及消息推送后未返回ack,则进行重复消费
// 正常消费
XREADGROUP GROUP groupID consumerID BLOCK 0 STREAMS
// BLOCK阻塞时间设置为0的时候,代表无限阻塞

// 消费未ack消息
XREADGROUP GROUP groupID consumerID STREAMS topic 0-0
// 0-0 表示读取未ack的消息
  1. 消息处理成功后需要返回ack,消息才会在redis中del
XACK topic groupID msgID
streams的优势分析
  • 支持发布订阅模式
  • 支持持久化存储
  • 支持ack机制
  • 支持消息缓存
对比list,pub/sub
mq实现方案发布订阅ack缓存丢失风险
listXXX
pub/subXX
streamsX
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客户端
  1. 实现redis连接池pool,还有初始化方法。这个在redis实现分布式锁的时候有写过,可参考
  2. 封装生产者跟消费者的方法
    1. XADD 生产者递交消息的方法
    2. XReadGroup 消费者消费信息的方法(消费队列信息和过期未ACK信息)
      1. XReadGroupPending 过期信息
      2. XReadGroup 队列信息
    3. XGroupCreate 创建分组
    4. 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
}
封装生产者对象
  1. 生产者需要内置client方便调用方法
  2. 生产者的唯一配置是 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)
}

封装消费者对象
  1. 消费者配置文件
    1. receiveTimeout 每轮接收信息的阻塞时长
    2. maxRetryLimit 消息的最大重试次数
    3. deadLetterMailbox 死信队列
    4. deadLetterDeliverTimeout 投递死信的超时阈值
    5. handleMsgsTimeout 处理消息流程的超时阈值
  2. 控制消费者的生命周期
    1. context.context
    2. context.CancelFunc
  3. 接收msg的回调函数 callbackFunc
  4. redis 客户端
  5. topic 消费的streams
  6. groupid 消费组
  7. consumerID 消费者
  8. 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)
}
  • 9
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
如果你在 Golang 中连接 Redis 时失败,可能是以下几个原因: 1. Redis 服务器未启动 请确保 Redis 服务器已经在运行。你可以在命令行下输入 `redis-cli` 命令,查看 Redis 是否能够正常启动。 2. Redis 服务器未开启远程访问 默认情况下,Redis 只允许本地访问。如果你需要从远程连接 Redis,需要修改 Redis 的配置文件,将 `bind` 属性设置为 Redis 服务器的 IP 地址。例如,如果 Redis 服务器的 IP 地址为 192.168.1.100,你需要将配置文件中的 `bind` 属性设置为 `bind 192.168.1.100`。 3. Redis 服务器的防火墙未开放端口 如果 Redis 服务器的防火墙开启了,你需要开放 Redis 服务器的端口号。默认情况下,Redis 使用的端口号为 6379。你可以在 Redis 服务器的防火墙中开放该端口。 4. Golang 代码中的连接参数错误 请检查你的 Golang 代码中的连接参数是否正确。例如,你需要确保 Redis 服务器的 IP 地址、端口号、密码等信息都正确。 以下是一个简单的 Golang 代码连接 Redis 的示例: ```go package main import ( "github.com/go-redis/redis" ) func main() { client := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", // no password set DB: 0, // use default DB }) pong, err := client.Ping().Result() if err != nil { fmt.Println("Failed to ping Redis:", err) return } fmt.Println("Successfully connected to Redis:", pong) } ``` 你可以根据自己的实际情况修改该代码中的连接参数。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值