最近做一个小型项目,因为rabbitMQ等专业软件比较重,故团队决定采用redis实现一个轻量的队列。
目标
在这篇文章中,你可以获得:
redigo
包的基本用法- 初步认知
context
包的作用 - 了解一个功能模块的实现思路以及如何逐步完善的步骤
- Go特性(
select
、channel
和goroutine
)的利用
最终代码量大概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下的整个生产-消费流程:
队列、消息的设计思路
接下来我们使用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队列只支持传入字符串值,那么我们需要两个方法,把消息内容转化为字符串以及从字符串转化回来,也就是序列化,将其命名为marshal
和unmarshal
。
同时我们注意到在队列取出消息之后,还会执行消费操作。当我们传递不同的信息时,可能需要执行的消费动作也不同;为扩展考虑,不能每新增一种消息就往队列中添加新的消费动作代码,所以我们最好让消息结构本身自带一个消费方法,只需要队列取出消息之后调用这个方法进行消费即可,将其命名为resolve
。
不同消息需要创建不同的消息结构,但是他们都最好遵照我们前面定下的消息结构规范,这样队列可以统一使用同一种流程来处理消息。因此我们这里使用接口来约定结构。
type IMessage interface {
Resolve()
GetChannel() string
Marshal() string
Unmarshal(string) IMessage
}
第三方库的选择
为了实现上面两个结构体,我们需要一些第三方库的协助。
- redis交互:这里笔者采用
gomodule/redigo
^3来实现。这个库自带维护一个redis连接池,可以为后面的多消费者扩展提供方便。 - 消息序列化:序列化有多种选择,比如JSON、Protobuf、Gob等。笔者采用
json-it