redis 队列_用redis和go做队列

85e99c0f09cb09811e7db6c45b0cd7bb.png

最近做一个小型项目,因为rabbitMQ等专业软件比较重,故团队决定采用redis实现一个轻量的队列。

0b6ba6024b1f2e1fde7feb6481ace153.png

目标

在这篇文章中,你可以获得:

  • redigo包的基本用法
  • 初步认知context包的作用
  • 了解一个功能模块的实现思路以及如何逐步完善的步骤
  • Go特性(selectchannelgoroutine)的利用

最终代码量大概265行左右。

redis队列的原生用法

redis并不是被设计用来做队列的,事实上它并不是那么适合作为队列载体——官方也不推荐用来做队列,甚至因为使用redis做队列的人太多,而促使antirez(redis的作者)开发了另一个名为Disque^1的专业队列库,据说将会加入到redis6中。

尽管如此,redis依旧提供了号称Reliable queue的队列指令^2。

我们知道,当生产者在另一端生成消息之后,这一端的消费者就要取出这一消息进行消费动作;而在消费的过程中如果出现任何异常——例如“程序崩溃”等问题,造成进程的退出,消息就会丢失。为此, redis官方提供了 RPOPLPUSH这一队列指令,在从队列中取出消息的同时又塞进另一个队列中。这样当程序发生异常退出时,我们也可以通过第二个队列来找回丢失的消息。

它的使用方法是:

127.0.0.1:6379> RPOPLPUSH sourceQueue destQueue

这意味着我们需要准备两个队列,一个待执行队列,一个执行队列。在消费动作完成之后,通过LREM执行删除执行队列中的消息成员;除此之外,我们还需要时常检查执行队列中是否有滞留的消息成员。如果有,表示之前有消息没被消费,再通过RPOPLPUSH指令重新放回待执行队列。

127.0.0.1:6379> LREM queue numbers msg

而生产者则是通过LPUSH这一指令将消息推入待执行队列的:

127.0.0.1:6379> LPUSH queue msg

下图展示了原生redis下的整个生产-消费流程:

38b00e0c0421008ba2beb86dc115033a.png

队列、消息的设计思路

接下来我们使用Go语言来编写实现队列的代码。

首先可以明确的是,我们需要一个队列结构,以及一个消息结构。

队列的功能

队列需要做哪些工作呢?队列需要和redis产生通信、交互。因此它需要拥有一个字段用来保存redis的连接;我们所有的对redis操作都通过队列来实现,因此最好在此结构上封装一些简易的redis操作方法,比如lrem;另外当我们把消息传给队列时,它需要有一个delivery方法将消息投递到队列中;此外也要有一个receive方法将消息从队列中取出。

基于这些描述,我们对队列的结构有了一个大致的了解,可以将其用代码描述出来:

type Queue struct {
    
    conn *redis.Connect
}

func (q *Queue) lrem(msg, queue string) {
    
    q.conn.lrem(queue, 1, msg)
}

// 大写是因为这个方法是提供给外部调用的
func (q *Queue) Delivery(msg, queue string) {
    
    q.conn.lpush(queue, msg)
}

func (q *Queue) Receive(source, dest string) {
    
    msg := q.conn.rpoplpush(source, dest)
    // 做一些消费操作
}

当然这只是一个雏形,后面我们会把它变得更复杂一点。

消息的功能

既然是消息,那么它就需要携带一些信息。

通过上一小节的队列功能设想,我们发现每个方法都需要告诉队列要投递的队列名称,因此我们其实可以把队列名称附加到消息结构体上,这样一来,队列结构拿到消息之后,可以通过调用getChannel之类的方法来获取要投递的队列名称。此外消息结构体需要存储的信息不一定是一个字符串那么简单,可能是更复杂的多维信息,并且维持一定的格式也有助于规范使用者使用消息,方便程序处理——但是redis队列只支持传入字符串值,那么我们需要两个方法,把消息内容转化为字符串以及从字符串转化回来,也就是序列化,将其命名为marshalunmarshal

同时我们注意到在队列取出消息之后,还会执行消费操作。当我们传递不同的信息时,可能需要执行的消费动作也不同;为扩展考虑,不能每新增一种消息就往队列中添加新的消费动作代码,所以我们最好让消息结构本身自带一个消费方法,只需要队列取出消息之后调用这个方法进行消费即可,将其命名为resolve

不同消息需要创建不同的消息结构,但是他们都最好遵照我们前面定下的消息结构规范,这样队列可以统一使用同一种流程来处理消息。因此我们这里使用接口来约定结构。

type IMessage interface {
    
    Resolve()
    GetChannel() string
    Marshal() string
    Unmarshal(string) IMessage
}

第三方库的选择

为了实现上面两个结构体,我们需要一些第三方库的协助。

  • redis交互:这里笔者采用gomodule/redigo^3来实现。这个库自带维护一个redis连接池,可以为后面的多消费者扩展提供方便。
  • 消息序列化:序列化有多种选择,比如JSON、Protobuf、Gob等。笔者采用json-it
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值