(1)NameServer:
1)消息队列中的状态服务器,集群的各个组件通过它来了解全局的信息 。类似微服务中注册中心的服务注册,发现,下线,上线的概念。
2)热备份:NamServer可以部署多个,相互之间独立,其他角色同时向多个NameServer 机器上报状态信息。
3)心跳机制:NameServer 中的 Broker、 Topic等状态信息不会持久存储,都是由各个角色定时上报并存储到内存中,超时不上报的话, NameServer会认为某个机器出故障不可用。
(2)Broker:RocketMQ 的核心,接收 Producer 发过来的消息、处理 Consumer 的消费消息请求、消息的持 久化存储、服务端过滤功能等 。
(3)Producer:消息的生产者,将消息投递到broker,投递消息要经历“请求-确认”机制,确保消息不会在投递过程中丢失。过程:生产者生产消息到broker,broker接受消息写入topic,之后给生产者发送确认相应,如果生产者没有收到服务端的确认或者收到失败的响应,则会重新发送消息;
(4)Consumer:消息的消费者,常用Consumer类 DefaultMQPushConsumer 收到消息后自动调用传入的处理方法来处理,实时性高 DefaultMQPullConsumer 用户自主控制 ,灵活性更高。在消费端,消费者在收到消息并完成自己的消费业务逻辑(比如,将数据保存到数据库中)后,也会给服务端发送消费成功的确认,服务端只有收到消费确认后,才认为一条消息被成功消费,否则它会给消费者重新发送这条消息,直到收到对应的消费成功确认。
(5)Topic:表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。
(6)tag:topic中消息的标签,消费者消费topic中的消息时可以根据tag标签分类消费
topic中队列是可配置的,多个队列相当于是负载均衡,将生产者投递到该topic中的消息分别加入各个队列中,图中为2个队列,即相当于将topic的消息承载体分为两段(即每条消息只会往每个队列里发一次,在topic中是唯一的),这样是为了提供性能,多实例并发生产和消费
topic中不能保证消息有序,但是在队列中消息有序
2、消费者组如何消费?
3、如何保证读取消息的有序性(一个消费组在一个主题下的多个队列并发消费就无法保证消息的顺序性)?
4、消息发送确认是否会影响效率?
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.7.0</version>
</dependency>
rocketmq:
# 生产者配置
producer:
#是否开启自动配置
isEnable: true
# 发送同一类消息的设置为同一个group,保证唯一
groupName: elink-service-message-producer
# 服务地址
namesrvAddr: rocketmq-nameserver1:8047;rocketmq-nameserver2:8047
# 消息最大长度 默认1024*4(4M)
maxMessageSize: 4096
# 发送消息超时时间,默认3000
sendMsgTimeout: 3000
# 发送消息失败重试次数,默认2
retryTimesWhenSendFailed: 3
# 消费者配置
consumer:
#是否开启自动配置
isEnable: true
# 官方建议:确保同一组中的每个消费者订阅相同的主题。
groupName: elink-service-message-consumer
# 服务地址,多个地址用 ';' 隔开
namesrvAddr: rocketmq-nameserver1:8047;rocketmq-nameserver2:8047
consumeThreadMin: 20
consumeThreadMax: 64
# 设置一次消费消息的条数,默认为1条
consumeMessageBatchMaxSize: 1
@Getter
public enum MessageCodeEnum implements BaseStrEnum {
/**
* 消息模块主题
*/
MESSAGE_TOPIC("elink-message","消息服务模块topic名称"),
/**
* 短信平台编号
*/
NOTE_TAG("note_tag","短信标题"),
DING_TAG("ding_tag","钉钉消息标题");
private final String code;
private final String msg;
MessageCodeEnum(String code, String msg){
this.code = code;
this.msg = msg;
}
}
@Configuration
@ConditionalOnProperty(prefix = "rocketmq.consumer", value = "isEnable", matchIfMissing = true)
@Slf4j
public class ConsumerConfig {
@Value("${rocketmq.consumer.namesrvAddr}")
private String namesrvAddr;
@Value("${rocketmq.consumer.groupName}")
private String groupName;
@Value("${rocketmq.consumer.consumeThreadMin}")
private int consumeThreadMin;
@Value("${rocketmq.consumer.consumeThreadMax}")
private int consumeThreadMax;
@Value("${rocketmq.consumer.consumeMessageBatchMaxSize}")
private int consumeMessageBatchMaxSize;
@Resource
private RocketMsgListener msgListener;
@Bean
public DefaultMQPushConsumer getRocketMQConsumer() {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(groupName);
consumer.setNamesrvAddr(namesrvAddr);
consumer.setConsumeThreadMin(consumeThreadMin);
consumer.setConsumeThreadMax(consumeThreadMax);
consumer.registerMessageListener(msgListener);
/**
* 1. CONSUME_FROM_LAST_OFFSET:第一次启动从队列最后位置消费,后续再启动接着上次消费的进度开始消费
* 2. CONSUME_FROM_FIRST_OFFSET:第一次启动从队列初始位置消费,后续再启动接着上次消费的进度开始消费
* 3. CONSUME_FROM_TIMESTAMP:第一次启动从指定时间点位置消费,后续再启动接着上次消费的进度开始消费
* 以上所说的第一次启动是指从来没有消费过的消费者,如果该消费者消费过,那么会在broker端记录该消费者的消费位置,如果该消费者挂了再启动,那么自动从上次消费的进度开始
*/
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
/**
* CLUSTERING (集群模式) :默认模式,同一个ConsumerGroup(groupName相同)每个consumer只消费所订阅消息的一部分内容,同一个ConsumerGroup里所有的Consumer消息加起来才是所
* 订阅topic整体,从而达到负载均衡的目的
* BROADCASTING (广播模式) :同一个ConsumerGroup每个consumer都消费到所订阅topic所有消息,也就是一个消费会被多次分发,被多个consumer消费。
*
*/
// consumer.setMessageModel(MessageModel.BROADCASTING);
consumer.setVipChannelEnabled(false);
consumer.setConsumeMessageBatchMaxSize(consumeMessageBatchMaxSize);
try {
/**
* 订阅topic,可以对指定消息进行过滤,例如:"TopicTest","tagl||tag2||tag3",*或null表示topic所有消息
*/
consumer.subscribe(MessageCodeEnum.MESSAGE_TOPIC.getCode(), "*");
consumer.start();
log.info("消费者初始化成功:{}", consumer.toString());
} catch (MQClientException e) {
e.printStackTrace();
log.error("消费者初始化失败:{}",e.getMessage());
}
return consumer;
}
}
@Component
@Slf4j
public class RocketMsgListener implements MessageListenerConcurrently {
@Autowired
private DingMessage dingMessage;
@Autowired
private SmsLogService smsLogService;
@Autowired
private NoteMessage noteMessage;
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
if (!CollectionUtils.isEmpty(list)) {
for (MessageExt messageExt : list) {
// 消息内容
String body = new String(messageExt.getBody());
log.info("接受到的消息为:{}", body);
String tags = messageExt.getTags();
String topic = messageExt.getTopic();
String msgId = messageExt.getMsgId();
String keys = messageExt.getKeys();
int reConsume = messageExt.getReconsumeTimes();
// 消息已经重试了3次,如果不需要再次消费,则返回成功
if (reConsume == 3) {
// TODO 补偿信息
// smsLogService.insertLog(topic, tags, msgId, keys, body, "【" + EnumUtil.getStrMsgByCode(tags, TagsCodeEnum.class) + "】消费失败");
log.error("消息重试超过3次,消费失败!");
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
// 先判断是否订阅该topic
if (MessageCodeEnum.MESSAGE_TOPIC.getCode().equals(topic)) {
boolean flag = false;
// 判断tag标签分类的业务,消息队列业务处理尽量单一
if (MessageCodeEnum.DING_TAG.getCode().equals(tags)) {
// 发送钉钉消息
flag = dingMessage.sendDingMessage(body);
} else if (MessageCodeEnum.NOTE_TAG.getCode().equals(tags)) {
// 发送短信
flag = noteMessage.sendNoteMessage(body);
} else {
log.info("未匹配到Tag【{}】" + tags);
}
if (!flag) {
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
}
}
// 消息消费成功
// ConsumeConcurrentlyStatus.RECONSUME_LATER broker会根据设置的messageDelayLevel发起重试,默认16次
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
@Configuration
@Slf4j
public class ProducerConfig {
@Value("${rocketmq.producer.groupName}")
private String groupName;
@Value("${rocketmq.producer.namesrvAddr}")
private String namesrvAddr;
@Value("${rocketmq.producer.maxMessageSize}")
private Integer maxMessageSize ;
@Value("${rocketmq.producer.sendMsgTimeout}")
private Integer sendMsgTimeout;
@Value("${rocketmq.producer.retryTimesWhenSendFailed}")
private Integer retryTimesWhenSendFailed;
@Bean
public DefaultMQProducer getRocketMQProducer() {
DefaultMQProducer producer;
producer = new DefaultMQProducer(this.groupName);
producer.setNamesrvAddr(this.namesrvAddr);
//如果需要同一个jvm中不同的producer往不同的mq集群发送消息,需要设置不同的instanceName
if(this.maxMessageSize!=null){
producer.setMaxMessageSize(this.maxMessageSize);
}
if(this.sendMsgTimeout!=null){
producer.setSendMsgTimeout(this.sendMsgTimeout);
}
//如果发送消息失败,设置重试次数,默认为2次
if(this.retryTimesWhenSendFailed!=null){
producer.setRetryTimesWhenSendFailed(this.retryTimesWhenSendFailed);
}
producer.setVipChannelEnabled(false);
try {
producer.start();
log.info("生产者初始化成功:{}",producer.toString());
} catch (MQClientException e) {
log.error("生产者初始化失败:{}",e.getMessage());
}
return producer;
}
}
public class MessageProducer {
@Autowired
private DefaultMQProducer producer;
/**
* 同步发送消息
* @author xch
* @date 2020/6/5 10:19 上午
* @param topic 主题
* @param tag 标签
* @param key 自定义的key,根据业务来定
* @param value 消息的内容
* @return org.apache.rocketmq.client.producer.SendResult
*/
public SendResult sendMessage(String topic,String tag,String key,String value){
String body = "topic:【"+topic+"】, tag:【"+tag+"】, key:【"+key+"】, value:【"+value+"】";
try {
Message msg = new Message(topic,tag,key,value.getBytes(RemotingHelper.DEFAULT_CHARSET));
return producer.send(msg);
} catch (UnsupportedEncodingException e) {
log.error("消息初始化失败!body:{}",body);
} catch (MQClientException | InterruptedException | RemotingException | MQBrokerException e) {
log.error("消息发送失败! body:{}",body);
}
return null;
}
/**
* 发送有序的消息
* @author xch
* @date 2020/6/5 10:23 上午
* @param messagesList Message集合
* @param messageQueueNumber 消息队列编号
* @return org.apache.rocketmq.client.producer.SendResult
*/
public SendResult sendOrderlyMessage(List<Message> messagesList, int messageQueueNumber) {
SendResult result = null;
for (Message message : messagesList) {
try {
result = this.producer.send(message, (list, msg, arg) -> {
Integer queueNumber = (Integer) arg;
return list.get(queueNumber);
}, messageQueueNumber);
} catch (MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
log.error("发送有序消息失败");
return result;
}
}
return result;
}
}
public class AsyncProducer {
public static void main(String[] args) throws Exception {
//Instantiate with a producer group name.
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
// Specify name server addresses.
producer.setNamesrvAddr("localhost:9876");
//Launch the instance.
producer.start();
producer.setRetryTimesWhenSendAsyncFailed(0);
for (int i = 0; i < 100; i++) {
final int index = i;
//Create a message instance, specifying topic, tag and message body.
Message msg = new Message("TopicTest",
"TagA",
"OrderID188",
"Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
producer.send(msg, new SendCallback() {
// 异步回调的处理
@Override
public void onSuccess(SendResult sendResult) {
System.out.printf("%-10d 异步发送消息成功 %s %n", index,
sendResult.getMsgId());
}
@Override
public void onException(Throwable e) {
System.out.printf("%-10d 异步发送消息失败 %s %n", index, e);
e.printStackTrace();
}
});
}
//Shut down once the producer instance is not longer in use.
producer.shutdown();
}
}