RocketMQ最佳总结与原理解析

683 阅读7分钟

简介

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;

    }

}