MetaQ是一款分布式、队列模型的消息中间件。基于发布订阅模式,有Push和Pull两种消费方式,支持严格的消息顺序,亿级别的堆积能力,支持消息回溯和多个维度的消息查询。metaq是rocketmq的开源版本,
rocketmq的一些文档:https://help.aliyun.com/document_detail/44397.html?spm=a2c4g.95837.0.0.3db95ac4zlV500
RocketMQ的架构包括四个主要组件:Name Server、Broker、Producer和Consumer。
Topic表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。
RocketMQ的架构支持水平扩展和高可用性,可以通过添加Broker节点来扩展集群的处理能力,并且通过Master-Slave模式来保证消息的可靠性和高可用性。RocketMQ通过Topic完成消息的发布和订阅。消息生产者将消息发送到Topic中,而消息消费者则通过订阅该Topic来消费消息
到底什么时候该用Topic,什么时候该用Tag?
RocketMQ的tag是用于消息标记的一种属性,可以更精细地控制消息的消费。在发送消息时,可以为每条消息设置tag,消费者在订阅消息时可以指定tag来只消费特定标记的消息,从而避免消费无用消息,提高系统性能。例如,对于一个订单系统,可以设置不同的tag来表示不同的订单状态,消费者只订阅感兴趣的订单状态的消息。
从以下几个方面进行判断:
● 消息类型是否一致:如普通消息、事务消息、定时(延时)消息、顺序消息,不同的消息类型使用不同的Topic,无法通过Tag进行区分。
● 业务是否相关联:没有直接关联的消息,如淘宝交易消息,京东物流消息使用不同的Topic进行区分;而同样是天猫交易消息,电器类订单、女装类订单、化妆品类订单的消息可以用Tag进行区分。
● 消息优先级是否一致:如同样是物流消息,盒马必须小时内送达,天猫超市24小时内送达,淘宝物流则相对会慢一些,不同优先级的消息用不同的Topic进行区分。
● 消息量级是否相当:有些业务消息虽然量小但是实时性要求高,如果跟某些万亿量级的消息使用同一个Topic,则有可能会因为过长的等待时间而“饿死”,此时需要将不同量级的消息进行拆分,使用不同的Topic。
总的来说,针对消息分类,您可以选择创建多个Topic,或者在同一个Topic下创建多个Tag。但通常情况下,不同的Topic之间的消息没有必然的联系,而Tag则用来区分同一个Topic下相互关联的消息,例如全集和子集的关系、流程先后的关系。
场景示例:
以天猫交易平台为例,订单消息和支付消息属于不同业务类型的消息,分别创建Topic_Order和Topic_Pay,其中订单消息根据商品品类以不同的Tag再进行细分,例如电器类、男装类、女装类、化妆品类等被各个不同的系统所接收。
在RocketMQ中,NameServer充当了注册中心的角色,它维护了所有可用的RocketMQ broker节点的信息,并为producer和consumer提供服务发现。当producer和consumer启动时,只需要指定NameServer的地址,NameServer就会将可用的broker节点的信息告诉它们,以便它们可以与相应的broker建立连接并进行消息传递。因此,用户只需要关注NameServer的地址,而不需要手动指定broker的地址。在某些情况下,RocketMQ 的注册中心可以出现数据不一致性,但必须保证高可用,就算是返回了包含不实的信息的结果也比什么都不返回要好,如果采用zookeeper来提供服务注册发现,如果某次选举时间过长(30 ~ 120s),那么将导致长时间获取不到任务服务的信息,造成严重的后果。因此,RocketMQ自制的NameServer实现的是AP(可用性 分区容错性)。[附:分区容错性”指分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务]
当出现消费者对某条消息重复消费的情况时,重复消费的结果与消费一次的结果是相同的,并且多次消费并未对业务系统产生任何负面影响,那么这个消费者的处理过程就是幂等的。
例如,在支付场景下,消费者消费扣款消息,对一笔订单执行扣款操作,扣款金额为100元。如果因网络不稳定等原因导致扣款消息重复投递,消费者重复消费了该扣款消息,但最终的业务结果是只扣款一次,扣费100元,且用户的扣款记录中对应的订单只有一条扣款流水,不会多次扣除费用。那么这次扣款操作是符合要求的,整个消费过程实现了消费幂等。
在互联网应用中,尤其在网络不稳定的情况下,消息队列RocketMQ版的消息有可能会出现重复。如果消息重复会影响您的业务处理,请对消息做幂等处理。
消息重复的场景如下:
● 发送时消息重复当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或者客户端宕机,导致服务端对客户端应答失败。 如果此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同但Message ID不同的消息。
● 投递时消息重复消息消费的场景下,消息已 投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。为了保证消息至少被消费一次,消息队列RocketMQ版的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且Message ID也相同的消息。
● 负载均衡时消息重复(包括但不限于网络抖动、Broker重启以及消费者应用重启) 当消息队列RocketMQ版的Broker或客户端重启、扩容或缩容时,会触发Rebalance,此时消费者可能会收到少量重复消息。
因为不同的Message ID对应的消息内容可能相同,有可能出现冲突(重复)的情况,所以真正安全的幂等处理,不建议以Message ID作为处理依据。最好的方式是以业务唯一标识作为幂等处理的关键依据,而业务的唯一标识可以通过消息Key设置。
以支付场景为例,可以将消息的Key设置为订单号,作为幂等处理的依据。具体代码示例如下:
Message message = new Message();
message.setKey("ORDERID_100");
SendResult sendResult = producer.send(message);
接入:
metaqJava客户端接入
<dependency>
<groupId>com.taobao.metaq.finalgroupId>
<artifactId>metaq-clientartifactId>
<version>4.3.2.Finalversion>
dependency>
rocketmq客户端接入:
<dependency>
<groupId>org.apache.rocketmqgroupId>
<artifactId>rocketmq-clientartifactId>
<version>4.5.2version>
dependency>
生产者:
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;
public class MQproducer {
private DefaultMQProducer producer;
public MQproducer() throws MQClientException {
producer = new DefaultMQProducer("producer_group");
producer.setNamesrvAddr("localhost:9876");
producer.start();
}
public void send(String topic, String message) throws MQClientException, InterruptedException, RemotingException, MQBrokerException {
Message msg = new Message(topic, "tag", message.getBytes());
SendResult sendResult = producer.send(msg);
System.out.println("Send message success." + sendResult);
}
public void shutdown() {
producer.shutdown();
}
}
消费者:
public class MQconsumer {
private DefaultMQPushConsumer consumer;
public MQconsumer() throws MQClientException {
consumer = new DefaultMQPushConsumer("consumer_group");
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("topic", "*");
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
System.out.println(Thread.currentThread().getName() + " Receive New Messages: " + msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();
}
public void shutdown() {
consumer.shutdown();
}
}
main:
public class Main {
public static void main(String[] args) throws MQBrokerException, RemotingException, InterruptedException, MQClientException {
MQproducer producer = new MQproducer();
producer.send("topic", "hello world");
producer.shutdown();
MQconsumer consumer = new MQconsumer();
TimeUnit.MINUTES.sleep(1);
consumer.shutdown();
}
}
如果订阅多个tag,写法:
consumer.subscribe("TopicB", "Tag1||Tag2", new MessageListener() {
public Action consume(Message message, ConsumeContext context) {
System.out.println(message.getMsgID());
return Action.CommitMessage;
}
});
使用metaq接入的代码示例:
public class Producer {
public static void main(String[] args) throws MQClientException, InterruptedException {
/**
* 一个应用创建一个Producer,由应用来维护此对象,可以设置为全局对象或者单例
* 注意:ProducerGroupName需要由应用来保证唯一
* ProducerGroup这个概念发送普通的消息时,作用不大,但是发送分布式事务消息时,比较关键,
* 因为服务器会回查这个Group下的任意一个Producer
*/
MetaProducer producer = new MetaProducer("manhongTestPubGroup");
/**
* Producer对象在使用之前必须要调用start初始化,初始化一次即可
* 注意:切记不可以在每次发送消息时,都调用start方法
*/
producer.start();
/**
* 下面这段代码表明一个Producer对象可以发送多个topic,多个tag的消息。
* 注意:send方法是同步调用,只要不抛异常就标识成功。但是发送成功也可会有多种状态,
* 例如消息写入Master成功,但是Slave不成功,这种情况消息属于成功,但是对于个别应用如果对消息可靠性要求极高,
* 需要对这种情况做处理。另外,消息可能会存在发送失败的情况,失败重试由应用来处理。
*/
try {
for (int i = 0; i < 20; i++) {
{
Message msg = new Message("Jodie_topic_1023",// topic
"TagA",// tag
"OrderID001",// key,消息的Key字段是为了唯一标识消息的,方便运维排查问题。如果不设置Key,则无法定位消息丢失原因。
("Hello MetaQ").getBytes());// body
SendResult sendResult = producer.send(msg);
System.out.println(sendResult);
}
{
Message msg = new Message("TopicTest2",// topic
"TagB",// tag
"OrderID0034",// key
("Hello MetaQ").getBytes());// body
SendResult sendResult = producer.send(msg);
System.out.println(sendResult);
}
{
Message msg = new Message("TopicTest3",// topic
"TagC",// tag
"OrderID061",// key
("Hello MetaQ").getBytes());// body
SendResult sendResult = producer.send(msg);
System.out.println(sendResult);
}
}
} catch (Exception e) {
e.printStackTrace();
}
/**
* 应用退出时,要调用shutdown来清理资源,关闭网络连接,从MetaQ服务器上注销自己
* 注意:我们建议应用在JBOSS、Tomcat等容器的退出钩子里调用shutdown方法
*/
producer.shutdown();
}
}
消息顺序消费,要保证消息顺序消费,同一个queue就只能被一个消费者所消费,因此对broker中消费队列加锁是无法避免的。同一时刻,一个消费队列只能被一个消费者消费,消费者内部,也只能有一个消费线程来消费该队列。即,同一时刻,一个消费队列只能被一个消费者中的一个线程消费
RocketMQ支持发送顺序消息,保证同一队列内消息的顺序消费。所谓顺序消息,就是指消息在生产者端按照一定顺序发送到MQ中,而消费者端接收到的消息也是按照相同顺序进行消费。
在RocketMQ中,顺序消息的实现需要保证以下两个条件:
顺序生产消息代码:
public void sendBySequence() throws MQBrokerException, RemotingException, InterruptedException, MQClientException {
String[] tags = new String[]{"TagA", "TagB", "TagC", "TagD", "TagE"};
for (int i = 0; i < 100; i++) {
// 订单ID相同的消息要有序
int orderId = i % 10;
Message msg =
new Message("TopicTest", tags[i % tags.length], "KEY" + i,
("Hello MetaQ " + i).getBytes());
//selector – 消息队列选择器,通过它我们可以获得目标消息队列以将消息传递到目标队列。
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Integer id = (Integer) arg;
int index = id % mqs.size();
return mqs.get(index);
}
}, orderId);
System.out.println(sendResult);
}
}
顺序消费消息代码:
consumer.subscribe("TopicTest", "TagA || TagC || TagD");
consumer.registerMessageListener(new MessageListenerOrderly() {
AtomicLong consumeTimes = new AtomicLong(0);
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
context.setAutoCommit(true);
System.out.println(Thread.currentThread().getName() + " Receive New Messages: " + msgs);
this.consumeTimes.incrementAndGet();
if ((this.consumeTimes.get() % 2) == 0) {
return ConsumeOrderlyStatus.SUCCESS;
} else if ((this.consumeTimes.get() % 3) == 0) {
return ConsumeOrderlyStatus.ROLLBACK;
} else if ((this.consumeTimes.get() % 4) == 0) {
return ConsumeOrderlyStatus.COMMIT;
} else if ((this.consumeTimes.get() % 5) == 0) {
context.setSuspendCurrentQueueTimeMillis(3000);
return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
return ConsumeOrderlyStatus.SUCCESS;
}