前言
本文主要侧重点在 java 的客户端,具体的如何搭建 RocketMQ 服务端不在本文讨论范围内
一、一些概念
1.1、rocketMQ 的组成
- name server 用来保存 Broker 相关 Topic 等元信息并给 Producer ,提供 Consumer 查找 Broker 信息。
- producer 消息生产者,负责产生消息
- consumer 消息消费者,负责消费消息
- push consumer:Consumer的一种,应用通常向Consumer对象注册一个Listener接口,一旦收到消息,Consumer对象立刻回调Listener接口方法
- pull consumer :Consumer的一种,应用通常主动调用Consumer的拉消息方法从Broker拉消息,主动权由应用控制
- Producer Group:多个 producer 可以同属于一个 Producer Group
- Consumer Group:多个consumer 可以同属于一个 Consumer Group,同个分组中的消费者一定要订阅相同的topic
- broker:消息中转角色,负责存储消息
1.2、消费方式
广播消费和集群消费
- 广播消费:一条消息被多个Consumer消费,即使这些 Consumer属于同一个 Consumer Group,消息也会被Consumer Group 中的每个 Consumer都消费一次。
- 集群消费:一个Consumer Group中的Consumer实例平均分摊消费消息。例如某个Topic有9条消息,其中一个Consumer Group有3个 实例(可能是3个进程或3台机器),那么每个实例只消费其中的3条消息。
1.3、消费者发送消息的方式
- 同步可靠:客户端阻塞,等待服务端响应结果,成功后,客户端继续运行
- 异步可靠:客户端不阻塞,设置一个钩子,待服务端响应成功后,调用钩子
- 单向:不关心服务端的响应结果,直接发给服务端就完事了
1.4、消息类型
- 普通消息
- 顺序消息
- 延时消息
- 批量消息
二、四种消息
2.1、普通消息的发送和消费
public class Test {
public static String PRODUCER_GROUP_NAME = "producer_group_1";
public static String CONSUMER_GROUP_NAME = "consumer_group_1";
public static String NAME_SERVER_HOST = "127.0.0.1:9876";
public static String TOPIC_NAME = "TEST_TOPIC";
public static void main(String[] args) throws Exception {
// 创建生产者实例
DefaultMQProducer producer = new DefaultMQProducer();
// 设置name server 信息
producer.setNamesrvAddr(NAME_SERVER_HOST);
// 设置 producer group
producer.setProducerGroup(PRODUCER_GROUP_NAME);
// 启动生产者实例
producer.start();
// 启动两个消费者
openConsumer("xfz1");
openConsumer("xfz2");
// 循环发送消息
for (int i = 1; i < 40; i++) {
Message msg = new Message();
msg.setTopic(TOPIC_NAME);
msg.setTags("tag1");
msg.setBody(("消息:" + i).getBytes());
// 使用生产者实例发送消息
producer.send(msg);
}
// 销毁生产者
producer.shutdown();
}
public static void openConsumer(String name) {
// 开启一个消费者线程
new Thread(() -> {
// 创建消费者实例
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
// 设置name server
consumer.setNamesrvAddr(NAME_SERVER_HOST);
// 设置消费者组
consumer.setConsumerGroup(CONSUMER_GROUP_NAME);
// 设置消费者name
consumer.setInstanceName(name);
// 订阅 topic
try {
consumer.subscribe(TOPIC_NAME, "tag1");
} catch (MQClientException e) {
e.printStackTrace();
}
// 设置钩子
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
for(MessageExt msg :msgs){
System.out.println("消费者:"+ consumer.getInstanceName()+" 消费了消息:"+new String(msg.getBody()));
}
// 睡眠500ms
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 启动consumer
try {
consumer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
}).start();
}
}
注意点:
- DefaultMQProducer、DefaultMQPushConsumer 都继承了 ClientConfig ,就是说,这两个类可以直接使用和设置 rocketMQ 的客户端配置,在上面的例子中,我只设置了name server 的host
- 发送消息时需要设置消息的 topic、tag、body
- DefaultMQPushConsumer 在有消息时,会调用我们设置好的钩子,即 MessageListenerConcurrently 的 consumeMessage方法
- Consumer 要设置实例名称
程序结果:
2.2、有序消息
2.2.1、rocketMQ 是如何做到有序的?
要了解这个问题,需要明白
- producer 和 message、 broker、queue 的关系
- consumer 和 message、broker、queue 的关系
producer 和 message、 broker、queue 的关系
broker 是消息 queue 的提供者,而 broker 支持分布式,即集群部署。当一个生产者实例发出了一条消息时,需要先选择一个 broker 的一个 queue。
consumer 和 message、broker、queue 的关系
而消费时,多个queue是同时拉取提交到消费者。
rocketMQ 如何做到有序
我们知道,同一个 queue 是可以做到 FIFO 的,如果我们想办法把所有消息投递到同一个 broker 的同一个 queue,自然消息就变成有序的了。但是,只是使用同一个 broker 的同一个 queue 就可以了吗?并不是,因为同一个 queue 的消息有可能被多个消费者的消费,而每个消费者又有可能有多个线程在消费,为了保持 queue 中的消息顺序消费,我们还需要保证同一时刻,只有一个消费者的一个消费线程在消费 queue。
消息置入同一个 queue 方案:rocketMQ 支持在生产者投递消息时,设置队列的选择算法,即含有 MessageQueueSelector 的 send 方法。
保证同一时刻,只有一个消费者的一个消费线程在消费:使用 DefaultMQPushConsumer 的 MessageListenerOrderly 这个监听器处理消息。
2.2.2、全局有序
所有消息全部进入同一个队列,消费时,只有一个消费者的一个线程可以消费这个队列的消息
生产者设置 MessageQueueSelector,选择 queue
// 使用生产者实例发送消息
try {
// producer.send(msg);
producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
return mqs.get(0);
}
},null);
} catch (Exception e) {
e.printStackTrace();
}
这里,select 返回的 queue,即为消息即将投递的 queue,我们直接把第一个 queue 返回,相当于所有的消息直接投递到第一个 queue
消费者顺序消费
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
for (MessageExt msg : msgs) {
System.out.println("消费者:" + consumer.getInstanceName() + " 消费了消息:" + new String(msg.getBody()) + " 消费线程:" + Thread.currentThread().getName()+" 消息所在队列id:"+msg.getQueueId());
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
全局有序demo代码
public class Test3 {
public static String PRODUCER_GROUP_NAME = "producer_group_1";
public static String CONSUMER_GROUP_NAME = "consumer_group_1";
public static String NAME_SERVER_HOST = "127.0.0.1:9876";
public static String TOPIC_NAME = "TEST_TOPIC";
public static void main(String[] args) throws Exception {
// 启动生产者
createProducer("prd1");
Thread.sleep(5000);
// 启动两个消费者
createConsumer("xfz1");
createConsumer("xfz2");
createConsumer("xfz3");
}
public static void createConsumer(String name) {
// 开启一个消费者线程
new Thread(() -> {
// 创建消费者实例
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
// 设置name server
consumer.setNamesrvAddr(NAME_SERVER_HOST);
// 设置消费者组
consumer.setConsumerGroup(CONSUMER_GROUP_NAME);
// 设置消费者name
consumer.setInstanceName(name);
consumer.setConsumeThreadMax(1);
consumer.setConsumeThreadMin(1);
// 订阅 topic
try {
consumer.subscribe(TOPIC_NAME, "tag1");
} catch (MQClientException e) {
e.printStackTrace();
}
// 设置钩子
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
for (MessageExt msg : msgs) {
System.out.println("消费者:" + consumer.getInstanceName() + " 消费了消息:" + new String(msg.getBody()) + " 消费线程:" + Thread.currentThread().getName()+" 消息所在队列id:"+msg.getQueueId());
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 启动consumer
try {
consumer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
}).start();
}
public static void createProducer(String name) {
new Thread(() -> {
// 创建生产者实例
DefaultMQProducer producer = new DefaultMQProducer();
// 设置name server 信息
producer.setNamesrvAddr(NAME_SERVER_HOST);
// 设置 producer group
producer.setProducerGroup(PRODUCER_GROUP_NAME);
// 设置生产者名称
producer.setInstanceName(name);
// 启动生产者实例
try {
producer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
// 循环发送消息
for (int i = 1; i < 10000; i++) {
Message msg = new Message();
msg.setTopic(TOPIC_NAME);
msg.setTags("tag1");
msg.setBody(("消息" + i).getBytes());
// 使用生产者实例发送消息
try {
// producer.send(msg);
producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
if (((Integer)arg & 1) == 1) {
// 奇数
return mqs.get(0);
}else {
// 偶数
return mqs.get(1);
}
}
}, i);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
2.2.3、局部有序
全局有序是局部有序的一种特殊情况,全局有序是所有的消息全部进入同一个 queue,这样所有的消息都会有序;而局部有序是某个场景下的一部分消息(比如id是偶数的消息,id小于1000的消息等等场景)进入同一个 queue,这样就可以保证在同一个场景下(同一个 queue)中,消息是有序的。
下面写一个demo,将 id 是奇数的消息和 id 是偶数的消息分别放入 queue,并消费
生产者设置 MessageQueueSelector,选择 queue
奇数放入第一个 queue,偶数放入第二个 queue。
producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
if ((num & 1) == 1) {
// 奇数
return mqs.get(0);
}else {
// 偶数
return mqs.get(1);
}
}
}, null);
2.3、延时消息
RocketMQ 支持发送延迟消息,但不支持任意时间的延迟消息的设置,仅支持内置预设值的延迟时间间隔的延迟消息。
预设值的延迟时间间隔为:1s、 5s、 10s、 30s、 1m、 2m、 3m、 4m、 5m、 6m、 7m、 8m、 9m、 10m、 20m、 30m、 1h、 2h
message 设置延时
msg.setDelayTimeLevel(3);
2.4、批量消息
批量发送消息可提高传递小消息的性能。同时也需要满足以下特征
- 批量消息要求必要具有同一topic、相同消息配置
- 不支持延时消息
- 建议一个批量消息最好不要超过1MB大小
代码:
List<Message> msgs = new ArrayList<>();
for (int i = 1; i < 40; i++) {
Message msg = new Message();
msg.setTopic(TOPIC_NAME);
msg.setTags("tag1");
msg.setBody(("消息:" + i).getBytes());
msgs.add(msg);
}
// 使用生产者实例发送消息
producer.send(msgs);
三、最佳实践
3.1、消息发送最佳实践
3.1.1、一个应用尽可能用一个Topic
一个应用尽可能用一个Topic,消息子类型用tags来标识,tags可以由应用自由设置。只有发送消息设置了tags,消费方在订阅消息时,才可以利用tags在broker做消息过滤。
3.1.2、打印合适的消息日志
消息发送成功或者失败,要打印消息日志,务必要打印sendresult和key字段。
3.1.3、发送成功的几个状态
send消息方法,只要不抛异常,就代表发送成功。但是发送成功会有多个状态,在sendResult里定义。
- SEND_OK
消息发送成功 - FLUSH_DISK_TIMEOUT
消息发送成功,但是服务器刷盘超时,消息已经进入服务器队列,只有此时服务器宕机,消息才会丢失 - FLUSH_SLAVE_TIMEOUT
消息发送成功,但是服务器同步到Slave时超时,消息已经进入服务器队列,只有此时服务器宕机,消息才会丢失 - SLAVE_NOT_AVAILABLE
消息发送成功,但是此时slave不可用,消息已经进入服务器队列,只有此时服务器宕机,消息才会丢失
3.1.4、消息发送的重试
3.1.4.1、只有同步发送才会进行失败重试,重新投递消息
rocketMQ 有3种类型的发送
- 同步
投递消息的线程阻塞,等待获取投递消息的结果,对应 send(Message msg) - 异步
投递消息的线程不阻塞,等投递结果返回时,调用设置好的钩子,对应 send(Message msg, SendCallback sendCallback) - oneway
投递消息的线程不阻塞,不关心投递结果,对应 sendOneway(Message msg)
只有同步发送时,才会在消息投递失败时,进行重试
3.2、消息消费最佳实践
3.2.1、一个 consumer group 只订阅一个 topic
rocketMQ 文档中明确指出,强烈建议一个 consumer group 只订阅一个 topic。
但是,能不能多个 consumer group 订阅同一个 topic 呢?答案是可以的。
多个 consumer group 订阅同一个 topic 时,Topic的一条消息会广播给所有订阅的ConsumerGroup,就是每个ConsumerGroup都会收,但是在一个ConsumerGroup内部给个Consumer是负载消费消息的,翻译一下就是:一条消息在一个group内只会被一个Consumer消费
3.2.2、消费端需要做到幂等
rocketmq 无法避免消息的重复消费,消费端需要做幂等处理。处理方式:
- 1、增加唯一性约束,如果出现重复插入,则直接报错,不进行处理即可
- 2、加锁(悲观锁或者乐观锁)
比如,如果希望更新一个字段,这个时候可以加悲观锁:
直接在jvm 层面加锁或者加分布式锁控制,或者在mysql 做select for update
也可以加乐观锁:使用MVCC