深入浅出消息队列----【消息过滤原理】
本文仅是文章笔记,整理了原文章中重要的知识点、记录了个人的看法
文章来源:编程导航-鱼皮【yes哥深入浅出消息队列专栏】
过滤放在 Broker 还是 Consumer?
如果消息过滤放在 Consumer 上,消费者拿到全部的消息,如果是需要消费的就进行处理,如果不是的话就不处理,这样岂不是加大了消费者的压力?让消费者处理了大量的无用消息吗?
因此,过滤要放在 Broker 里。
也就说消费者去拉取消息的时候,Broker 只给消费者想要的消息,自动把不需要的那部分过滤了。
Tag 和 SQL
那 RocketMQ 支持怎样的过滤方式呢?
官方提供了两种方式,分别是:
- Tag 标签过滤
- SQL 属性过滤
Tag 标签过滤
这个是生产上最常用消息的过滤方式。
使用起来也很简单,就是在生产者构造消息的时候,给消息打上标记(每条消息仅允许设置一个 Tag标签)。
message.setTags("TagYes");
这样就给消息打上了标签,然后消费者在订阅主题的时候,指定自己想要接受的 tag 即可。比如:
consumer.subscribe("TopicA", "TagYes");
然后这个订阅关系会随着心跳发送给 Broker,这样 Broker 就知晓消费者们想要过滤的 Tag。
当对应消费者去 Broker 拉取消息的时候,就可以过滤掉不要消息。
交易相关的消息都发往 Trader_Topic 这个主题,其中分别有 order(订单消息)、pay(支付消息)、logistics(物流消息)这三类消息。
然后物流、支付、交易成功率分析、实时计算这四个系统都订阅了 Trader_Topic 这个主题,但并不是所有的消息都需要。
比如物流系统仅关注物流消息,支付系统仅关注支付消息,交易成功率分析则是关注订单和支付两个消息,实时计算系统则是想要处理所有的消息。
这时候就可以利用 Tag 来给不同的消息打标,区分这三类消息,让对应的系统仅处理对应的子消息即可。
消费者订阅 Tag 有多个规则:
- 单 Tag 匹配,物流系统的订阅,
consumer.subscribe("Trader_Topic", "logistics")
。 - 多 Tag 匹配,交易成功率分析系统的订阅,
consumer.subscribe("Trader_Topic", "order || pay")
,不同 Tag 间使用两个竖线(||)隔开,多个 Tag 之间为或的关系,任意一个 Tag 命中都会被消费者拉取。 - 全匹配,实时计算系统的订阅,使用星号(*)作为全匹配表达式,表示全都要。
SQL 属性过滤
RocketMQ 提供了一种更高级的过滤方式:SQL 属性过滤。
在发送消息的时候,生产者可以给消息设置属性,属性是一个键值对(key/value),然后消费者在订阅时可以设置 SQL 过滤表达式来过滤多个属性。
以下代码就是生产者在发送消息时给消息设置了一个属性,同时也设置了 Tag。
message.setTags("TagYes");
message.putUserProperty("a", "1");
然后消费者在订阅时候可以指定 SQL 过滤条件:
consumer.subscribe("TopicA", MessageSelector.bySql("(TAGS is not null and TAGS in ('TagA', 'TagYes'))" + "and (a is not null and a between 0 and 3)"));
可以看到,SQL 过滤条件比较灵活,不仅可以通过是否有值来过滤,还可以根据值的内容且能结合多个条件(消息可以设置多属性)一起来判断。
SQL 里面利用 TAGS 这个属性就能用来判断 tag 的值,所以 SQL 属性过滤功能其实是包含了 Tag 过滤的。
SQL 属性过滤式使用 SQL92 语法来作为过滤规则表达式的,我们来看下官网上列举的语法规范图:
如果 SQL 表达式计算出现异常,比如类型不一致等情况,消息默认会被过滤,不会被消费者消费。
Tag 过滤原理解析
首先,生产者在构建消息时候设置了 tag,实际上就是往消息属性设置了一个键值对,key 的值就是 ‘TAGS’(也因此 SQL 可以覆盖 tag 过滤,因为本质上就是通过属性来判断的)。
生产者发送消息到 Broker,遵循正常消息的存储逻辑,写到 commitlog 里,且分发到 ConsumeQueue。
每条消息映射到 ConsumeQueue 的内容都是 commilog offset、size、tag hashcode。
而利用 tag hashcode 就能快速的判断当前要取的消息打了哪个 tag,不需要取 commitlog 拉取真正的消息内容再解析,这样就高效的完成了 tag 的过滤。
而 Broker 是如何得知当前来拉消息的消费者要订阅的是哪个 tag ?
是从消费者定时给 Broker 的心跳包里面知晓的,消费者启动后就会定时上报自己的订阅信息给 Broker,订阅信息结构如下:
其中的 tagsSet 存储的时订阅的 tag,而 codeSet 就是 tag 对应的 hash 值,也就是 ConsumeQueue 里的 tag hashcode。
所以 Broker 在消费者来拉消息的时候,利用请求的 offset 的从 ConsumerQueue 能直接得到消息的 tag hashcode,且本地已经存储了当前消费者的订阅信息,因此无需解析消息体也无需取哪所要消费者的订阅值,直接利用 hashcode 对比当前消息是否应该被改消费者拉取。
如果 hashcode 不一致,则跳过这条消息以达到过滤的作用。
为什么要利用 tag hashcode 来判断,不能直接利用 tag 字符来判断吗?
因为 ConsumerQueu 的设计就是定长的,而 tag 字符的长度不能控制,但是 hash 的长度是可以控制的,所以采用哈希。
哈希是会碰撞的,也就是说不同的 tag 可能产生一样的 hash 值,这个时候不就出现问题了吗?
显然,RocketMQ 也考虑到了这一点,当消费者拉取到经过 Broker 过滤的消息后,它还会通过 tag 判断一百年,确保过滤掉因为哈希碰撞导致的消息。
SQL 属性过滤原理解析
本质上和 tag 过滤是一样的,差别就在于SQL 属性过滤需要从 commitlog 获取消息,然后解析其中的属性,紧接着再做 SQL 匹配,不匹配的消息被过滤,校验通过的消息被消费者拉取到本地。
此时因为不存在 hash 碰撞的情况,所以消费者本地不需要再进行二次校验。
可以确定的事:SQL 属性过滤的性能比 tag 过滤差。
首先 SQL 解析比 tag 直接 equals 对比慢,其次 SQL 属性过滤还需要从 commitlog 获取消息体,解析里面的属性再比对,而 tag 过滤直接拿 ConsumerQueue 里的内容就能判断,这里又慢了一波。
所以使用 SQL 过滤的时候,需要考虑下性能。不过一般场景用 tag 就够了。