这篇文章探讨的话题是 topic 和 channel 的消息持久化,以及topic 的创建和自动删除等。
临时 topic
nsq 有两种类型的 topic,一个是普通的topic,一个是临时topic,临时topic以“#ephemeral”结尾。相比普通topic,临时topic有以下特性:
- 消息不会持久化。nsq 中的消息会存储在内存(更具体一点,是 golang 中的 channel)和磁盘上,当内存中的队列满时,消息会存储到磁盘上。临时 topic 则是在内存满后直接丢弃消息。
- topic 自动删除。普通 topic 是不会被自动删除的,但是临时 topic 在没有消费 channel (非 golang 的channel)存在时,会自动删除该 topic。但是有个特殊例外,即临时 topic 先有了,但是此时还没有被任何消费 channel 消费,则数据会被保留,topic 也不会被删除(否则就乱套了)
和topic对应,channel 也分普通 channel 和 临时channel。临时channel的消息是不会被持久化的,而且在断开连接后,会被直接删除。
因而,就有了如下4种组合
普通 topic | 临时 topic | |
普通channel | topic 和 channel 的消息都会被持久化 | 1. topic 中的消息会被复制到 channel 2. topic 的消息不会持久化 3. channel的消息会持久化 4. 因为一直存在channel,所以这个 topic 无法被自动删除 |
临时channel | 1. topic 的消息会被持久化 2. channel 中的消息不会持久化 | 1. topic 和 channel 都不会被持久化 2. 一旦 channel 断开,则 topic 也会被自动删除 |
上面4中组合的逻辑非常好理解,因为它是两个维度的实现的叠加,关键得在于明白,channel 和 topic 各自维护了自己的消息队列(都有自己的内存和磁盘存储实现),且消息被发送到 topic 后,会发生消息的复制,即 topic 中的消息会被复制到所有的关联 channel 中。然后在这几个基础上,增加【临时】这个特质,就有了上面的4种组合。
特殊情况
当我们直接通过 nsqd 去订阅 topic 时,nsqd 会自动帮我们创建 topic,并同步给所有的 nsqlookup(如果不知道nsqd 和 nsqlookup,自行去官网看下nsq的设计)。在这种情况下,我们上面提到的逻辑特性都成立。
但是 nsq 作为一个去中心化的分布式消息队列,如果直连 nsqd,岂不是放弃了它的这个特性。所以一般情况下,都会通过nsqlookup去订阅消息,那订阅的逻辑就变成了:
- 通过 nsqlookup 查找哪些 nsqd 上有该 topic
- 连接到所有 nsqd 去订阅消息
通过这个路径去订阅消息的时候,其实就不会自动创建 topic。
试想一下如下一个业务场景,希望通过使用临时 topic 和 临时 channel 去实现在一个 topic 没人消费的时候,自动删除所有的相关数据。如果是通过 nsqlookup 去订阅消息的话,则有可能在生产者还未来得及发消息的时候,订阅就已经断开了,如果后续生产者发了一个消息,那就意味着这个 topic 不会被自动删除(因为 topic 是在生产的时候创建的,此时已经没有了消费者),除非重启 nsqd。
也许有人会发现,nsq 维护了很多http的 api 接口,其中有一个就是创建topic,那我只要在订阅之前手动调接口创建一下 topic,是不是就好了。是也不全是,如果我们调的是 nsqlookup 的topic创建接口,这个 topic 其实不会在任何 nsqd 上创建,所以依然会出现刚才这个情况。
而且,如果这个 topic 后续既没人消费也没人生产,则这个topic虽然不会在nsqd上有任何影响,但是会影响到 nsqlookup。如果只是一两个倒也罢了,但是如果成千上万甚至几十万个呢?这是临时 topic,我们预期会自动删除的,很有可能会被大量使用。
如果是调用的 nsqd 的创建 topic 接口,则会在 nsqd 上创建一个 topic,并被同步到 nsqlookup,此时在通过 nsqlookup 订阅消息,则会创建一个消费者 channel,从而可以缓解这个问题(这里之所以说是缓解,因为这两个毕竟不是一个事务)。