简介
RocketMQ作为阿里巴巴纯Java开发、分布式的消息队列。支持顺序消息、事务消息、批量消息、同步发送、异步发送、消息过滤等特性。同时可解决分布式事务问题,是目前主流的消息队列框架。
角色
- producer producer是消息的生产者,生成消息并发送给broker端。
- broker broker是消息队列的服务器,提供消息的接收、存储、推送等功能。
- nameServer nameServer是为整个MQ提供服务治理、协调、路由等功能,生产者和消费者都需要从name server中获取主题的路由信息:存储在哪个broker,每个broker上该主题的队列等等。
- comsumer comsumer是消息的消费者,能够从broker拉取消息并消费。
- message queue 消息队列,每个消息队列都有自己的offset,类似于数组的下标
- group 每个producer和comsumer都需要指定一个分组,代表着业务角色。若不指定,会默认指定为"DEFAULT_PRODUCER"或"""DEFAULT_CONSUMER"
- topic 消息主题,用于隔离不同业务的消息,生产者会将消息发送到指定的topic,而消费者如果需要消费这个消息,则需要订阅此topic
- tag 消息标签,用于隔离同一个topic的消息,同一个topic可以指定不同的tag,消费者可以根据tag过滤topic的消息。
RocketMQ是怎么保证可靠性?
- 发送方:
1.同步发送:通过阻塞发送的方式,同步等待broker的应答,是否持久化成功,若无应答,会重复投递,at lease one
2.异步发送:基于回调的方式,提供两个方法,onSucess,onException,另外的线程负责处理相应的回调结果,但发送失败,业务可以自行决定是否需要重试。
3.单向发送:不可靠的发送方式,发送端不关心broker的返回值。
- broker端:
1.通过部署多broker的多主多从提高可用性,解决单点故障问题。
2.刷盘策略:
1、同步刷盘:当消息写入到pageCache时,会同步写入到磁盘中,broker宕机后,也可以从磁盘中恢复数据。(性能损耗)
2、异步刷屏(默认):当消息写入到pageCache时,就直接返回成功,等pageCache到达一定数量,再写入到磁盘中,此种方式吞吐量大,但存在消息丢失。
- 消费方:
1.应答机制:解决消息的确认,但broker可能无法接收到ack,导致重复消费,应该要做幂等。
2.重试机制:当消息消费失败时,会进行重试,保证消息至少一次被消费。
3.死信队列,若消费失败达一定次数,会写入到死信队列。
生产者
同步发送
//1.创建消息生产者producer,并制定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
// 2.指定Nameserver地址
producer.setNamesrvAddr("127.0.0.1:9876");
// 3.启动producer
producer.start();
// 4.创建消息对象,指定主题Topic、Tag和消息体
//参数:Message(String topic, String tags, byte[] body)
for (int i = 0; i < 10; i++) {
Message msg = new Message("topic","tag1","MyMessage".getBytes());
// 5.同步发送消息
producer.send(msg);
}
// 6.关闭生产者producer
producer.shutdown();
异步发送
适用于发送对响应时间敏感的消息
Message msg = new Message("topic","tag1","MyMessage".getBytes());
// 发送异步消息
producer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("异步发送消息成功");
}
@Override
public void onException(Throwable throwable) {
log.error("异步发送消息失败");
throwable.printStackTrace();
}
});
// 6.关闭生产者producer
producer.shutdown();
消息类型
单向消息
适用于不关心发送结果的消息,比如日志消息
Message message = new Message("topic","tag1","myLogMessage".getByte());
producer.sendOneway(message);
顺序消息
一般情况下,RocketMQ会通过轮询的方式,将消息放到不同的消息队列中,导致消费者从多个队列获取消息的时候,无法保证消息的顺序消费。
轮询选队列的源码
public MessageQueue selectOneMessageQueue(String lastBrokerName) {
int index;
int i;
if (lastBrokerName != null) {
// 根据TopicPublishInfo中维护的index,每次发送消息,对索引进行+1
// 再进行取余,从而实现轮询选择不同队列
index = this.sendWhichQueue.getAndIncrement();
for(i = 0; i < this.messageQueueList.size(); ++i) {
int pos = Math.abs(index++) % this.messageQueueList.size();
MessageQueue mq = (MessageQueue)this.messageQueueList.get(pos);
if (!mq.getBrokerName().equals(lastBrokerName)) {
return mq;
}
}
return null;
} else {
index = this.sendWhichQueue.getAndIncrement();
i = Math.abs(index) % this.messageQueueList.size();
return (MessageQueue)this.messageQueueList.get(i);
}
}
- 如何保证消息的顺序? RocketMQ提供了队列选择器的方式,让同一个业务流程下发出的消息保存在同一个队列中,以此保证消息的顺序性。 例如,根据订单id作为标识,将相同订单的下单、支付、发货等业务流程按顺序发送到同一个队列,保证了顺序。 自定义选择器实现
Message message = new Message("topic","tag1","messageStr".getByte());
/**
* 参数1: 消息对象
* 参数2: 选择器
* 参数3: 业务标识,这里采用订单id
*/
producer.send(message,new MessageQueueSelector(){
@Override
public MessageQueue select(List<MessageQueue> list, Message message, Object arg) {
// 对订单id进行hash算法或取余,同一个订单id的hash值一致,
// 先进先出同一个队列,保证消息的顺序性。
Integer orderId = (Integer) arg;
int i = orderId % list.size();
MessageQueue messageQueue = list.get(i);
return messageQueue;
}
},arg);
- RocketMQ本身也提供了三种队列选择器,含源码分析
1.SelectMessageQueueByHash -- hash算法选择器
public class SelectMessageQueueByHash implements MessageQueueSelector {
public SelectMessageQueueByHash() {
}
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
// 获取业务标识的hash值,同一个业务标识的hash一致,以此选择同一队列
int value = arg.hashCode();
if (value < 0) {
value = Math.abs(value);
}
value %= mqs.size();
return (MessageQueue)mqs.get(value);
}
}
2.SelectMessageQueueByRandoom -- 随机算法选择器
public class SelectMessageQueueByRandoom implements MessageQueueSelector {
private Random random = new Random(System.currentTimeMillis());
public SelectMessageQueueByRandoom() {
}
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
// 通过随机算法,随机获取队列
int value = this.random.nextInt();
if (value < 0) {
value = Math.abs(value);
}
value %= mqs.size();
return (MessageQueue)mqs.get(value);
}
}
3.通过机房位置分配,开源版本的RocketMQ目前是空实现
public class SelectMessageQueueByMachineRoom implements MessageQueueSelector {
private Set<String> consumeridcs;
public SelectMessageQueueByMachineRoom() {
}
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
return null;
}
public Set<String> getConsumeridcs() {
return this.consumeridcs;
}
public void setConsumeridcs(Set<String> consumeridcs) {
this.consumeridcs = consumeridcs;
}
}
延迟消息
在电商系统中,提交了一个订单就可以发送一个延时消息,1h后去检查这个订单的状态,如果还是未付款就取消订单释放库存。
设置消息属性:
// 只支持固定的几个时间,详看delayTimeLevel
// 设置延时等级2,这个消息将在5s之后发送
message.setDelayTimeLevel(2);
delayTimeLevel等级
delayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
批量消息
批量消息能显著地提升小消息的性能,可以减少与broker的交互次数。 限制是这些批量消息需具备相同的topic。批量消息不可以是延迟消息,同时大小不可以超过4MB
String topic = "BatchTest";
List<Message> messages = new ArrayList<>();
messages.add(new Message(topic, "TagA", "OrderID001", "Hello world 0".getBytes()));
messages.add(new Message(topic, "TagA", "OrderID002", "Hello world 1".getBytes()));
messages.add(new Message(topic, "TagA", "OrderID003", "Hello world 2".getBytes()));
try {
producer.send(messages);
} catch (Exception e) {
e.printStackTrace();
//处理error
}
事务消息
事务消息通过消息二次提交机制,可用于解决分布式事务问题。
- 发送事务消息
/**
* 发送事务消息
*/
@Test
public void sendTrasitionMessage() throws Exception {
// 创建事务的消息提供者
TransactionMQProducer producer = new TransactionMQProducer("group");
// 设置namesrv地址
producer.setNamesrvAddr("127.0.0.1:9876");
// 设置本地事务的检查方法
producer.setTransactionListener(new TransactionListener() {
/**
* @param message 消息对象
* @param o
* @return 事务状态
*/
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
if (message.getTags().equals("TAG1")) {
// 本地事务执行完成,提交消息事务
return LocalTransactionState.COMMIT_MESSAGE;
}else if (message.getTags().equals("TAG2")) {
// 本地事务执行失败,回滚消息事务
return LocalTransactionState.ROLLBACK_MESSAGE;
}else if (message.getTags().equals("TAG3")) {
// 消息状态未知
return LocalTransactionState.UNKNOW;
}
return null;
}
/**
* 消息状态为UNKNOW时的回查函数,检查本地事务的执行情况
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
System.out.println(messageExt.getBody());
return LocalTransactionState.COMMIT_MESSAGE;
}
});
// 开启生产者
producer.start();
// 创建消息体
Message mesage = new Message("Topic","TAG1","myMsg".getBytes());
// 发送事务消息
producer.sendMessageInTransaction(mesage,null);
producer.shutdown();
}
- 事务状态
COMMIT_MESSAGE: 本地事务已提交,允许消费者消费该消息
ROLLBACK_MESSAGE: 本地事务回滚,删除该消息
UNKONW: 事务状态未知,broker需要回查本地事务
- 注意事项:
1.事务消息不支持延迟特性(源码会清空延迟等级)
// ignore DelayTimeLevel parameter
if (msg.getDelayTimeLevel() != 0) {
MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_DELAY_TIME_LEVEL);
}
2.消息不能保证只被检查或消费一次,因此消费方需要做幂等处理
3.不支持批量发送
- 事务消息二次提交机制(源码分析):
1.生产者发送消息给broker -- 事务消息存放在特定队列中,此时消费不到
// 清空延迟消息的level
if (msg.getDelayTimeLevel() != 0) {
MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_DELAY_TIME_LEVEL);
}
SendResult sendResult = null;
// 设置为预消息
MessageAccessor.putProperty(msg,
MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
MessageAccessor.putProperty(msg,
MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());
try {
// 发送half半消息(prepared消息)
sendResult = this.send(msg);
} catch (Exception e) {
throw new MQClientException("send message Exception", e);
}
2.发送半消息成功之后,执行本地事务
switch (sendResult.getSendStatus()) {
case SEND_OK: {
if (null != localTransactionExecuter) {
// 执行本地事务
localTransactionState = localTransactionExecuter.
executeLocalTransactionBranch(msg, arg);
} else if (transactionListener != null) {
log.debug("Used new transaction API");
localTransactionState = transactionListener.
executeLocalTransaction(msg, arg);
}
if (null == localTransactionState) {
localTransactionState = LocalTransactionState.UNKNOW;
}
}
3.将本地事务状态通过请求头传给broker
switch (localTransactionState) {
case COMMIT_MESSAGE:
requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_COMMIT_TYPE);
break;
case ROLLBACK_MESSAGE:
requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE);
break;
case UNKNOW:
requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_NOT_TYPE);
break;
default:
break;
}
requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup());
requestHeader.setTranStateTableOffset(sendResult.getQueueOffset());
requestHeader.setMsgId(sendResult.getMsgId());
String remark = localException != null ? ("executeLocalTransactionBranch exception: " + localException.toString()) : null;
// 将事务状态传给broker
endTransactionOneway(brokerAddr, requestHeader, remark,
this.defaultMQProducer.getSendMsgTimeout());
4.若本地事务提交,broker则提交该消息。 -- 将事务消息推送到真实队列中
Broker端事务提交/回滚操作(这里取endTransaction部分)
代码入口:org.apache.rocketmq.broker.processor.EndTransactionProcessor
if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == requestHeader.getCommitOrRollback()) {
result = this.brokerController.getTransactionalMessageService().commitMessage(requestHeader);
// 将msg的目标队列 设置为真实的队列/主题
MessageExtBrokerInner msgInner = endMessageTransaction(result.getPrepareMessage());
MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_TRANSACTION_PREPARED);
// 并发送最终的消息到真实队列
RemotingCommand sendResult = sendFinalMessage(msgInner);
if (sendResult.getCode() == ResponseCode.SUCCESS) {
// 删除原事务队列的消息
this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
}
return sendResult
}
4.若本地事务回滚,broker则删除消息。 -- 从特定事务队列中删除该消息
else if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE == requestHeader.getCommitOrRollback()) {
result = this.brokerController.getTransactionalMessageService().
rollbackMessage(requestHeader);
if (result.getResponseCode() == ResponseCode.SUCCESS) {
RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
if (res.getCode() == ResponseCode.SUCCESS) {
this.brokerController.getTransactionalMessageService().
deletePrepareMessage(result.getPrepareMessage());
}
return res;
}
}
5.若因网络等原因导致事务状态为UNKNOW,broker回查未知的消息。
6.定时回查事务状态。 -- 若失败,则删除消息,若事务成功,则提交消息到真实队列。
消息过滤
消费者在订阅主题时,可以根据tag或者sql来过滤特定条件的消息,例:
消息过滤的方式:
Tag过滤:
指定消息对象的tag属性。
SQL过滤:
指定消息的属性
生产者:
// 创建消息
Message message = new Message("topic","tag1","myMsg".getBytes());
// 设置用户属性
message.putUserProperty("a","10");
消费者:
// 通过tag过滤消息
consumer.subscribe("topic","tag1");
// 通过sql过滤消息
consumer.subscribe("topic", MessageSelector.bySql("a > 5"));
消费者
非顺序消费
通过ConsumeConcurrentlyContext实现
@PostConstruct
public void consumerMsg() throws MQClientException {
consumer.setNamesrvAddr(namesrvAddr);
consumer.subscribe("topic","*");
consumer.setMessageModel(MessageModel.CLUSTERING);
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
Iterator<MessageExt> it = msgs.iterator();
if (it.hasNext()) {
MessageExt msgExt = it.next();
// 一定要把msgId输出到日志,它是消息的唯一标识,没有它就没法排查消息是否收到,MessageExt.toString()就会输出msgId
log.info(Thread.currentThread().getName() + " 开始消费: " + msgExt);
String msg = new String(msgExt.getBody(), Charsets.UTF_8);
TakeCrawlerReq takeCrawlerReq = gson.fromJson(msg, TakeCrawlerReq.class);
Boolean isSuccess = doComsumer(takeCrawlerReq);
if (!isSuccess) {
// 重试消费
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
// 消费成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 开启消费者
try {
consumer.start();
} catch (MQClientException e) {
// TODO Auto-generated catch block
log.error(e.getMessage());
}
log.info("consumer init success");
}
顺序消费
消费者的顺序消费对应着生产者的顺序发送, 通过MessageListenerOrderly监听器实现,不同的队列由专门的线程进行处理,保证了顺序消费能力。
// 消费顺序消息
@PostConstruct
public void comsumer() throws MQClientException {
comsumer = new DefaultMQPushConsumer("comsumerSubGroup");
comsumer.setNamesrvAddr(namesrvAddr);
comsumer.subscribe(topic,"*");
comsumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) {
Iterator<MessageExt> it = list.iterator();
if (it.hasNext()) {
MessageExt messageExt = it.next();
String message = new String(messageExt.getBody(), Charsets.UTF_8);
boolean isSuccess = doComsumer(message);
if (isSuccess) {
return ConsumeOrderlyStatus.SUCCESS;
} else {
// 回滚消息
return ConsumeOrderlyStatus.ROLLBACK;
}
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
try {
comsumer.start();
} catch (MQClientException e) {
log.error(e.getMessage());
}
log.info("consumer init success");
}
消费模式
默认为集群消费模式:
消息队列会被集群中的消费者瓜分
广播模式:
广播模式下,每个消费者都会消费一遍队列里的消息
// 设置广播模式
consumer.setMessageModel(MessageModel.BROADCASTING);
封装通用组件工具类
- 封装消息对象的统一规范(消息基类)
/**
* @description:MQ的基类
*/
public abstract class BaseMQMsg {
// gson属性不需要被序列化,只去序列化带业务含义的属性值
private static transient Gson gson = new Gson();
public String getJson() {
return gson.toJson(this);
}
public byte[] getMsgBody() {
try {
return gson.toJson(this).getBytes("utf-8");
} catch (UnsupportedEncodingException e) {
// 不做处理,返回null
}
return null;
}
}
- 定义消息对象 继承于基类,用于封装消息的信息,在发送消息时,通过getMsgBody()即可将属性转换成二进制格式用于发送。
@Setter
@Getter
@ToString
public class ProductMQ extends BaseMQMsg {
// 手机imei号
private String imei;
// 类目id
private Integer cateId;
// 品牌id
private Integer brandId;
// 机型
private Integer modelId;
// 手机序列号
private String seriesNo;
}
- 封装一个开箱即用的发送者,并支持重试机制
@Slf4j
public class RetryProducer{
private DefaultMQProducer producer;
// 延迟时间
private Integer delayTime;
// 重试次数
private Integer retryTimes;
private Gson gson = new Gson();
// 带定时调度的线程池
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(20);
/**
* producer发送到broker -- 异步消息
*
* @param logStr 请求链路,用于日志追踪
* @param topic 消息主题
* @param tag 消息tag
* @param keys 消息key,可以为null,用于查询消息
* @param mqMsg 消息对象
*/
@Override
public void sendMsg(final String logStr, String topic, String tag, String keys, BaseMqMsg mqMsg) {
try {
Message message = new Message(topic,tag,keys,mqMsg.getMsgBody());
// 设置请求链路标识
message.putUserProperty("logStr",logStr);
// 设置发送时间
message.putUserProperty("SEND_TIMESTAMP",System.currentTimeMillis()+"");
log.info("send begin, msg: {}", gson.toJson(message));
MQRetryMsg retryMsg = new MQRetryMsg(message);
// 实际上的发送逻辑在beginSend方法
beginSend(logStr,retryMsg);
} catch (Exception e) {
log.error("method=sendMsg {} sendMSG={}", logStr, mqMsg, e);
}
}
/**
* producer发送到broker -- 顺序消息
*
* @param logStr 请求链路,用于日志追踪
* @param topic 消息主题
* @param tag 消息tag
* @param keys 消息key,可以为null,用于查询消息
* @param mqMsg 消息对象
* @param arg 业务标识:如订单id
*/
@Override
public void sendOrderlyMsg(String logStr, String topic, String tag, String keys, BaseMqMsg mqMsg, Integer arg) {
try {
Message message = new Message(topic,tag,keys,mqMsg.getMsgBody());
// 设置请求链路标识
message.putUserProperty("logStr",logStr);
// 设置发送时间
message.putUserProperty("SEND_TIMESTAMP",System.currentTimeMillis()+"");
log.info("send begin, msg: {}", gson.toJson(message));
// 通过选择器,来选择消息存放的队列
producer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> list, Message message, Object arg) {
// 对订单id进行hash算法或取余,同一个订单id的hash值一致,先进先出同一个队列,保证消息的顺序性。
Integer orderId = (Integer) arg;
int i = orderId % list.size();
MessageQueue messageQueue = list.get(i);
return messageQueue;
}
},arg);
} catch (Exception e) {
rertySend(logStr,message,e);
}
}
/**
* 消息发送,并保证重试机制
* @param logStr 请求链路标识
* @param message 适配了重试次数的消息对象
*/
private void beginSend(String logStr, MQRetryMsg message) {
try {
producer.send(message.getMsg(), new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
}
@Override
public void onException(Throwable e) {
// 发送失败,进行重试
rertySend(logStr,message,e);
}
});
} catch (Exception e) {
// 发送失败,进行重试
rertySend(logStr,message,e);
}
}
/**
* 重试发送
*/
private void rertySend(String logStr, MQRetryMsg message, Throwable e) {
// 判断重试次数是否到达阈值
if (retryTimes > message.getRetryTimes()) {
log.info("重试发送消息,重试次数={}",message.getRetryTimes());
// 已重试次数+1
message.incrRetryTime();
// 通过时间调度的线程池执行
scheduledExecutorService.schedule(() ->
beginSend(logStr,message), delayTime, TimeUnit.SECONDS);
} else {
log.info("已达到重试次数,消息发送失败,msg={}",gson.toJson(message.getMsg()),e);
}
}
// 初始化producer
public RetryProducer(String producerGroup, String namesrvAddr, Integer delayTime, Integer sendMsgTimeout, Integer retryTimes) {
try {
producer = new DefaultMQProducer();
producer.setProducerGroup(producerGroup);
producer.setNamesrvAddr(namesrvAddr);
producer.setSendMsgTimeout(sendMsgTimeout);
producer.start();
} catch (Exception e) {
log.error("init faild", e);
}
this.delayTime = delayTime;
this.retryTimes = retryTimes;
}
}