diskqueue是nsq消息持久化的核心,内容较多,故分为多篇
1. diskqueue第一篇 - 是什么,为什么需要它,整体架构图,对外接口
2. diskqueue第二篇 - 元数据文件,数据文件,启动入口,元数据文件的读写及保存
3. diskqueue第三篇 - 数据定义详解,运转核心ioloop()源码详解
4. diskqueue第四篇 - 怎么写入消息,怎么对外发送消息
5. diskqueue第五篇 - 追尾检测,错误处理,如何正常关闭
6. diskqueue第六篇 - 如何使用diskqueue,示例
经过前面5篇博客的介绍,大家对diskqueue的整体框架、内部原理应该很清楚了,这篇博客我们讲如何使用diskqueue,毕竟学习它是为了更好地使用。以nsq中实际使用为例,希望大家能看得更清楚一些
1. 调用diskqueue.New()函数来创建一个新的diskqueue
2. 调用diskqueue.Put()函数往diskqueue中写入消息,需在select中执行
3. 调用diskqueue.ReadChan()函数从diskqueue中接收消息,需在select中执行
4. 调用diskqueue.Empty()函数+diskqueue.Delete()函数来删除一个diskqueue
5. 调用diskqueue.Close()函数正常关闭一个diskqueue
示例代码,源码位置在nsq/nsqd/channel.go
// 新建一个channel
func NewChannel(topicName string, channelName string, nsqd *NSQD, deleteCallback func(*Channel)) *Channel {
// 省略无关代码
c.backend = diskqueue.New(
backendName,
nsqd.getOpts().DataPath,
nsqd.getOpts().MaxBytesPerFile,
int32(minValidMsgLength),
int32(nsqd.getOpts().MaxMsgSize)+minValidMsgLength,
nsqd.getOpts().SyncEvery,
nsqd.getOpts().SyncTimeout,
dqLogf,
// 省略无关代码
}
上面的NewChannel()函数是创建channel,内部调用了diskqueue.New()来创建一个新的diskqueue,内部参数不再一一解释了
示例:topic往channel中压入消息时会调用put()函数,源码位置在nsq/nsqd/channel.go
// 压入一个新消息
func (c *Channel) put(m *Message) error {
select {
case c.memoryMsgChan <- m: // 先尝试压入内存队列
default:
err := writeMessageToBackend(m, c.backend) // 不成功的话则压入持久化队列
c.nsqd.SetHealth(err)
if err != nil {
c.nsqd.logf(LOG_ERROR, "CHANNEL(%s): failed to write message to backend - %s", c.name, err)
return err
}
}
return nil
}
put()函数内部:先尝试往内存队列中压入,如果不成功,则调用writeMessageToBackend()函数压入到持久化队列,即diskqueue
再来看writeMessageToBackend()函数,源码位置在nsq/nsqd/message.go
// 把指定消息写入持久化队列
func writeMessageToBackend(msg *Message, bq BackendQueue) error {
// 使用buffpoll作为写入缓冲区
buf := bufferPoolGet()
defer bufferPoolPut(buf)
// 先把消息写入buff
_, err := msg.WriteTo(buf)
if err != nil {
return err
}
// 再往队列中写
return bq.Put(buf.Bytes())
}
函数内部先把消息序列化成[]byte,再调用diskqueue.Put()函数写入
示例:客户端连接从持久化队列中接收消息,往客户端推送
源码位置:nsq/nsqd/protocol_v2.go的messagePump()函数
// 客户端连接的消息循环
func (p *protocolV2) messagePump(client *clientV2, startedChan chan bool) {
var backendMsgChan <-chan []byte // 持久化队列的通道(只读)
// 省略无关代码
backendMsgChan = subChannel.backend.ReadChan()
select {
// 省略无关代码
case b := <-backendMsgChan: // 持久化队列中有消息
if sampleRate > 0 && rand.Int31n(100) > sampleRate {
continue
}
}
}
可以看到messagePump()函数中的backendMsgChan会被赋值为diskqueue.ReadChan()的返回值,这是个只读的通道,然后在select中读取
示例:nsq的channel关闭时会调用diskqueue的Empty()和Delete()函数
源码位置:nsq/nsqd/channel.go的Delete()函数
// 删除channel的处理
func (c *Channel) Delete() error {
return c.exit(true)
}
函数内部调用了exit()函数,参数为true表删除,再来看exit()函数的实现
// channel退出操作(删除或关闭channel时调用)
func (c *Channel) exit(deleted bool) error {
// 省略无关代码......
// 删除channel时的操作
if deleted {
c.Empty() // 清空消息(清空内存队列,清空持久化队列->调用Empty()函数)
return c.backend.Delete()
}
// 关闭channel时的操作
c.flush() // 把数据刷到磁盘
return c.backend.Close()
}
可以看到参数deleted为true时,有两步操作
第一步:调用Empty()函数,该函数内部调用了diskqueue.Empty()函数进行清空操作
第二步:调用diskqueue.Delete()函数
示例: nsq的channel关闭时会调用diskqueue.Delete()函数
源码位置:nsq/nsqd/channel.go的Close()函数
// 关闭channel的处理(nsqd关闭时才调用)
func (c *Channel) Close() error {
return c.exit(false)
}
函数内部调用了exit()函数,参数为false
具体exit()函数的实现,上面第4模块刚刚讲过,大家可以看到函数最下面调用了diskqueue.Close()函数,不再重复了
本篇博客主要讲如何使用diskqueue,并对各种用法都给出了示例,示例取自nsq。只要大家能仔细看下nsq的使用例子,相信以后自己使用时会得心应手
至此,diskqueue全部讲完