我们前面提到,一个主题只能有一种消息类型,那么只要是同一个消息类型,我们就可以发送到同一个主题。但这些消息中可能涉及到多个业务,他们对应的消费者的消费逻辑可能不一样。如果我们单个消费者接收该主题的所有消息进行消费,那么在我们的消费逻辑中需要很多的判断来判断其走哪个业务逻辑,无疑增加了消费逻辑的复杂度。使用消息过滤的方式,即可在 MQ 服务端进行消息的过滤,将消息投递给特定的消费者,使每个消费者只需关注与自身相关的消息进行消费。
tag 标签过滤方式为,生产者发送消息时为消息设置一个 Tag 标签,消费者在消费时匹配该 Tag ,如果匹配则消费发送给该消费者,如果不匹配则不发送。
Message message = messageBuilder.setTopic("topic")
//设置消息索引键,可根据关键字精确查找某条消息。
.setKeys("messageKey")
//设置消息Tag,用于消费端根据指定Tag过滤消息。
//该示例表示消息的Tag设置为"TagA"。
// Tag使用可见字符,建议长度不超过128字符
.setTag("TagA")
//消息体。
.setBody("messageBody".getBytes())
.build();
String topic = "Your Topic";
//只订阅消息标签为"TagA"的消息。
FilterExpression filterExpression = new FilterExpression("TagA", FilterExpressionType.TAG);
pushConsumer.subscribe(topic, filterExpression);
2.2 匹配多个Tag标签
String topic = "Your Topic";
//只订阅消息标签为"TagA"、"TagB"或"TagC"的消息。
FilterExpression filterExpression = new FilterExpression("TagA||TagB||TagC", FilterExpressionType.TAG);
pushConsumer.subscribe(topic, filterExpression);
2.3 匹配所有消息
String topic = "Your Topic";
//使用Tag标签过滤消息,订阅所有消息。
FilterExpression filterExpression = new FilterExpression("*", FilterExpressionType.TAG);
pushConsumer.subscribe(topic, filterExpression);
SQL属性过滤是 RocketMQ 提供的高级消息过滤方式,通过生产者为消息设置的属性(Key)及属性值(Value)进行匹配。生产者在发送消息时可设置多个属性,消费者订阅时可设置SQL语法的过滤表达式过滤多个属性。
注:Tag是一种系统属性,所以SQL过滤方式也兼容Tag标签过滤。在SQL语法中,Tag的属性名称为TAGS。
Message message = messageBuilder.setTopic("topic")
//设置消息索引键,可根据关键字精确查找某条消息。
.setKeys("messageKey")
//设置消息Tag,用于消费端根据指定Tag过滤消息。
//该示例表示消息的Tag设置为"messageTag"。
.setTag("messageTag")
//消息也可以设置自定义的分类属性,例如环境标签、地域、逻辑分支。
//该示例表示为消息自定义一个属性,该属性为地域,属性值为杭州。
.addProperty("Region", "Hangzhou")
//消息体。
.setBody("messageBody".getBytes())
.build();
String topic = "topic";
//只订阅地域属性为杭州的消息。
FilterExpression filterExpression = new FilterExpression("Region IS NOT NULL AND Region='Hangzhou'", FilterExpressionType.SQL92);
simpleConsumer.subscribe(topic, filterExpression);
2.2 同时根据多个自定义属性匹配消息
String topic = "topic";
//只订阅地域属性为杭州且价格属性大于30的消息。
FilterExpression filterExpression = new FilterExpression("Region IS NOT NULL AND price IS NOT NULL AND Region = 'Hangzhou' AND price > 30", FilterExpressionType.SQL92);
simpleConsumer.subscribe(topic, filterExpression);
2.3 匹配Topic中的所有消息,不进行过滤
String topic = "topic";
//订阅所有消息。
FilterExpression filterExpression = new FilterExpression("True", FilterExpressionType.SQL92);
simpleConsumer.subscribe(topic, filterExpression);
语法 | 说明 | 示例 |
---|---|---|
IS NULL | 判断属性不存在。 | a IS NULL :属性a不存在。 |
IS NOT NULL | 判断属性存在。 | a IS NOT NULL:属性a存在。 |
> >= < <= | 用于比较数字,不能用于比较字符串,否则消费者客户端启动时会报错。 说明 可转化为数字的字符串也被认为是数字。 | a IS NOT NULL AND a > 100:属性a存在且属性a的值大于100。 a IS NOT NULL AND a > ‘abc’:错误示例,abc为字符串,不能用于比较大小。 |
BETWEEN xxx AND xxx | 用于比较数字,不能用于比较字符串,否则消费者客户端启动时会报错。等价于>= xxx AND <= xxx。表示属性值在两个数字之间。 | a IS NOT NULL AND (a BETWEEN 10 AND 100):属性a存在且属性a的值大于等于10且小于等于100。 |
NOT BETWEEN xxx AND xxx | 用于比较数字,不能用于比较字符串,否则消费者客户端启动会报错。等价于< xxx OR > xxx,表示属性值在两个值的区间之外。 | a IS NOT NULL AND (a NOT BETWEEN 10 AND 100):属性a存在且属性a的值小于10或大于100。 |
IN (xxx, xxx) | 表示属性的值在某个集合内。集合的元素只能是字符串。 | a IS NOT NULL AND (a IN (‘abc’, ‘def’)):属性a存在且属性a的值为abc或def。 |
= <> | 等于和不等于。可用于比较数字和字符串。 | a IS NOT NULL AND (a = ‘abc’ OR a<>‘def’):属性a存在且属性a的值为abc或a的值不为def。 |
AND OR | 逻辑与、逻辑或。可用于组合任意简单的逻辑判断,需要将每个逻辑判断内容放入括号内。 | a IS NOT NULL AND (a > 100) OR (b IS NULL):属性a存在且属性a的值大于100或属性b不存在。 |
由于SQL属性过滤是生产者定义消息属性,消费者设置SQL过滤条件,因此过滤条件的计算结果具有不确定性,服务端的处理方式如下:
- 异常情况处理:如果过滤条件的表达式计算抛异常,消息默认被过滤,不会被投递给消费者。例如比较数字和非数字类型的值。
- 空值情况处理:如果过滤条件的表达式计算值为null或不是布尔类型(true和false),则消息默认被过滤,不会被投递给消费者。例如发送消息时未定义某个属性,在订阅时过滤条件中直接使用该属性,则过滤条件的表达式计算结果为null。
- 数值类型不符处理:如果消息自定义属性为浮点型,但过滤条件中使用整数进行判断,则消息默认被过滤,不会被投递给消费者。
消息按照达到服务器的先后顺序被存储到队列中,理论上每个队列都支持无限存储。但实际情况下,服务端节点物理存储空间有限,消息时无法永久存储的。
RocketMQ 使用存储时长作为消息存储的依据,即每个节点对外承诺消息的存储时长。在存储时长范围内的消息都会被保留,无论消息是否被消费;超过时长限制的消息则会被清理掉。
消息保存时长并不能完整控制消息的实际保存时间,因为消息存储仍然使用本地磁盘,本地磁盘空间不足时,为保证服务稳定性消息仍然会被强制清理,导致消息的实际保存时长小于设置的保存时长。 意味着当消费能力严重不足时,消息会产生大量堆积,导致没有可以磁盘空间,进一步导致未消费消息被清理机制清理掉。
RocketMQ 消息默认存储在本地磁盘文件中,存储文件的根目录由 Broker 配置参数 storePathRootDir 决定,其默认路径为:~/store。用户路径下的 store 目录。该目录的目录结构如下:
├── timerwheel
├── timerlog
│ └── 00000000000000000000
├── lock
├── index
│ └── 20230908092441339
├── consumequeue
│ ├── TestTopic
│ │ ├── 7
│ │ │ └── 00000000000000000000
│ │ └── 4
│ │ └── 00000000000000000000
│ ├── SCHEDULE_TOPIC_XXXX
│ │ ├── 2
│ │ │ └── 00000000000000000000
│ │ └── 17
│ │ └── 00000000000000000000
│ ├── rmq_sys_wheel_timer
│ │ └── 0
│ │ └── 00000000000000000000
│ ├── RMQ_SYS_TRANS_OP_HALF_TOPIC
│ │ └── 0
│ │ └── 00000000000000000000
│ ├── RMQ_SYS_TRANS_HALF_TOPIC
│ │ └── 0
│ │ └── 00000000000000000000
│ ├── rmq_sys_REVIVE_LOG_DefaultCluster
│ │ ├── 7
│ │ │ └── 00000000000000000000
│ │ ├── 6
│ │ │ └── 00000000000000000000
│ │ ├── 5
│ │ │ └── 00000000000000000000
│ │ ├── 4
│ │ │ └── 00000000000000000000
│ │ ├── 3
│ │ │ └── 00000000000000000000
│ │ ├── 2
│ │ │ └── 00000000000000000000
│ │ ├── 1
│ │ │ └── 00000000000000000000
│ │ └── 0
│ │ └── 00000000000000000000
│ ├── %RETRY%MY_RETRY_GROUP_MY_NORMAL_TOPIC
│ │ └── 0
│ │ └── 00000000000000000000
│ ├── %RETRY%DLQ_CONSUMER_GROUP_%DLQ%MY_RETRY_GROUP
│ │ └── 0
│ │ └── 00000000000000000000
│ ├── MY_TRANSACTION_TOPIC
│ │ ├── 4
│ │ │ └── 00000000000000000000
│ │ └── 1
│ │ └── 00000000000000000000
│ ├── MY_NORMAL_TOPIC
│ │ ├── 7
│ │ │ └── 00000000000000000000
│ │ ├── 5
│ │ │ └── 00000000000000000000
│ │ ├── 4
│ │ │ └── 00000000000000000000
│ │ ├── 2
│ │ │ └── 00000000000000000000
│ │ └── 0
│ │ └── 00000000000000000000
│ ├── MY_FIFO_TOPIC
│ │ ├── 6
│ │ │ └── 00000000000000000000
│ │ ├── 5
│ │ │ └── 00000000000000000000
│ │ ├── 3
│ │ │ └── 00000000000000000000
│ │ ├── 2
│ │ │ └── 00000000000000000000
│ │ └── 1
│ │ └── 00000000000000000000
│ ├── MY_DELAY_TOPIC
│ │ ├── 6
│ │ │ └── 00000000000000000000
│ │ ├── 5
│ │ │ └── 00000000000000000000
│ │ ├── 3
│ │ │ └── 00000000000000000000
│ │ └── 1
│ │ └── 00000000000000000000
│ ├── %DLQ%MY_RETRY_GROUP
│ │ └── 0
│ │ └── 00000000000000000000
│ └── %DLQ%DLQ_CONSUMER_GROUP
│ └── 0
│ └── 00000000000000000000
├── config
│ ├── topics.json.bak
│ ├── topics.json
│ ├── timermetrics.bak
│ ├── timermetrics
│ ├── timercheck
│ ├── subscriptionGroup.json.bak
│ ├── subscriptionGroup.json
│ ├── delayOffset.json.bak
│ ├── delayOffset.json
│ ├── consumerOrderInfo.json.bak
│ ├── consumerOrderInfo.json
│ ├── consumerOffset.json.bak
│ ├── consumerOffset.json
│ ├── consumerFilter.json.bak
│ └── consumerFilter.json
├── compaction
│ ├── position-checkpoint.bak
│ └── position-checkpoint
├── commitlog
│ ├── 00000000001073741824
│ └── 00000000000000000000
├── checkpoint
├── abort.bak
└── abort
┌───────────────┬───────────────────────────────┬───────────────┬───────────────┐
│ Key HashCode │ Physical Offset │ Time Diff │ Next Index Pos│
│ (4 Bytes) │ (8 Bytes) │ (4 Bytes) │ (4 Bytes) │
├───────────────┴───────────────────────────────┴───────────────┴───────────────┤
│ Index Store Unit │
│ │
commitlog 文件夹下,00000000000000000000 代表了第一个日志数据文件,其最大大小为 1G = 1073741824;其起始偏移量为 0,当第一个文件写满,则生成第二个文件,其文件名为 00000000001073741824,其起始偏移量为 1073741824,依此类推。
在安全要求较高的服务器环境下,或者多项目组共享一个 RocketMQ 集群时,建议大家开启权限控制,这样可以防止多项目混用 RocketMQ 资源,也可以控制非法的访问请求。
plain_acl.yml 配置文件,在 ${RocketMQ_HOME}/conf/plain_acl.yml,其默认配置如下:
# 全局IP白名单
globalWhiteRemoteAddresses:
- 10.10.103.*
- 192.168.0.*
# 账号配置列表
accounts:
# Access Key 类似账号名
- accessKey: RocketMQ
# Secret Key
secretKey: 12345678
# 用户IP白名单列表
whiteRemoteAddress:
# 当前用户是否为管理员用户
admin: false
# 默认的Topic权限
defaultTopicPerm: DENY
# 默认的ConsumerGroup权限
defaultGroupPerm: SUB
# 各个Topic的权限
topicPerms:
- topicA=DENY
- topicB=PUB|SUB
- topicC=SUB
# 各个ConsumerGroup的权限
groupPerms:
# the group should convert to retry topic
- groupA=DENY
- groupB=PUB|SUB
- groupC=SUB
- accessKey: rocketmq2
secretKey: 12345678
whiteRemoteAddress: 192.168.1.*
# 如果是管理员用户,则拥有所有权限
admin: true
注1:特殊的权限,比如 UPDATE_AND_CREATE_TOPIC 等,只能由 admin 账号操作。
注2:对于某个资源(比如:topic),如果有其对应的权限配置,则使用配置的权限,如果没有配置则使用默认的权限。
注意:IP 白名单如果通过,则不会验证签名,如果你要使用签名方式认证,最好就不要设置对应的 IP 白名单。
通过 broker 的 aclEnable 配置来开启权限校验。
我们前面还提到建议关闭 autoCreateSubscriptionGroup (是否自动创建消费者分组)、autoCreateTopicEnable (是否自动创建Topic),这两个参数也都是 broker 的配置参数,我们在此一并配置了。
brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
# 以上为 broker.conf 的默认配置
# 开启权限验证
aclEnable = true
# 关闭自动创建Topic
autoCreateTopicEnable = false
# 关闭自动创建消费者分组
autoCreateSubscriptionGroup = false
关闭自动创建 Topic 后,生产者执行时将报没有对应主题的错误。
关闭自动创建消费者分组后,消费者示例可以成功启动,但其无法消费任何消息。
$> ./bin/mqshutdown proxy
$> ./bin/mqshutdown broker
$> nohup sh bin/mqbroker -n localhost:9876 -c conf/broker.conf --enable-proxy &
注:以上命令在 RocketMQ home 目录下执行的
DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName",
new AclClientRPCHook(new SessionCredentials("accessKey","secretKey"));
);
ClientConfiguration configuration = ClientConfiguration.newBuilder()
// endpoints 即为 proxy 的地址,多个用分号隔开。如:xxx:8081;xxx:8081
.setEndpoints(MyMQProperties.ENDPOINTS)
.setCredentialProvider(new StaticSessionCredentialsProvider("accessKey","secretKey"))
.build();
属性名 | 默认值 | 说明 |
---|---|---|
listenPort | 10911 | 接受客户端连接的监听端口 |
namesrvAddr | null | nameServer 地址 |
brokerIP1 | 网卡的 InetAddress | 当前 broker 监听的 IP |
brokerIP2 | 跟 brokerIP1 一样 | 存在主从 broker 时,如果在 broker 主节点上配置了 brokerIP2 属性,broker 从节点会连接主节点配置的 brokerIP2 进行同步 |
brokerName | null | broker 的名称 |
brokerClusterName | DefaultCluster | 当前 broker 所属的 Cluser 名称 |
brokerId | 0 | broker id, 0 表示 master, 其他的正整数表示 slave |
storePathCommitLog | $HOME/store/commitlog/ | 存储 commit log 的路径 |
storePathConsumerQueue | $HOME/store/consumequeue/ | 存储 consume queue 的路径 |
mapedFileSizeCommitLog | 1024 * 1024 * 1024(1G) | commit log 的映射文件大小 |
deleteWhen | 04 | 在每天的什么时间删除已经超过文件保留时间的 commit log |
fileReservedTime | 72 | 以小时计算的文件保留时间 |
brokerRole | ASYNC_MASTER | SYNC_MASTER/ASYNC_MASTER/SLAVE |
flushDiskType | ASYNC_FLUSH | SYNC_FLUSH/ASYNC_FLUSH SYNC_FLUSH 模式下的 broker 保证在收到确认生产者之前将消息刷盘。ASYNC_FLUSH 模式下的 broker 则利用刷盘一组消息的模式,可以取得更好的性能。 |