RocketMQ 使用与基本原理

简介

  RocketMQ 原先是阿里巴巴内部使用的消息中间件,于 2017 年提交到 Apache 基金会成为 Apache 基金会的顶级开源项目,官方网址:http://rocketmq.apache.org。阿里的消息中间件有很长的历史,从 2007 年的 Notify到2010 年的 Napoli,2011 年升级后改为 MetaQ ,然后到 2012 年开始做 RocketMQ。RocketMQ吸取了阿里之前的消息系统的经验与教训并参考了业界其他消息系统(如Kafka等),秉承尽量简单的原则,进行设计与实现。在阿里内部, RocketMQ 很好地服务了团大大小小上千个应用,在每年的双十一当天,更有不可思议的万亿级消息通过 RocketMQ 流转(在2017 年的双十一当天,整个阿里巴巴集团通过 RocketMQ 流转的线上消息达到了万亿级,峰值 TPS 达到 5600 万),在阿里大中台策略上发挥着举足轻重的作用。RocketMQ 是使用 Java 语言开发的,比起 Kafka Scala 和RabbitMQ Erlang语言,对于Java技术栈的技术人员,可以方便的查看其实现源码,并在其基础上做定制化开发。

基本概念

1.消息生产者(Producer):负责生产消息,一般由业务系统负责生产消息。一个消息生产者会把业务应用系统里产生的消息发送到broker服务器。RocketMQ提供多种发送方式,同步发送、异步发送、顺序发送、单向发送。同步和异步方式均需要Broker返回确认信息,单向发送不需要。
2.消息消费者(Consumer):负责消费消息,一般是后台系统负责异步消费。一个消息消费者会从Broker服务器拉取消息、并将其提供给应用程序。从用户应用的角度而言提供了两种消费形式:拉取式消费、推动式消费。
3.主题(Topic):表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。
4.代理服务器(Broker Server):消息中转角色,负责存储消息、转发消息。代理服务器在RocketMQ系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备。代理服务器也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等。
5.名字服务(Name Server):名称服务充当路由消息的提供者。生产者或消费者能够通过名字服务查找各主题相应的Broker IP列表。多个Namesrv实例组成集群,但相互独立,没有信息交换。
6.消息消费模式:
1.Pull Consumer消费的一种类型,应用通常主动调用Consumer的拉消息方法从Broker服务器拉消息、主动权由应用控制。一旦获取了批量消息,应用就会启动消费过程。
2.Push Consumer消费的一种类型,该模式下Broker收到数据后会主动推送给消费端,该消费模式一般实时性较高。
7.生产者组(Producer Group):同一类Producer的集合,这类Producer发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,则Broker服务器会联系同一生产者组的其他生产者实例以提交或回溯消费(消息服务器在回查事务状态时会随机选择该组中任何个生产者发起事务回查请求)。
8.消费者组(Consumer Group):同一类Consumer的集合,这类Consumer通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。要注意的是,消费者组的消费者实例必须订阅完全相同的Topic。RocketMQ 支持两种消息模式:集群消费(Clustering)和广播消费(Broadcasting)。
9.集群消费(Clustering):集群消费模式下,相同Consumer Group的每个Consumer实例平均分摊消息。
10.广播消费(Broadcasting):广播消费模式下,相同Consumer Group的每个Consumer实例都接收全量的消息。
11.普通顺序消息(Normal Ordered Message):普通顺序消费模式下,消费者通过同一个消费队列收到的消息是有顺序的,不同消息队列收到的消息则可能是无顺序的。
12.严格顺序消息(Strictly Ordered Message):严格顺序消息模式下,消费者收到的所有消息均是有顺序的。
13.消息(Message):消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。RocketMQ中每个消息拥有唯一的Message ID,且可以携带具有业务标识的Key。系统提供了通过Message ID和Key查询消息的功能。
14.标签(Tag):为消息设置的标志,用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务目的在同一主题下设置不同标签。标签能够有效地保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。
15.消息模型(Message Model):RocketMQ主要由 Producer、Broker、Consumer 三部分组成,其中Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息。Broker 在实际部署过程中对应一台服务器,每个 Broker 可以存储多个Topic的消息,每个Topic的消息也可以分片存储于不同的 Broker。Message Queue 用于存储消息的物理地址,每个Topic中的消息地址存储于多个 Message Queue 中。ConsumerGroup 由多个Consumer 实例构成。
16.读写队列 :在消息发送时,使用写队列个数返回路由信息,而消息消费时按照读队列个数返回路由信息。在物理文件层面,只有写队列才会创建文件。如果写队列个数是8,设置的读队列个数是4.这个时候,会创建8个文件夹,代表0 1 2 3 4 5 6 7,但在消息消费时,路由信息只返回4,在具体拉取消息时,就只会消费0 1 2 3这4个队列中的消息,4 5 6 7压根就没有消息。反过来,如果写队列个数是4,读队列个数是8,在生产消息时只会往0 1 2 3中生产消息,消费消息时则会从0 1 2 3 4 5 6 7所有的队列中消费,当然 4 5 6 7中压根就没有消息 ,假设消费group有两个消费者,事实上只有第一个消费者在真正的消费消息(0 1 2 3),第二个消费者压根就消费不到消息。只有readQueueNums>=writeQueueNums,程序才能正常进行。最佳实践是readQueueNums=writeQueueNums。那rocketmq为什么要区分读写队列呢?rocketmq设置读写队列数的目的在于方便队列的缩容和扩容。思考一个问题,一个topic在每个broker上创建了128个队列,现在需要将队列缩容到64个,怎么做才能100%不会丢失消息,并且无需重启应用程序?最佳实践:先缩容写队列128->64,写队列由0 1 2 …127缩至 0 1 2 …63。等到64 65 66…127中的消息全部消费完后,再缩容读队列128->64.(同时缩容写队列和读队列可能会导致部分消息未被消费)

RocketMQ NameServer

在这里插入图片描述

NameServer是RocketMQ的路由信息中心,Broker消息服务器在启动时向所有NameServer注册,消息生产者(Producer)在发送消息之前先从NameServer获取Broker服务器地址列表,然后根据负载算法从列表中选择一台消息服务器进行消息发送。NameServer 与每台Broker服务器保持长连接,并间隔30sBroker检测Broker是否存活,如果检测到Broker从路由注册表中将其移除,但是路由变化不会马上通知消息生产者,为什么要这样设计呢?这是为了降低NameServer实现的复杂性,在消息发送端提供容错机制来保证消息发送的高可用性。
不同于Kafka依赖Zookeeper来保证路由信息的强一致性,各个RocketMQ NameServer彼此之间互不通信,也就是NameServer务器之间在某一时刻的数据并不会完全相同,但这对消息发送不会造成任何影响,因为RokectMQ在消息发送时,消息发送客户端做了容错与重试,没有必要在NameServer端进行数据一致性的保证,增加设计复杂度和牺牲系统性能,转而在客户端进行处理。这是RokectMQ设计的基本原则,尽量保持设计简单高效。

消息发送

RocketMQ源码分析之消息发送原理

Broker启动时,向NameServer注册信息
客户端调用producer发送消息时,会先从NameServer获取该topic的路由信息。消息头code为GET_ROUTEINFO_BY_TOPIC
从NameServer返回的路由信息,包括topic包含的队列列表和broker列表
Producer端根据查询策略,选出其中一个队列,用于后续存储消息
每条消息会生成一个唯一id,添加到消息的属性中。属性的key为UNIQ_KEY
对消息做一些特殊处理,比如:超过4M会对消息进行压缩
producer向Broker发送rpc请求,将消息保存到broker端

消息存储

MQ中间件从存储模型来 分为需要持久化和不需要持久化的两种。大多数的MQ是支持持久化存储的,比如 Act veMQ RabbitMQ Kafka,RocketMQ等。RocketMQ的主要存储文件包括 Commitlog 文件、 ConsumeQueue 文件、 IndexFile文件。
消息结构及相关文件的组织方式:
在这里插入图片描述

Commitlog:消息存储文件,所有消息主题的消息都存储在 CommitLog 文件中。CommitLog 文件在磁盘上的组织方式如下,每个文件默认大小1G,文件名称为第一个消息的偏移量,
在这里插入图片描述

ConsumeQueue:消息消费队列,消息到达 CommitLog 文件后,将异步转发到消息消费队列,供消息消费者消费。
在这里插入图片描述

IndexFile::消息索引文件,主要存储消息 Key Offset 的对应关系。
RocketMQ接收到消息后进行存储的流程如下:
在这里插入图片描述

Broker端收到消息后,将消息原始信息保存在CommitLog文件对应的MappedFile中,然后异步刷新到磁盘
ReputMessageServie线程异步的将CommitLog中MappedFile中的消息保存到ConsumerQueue和IndexFile中
ConsumerQueue和IndexFile只是原始文件的索引信息

消息投递消费

在RocketMQ中一般有两种获取消息的方式,一个是拉(pull,消费者主动去broker拉取),一个是推(push,主动推送给消费者)。Pull方式是消费者从server端拉消息,主动权在消费端,可控性好,但是时间间隔不好设置,间隔太短,则空请求会多,浪费资源,间隔太长,则消息不能及时处理。RocketMQ的push方式是对Pull方式的封装,实时性高,但增加服务端负载,消费端能力不同,如果push的速度过快,消费端会出现很多问题。
RocketMq消息处理整个流程如下:
在这里插入图片描述

消息接收:消息接收是指接收producer的消息,处理类是SendMessageProcessor,将消息写入到commigLog文件后,接收流程处理完毕;
消息分发:broker处理消息分发的类是ReputMessageService,它会启动一个线程,不断地将commitLong分到到对应的consumerQueue,这一步操作会写两个文件:consumerQueue与indexFile,写入后,消息分发流程处理 完毕;
消息投递:消息投递是指将消息发往consumer的流程,consumer会发起获取消息的请求,broker收到请求后,调用PullMessageProcessor类处理,从consumerQueue文件获取消息,返回给consumer后,投递流程处理完毕。
下面我们就了解一下RocketMQ的Push方式的默认消息投递消费实现基本原理。
1.消息消费者启动
Step1 :构建主题订阅信息 SubscriptionData 并加入到 Rebalancelmpl 的订阅消息中。定阅关系来源主要有两个
1 )通过调用 DefaultMQPushConsumerlmpl#subscribe( String topic, String subExpression) 方法
2 )订阅重试主题消息。从这里可以看出RocketMQ消息重试是以消费组为单位,而不是主题,消息重试主题名为 %RETRY%+消费组名 消费者在启动的时候会自动订阅该主题,参与该主题的消息队列负载
Step2 :根据消息模式,初始化 MQClientlnstance Rebalancelmple (消息重新负载实现类)等
Step3 :初始化消息进度,如果消息消费是集群模式,那么消息进度从 Broker 上获取;如果是广播模式则存在本地
Step4 :根据是否是顺序消费,创建不同的消费端消费线程服务
Step5 :向 MQClientlnstance 注册消费者,并启动 MQClientlnstance
2.消息拉取
消息有两种模式,广播和集群模式,广播模式比较简单,每一个消费者需要去拉取订阅主题下所有消费队列的消息,但是对应集群模式就比较复杂了,下面我看下集群模式下的消息拉取流程。
PullMessageService#run方法内,通过while ( ! this. is Stopped ( ) )这种方式,不断的从pullRequestQueue中获取PullRequest,来进行消息拉取。pullRequestQueue是一个BlockingQueue,当pullRequestQueue中没有PullRequest的时候,该线程回一直阻塞,PullRequest会在rebalance,一次拉取任务完成时加入到pullRequestQueue中,当有了PullRequest时,PullMessageService线程就可以从Broker拉取消息了,当服务端返回消息后,会将消息放到ProcessQueue中,以供消费者消费,做到消息的拉取和消费解耦。
3.消息消费
1.消息消费有两种方式:顺序消费和并发消费,以并发消费为例,当PullMessageService将消息拉回本地,存入ProcessQueue,调用 ConsumMessageService#submitConsumeRequest 方法进行消息消费。
2.消息开始进行处理,重设恢复TOPIC,这是由消息重试机制决定的,RocketMQ将消息存入 commitlog 文件时,如果发现消息的延时级别 delayTimeLevel 大于0,会首先将重试主题存人在消息的属性中,然后设置主题名称为 SCHEDULE_TOPIC ,以便时间到后重新参与消息消费
3.执行具体的消息消费,调用应用程序消息监昕器的 consumeMessage 方法,进入到具体的消息消费业务逻辑,返回该批消息的消费结果最终将返回 CONSUME_SUCCESS (消费成功)或 RECONSUME_LATER (需要重新消费)
4.处理异常情况,并更新消息消费进度
在RocketMQ中还有一种情况就是消息消费重试,它的实现方式是基于定时消息的,当消息写入时,如消息的延时级别 delayTimeLevel 大于0,会首先将重试主题存人在消息的属性中,设置主题名称为 SCHEDULE_TOPIC,RocketMQ后台有定时任务线程不但扫描生效的消息,触发消息消费,并且每次消费的时候delayTimeLevel就好增加1,并且,每个消息有个最大重试次数,当重试到达这个最大重试次数之后这个消息就会被MQ存储在一个叫死信队列(DLQ)中,需要人工干预才能消费。

HA的设计

为了提高消息消费的高可用性,避免 Broker 发生单点故障引起存储在 Broker 上的消息无法及时消费, RocketMQ 入了 Broker 主备机制 即消息消费到达主服务器后需要将消息同步到消息从服务器,如果主服务器 Broker 君机后,消息消费者可以从从服务器拉取消息。
在这里插入图片描述

RocketMQ 的主从同步机制如下:

  1. 首先启动Master并在指定端口监听;
  2. 客户端启动,主动连接Master,建立TCP连接;
  3. 客户端以每隔5s的间隔时间向服务端拉取消息,如果是第一次拉取的话,先获取本地commitlog文件中最大的偏移量,以该偏移量向服务端拉取消息;
  4. 服务端解析请求,并返回一批数据给客户端;
  5. 客户端收到一批消息后,将消息写入本地commitlog文件中,然后向Master汇报拉取进度,并更新下一次待拉取偏移量;
  6. 然后重复第3步;
    RocketMQ主从同步一个重要的特征:主从同步不具备主从切换功能,即当主节点宕机后,从不会接管消息发送,但可以提供消息读取。4.5版本之后引入了多副本机制,实现了主从自动切换。
    当数据同步到从服务器之后,在默认情况下RocketMQ会优先选择从主服务器进行拉取消息,并不是通常意义的上的读写分离,但是当RocketMQ计算内存使用超过一定阈值的时候,就建议下一次拉取到从服务器拉取消息。

事务消息

RocketMQ 事务消息的实现原理基于两阶段提交和定时事务状态回查来决定消息最终是提交还是回滚。
1.应用程序在事务内完成相关业务数据落库后,需要同步调用 RocketMQ 消息发送接口,发送状态为 prepare 的消 消息发送成功后, RocketMQ 服务器会回调 RocketMQ消息息发送者的事件监听程序,记录消息的本地事务状态,该相关标记与本地业务操作同属一个事务,确保消息发送与本地事务的原子性。
2.RocketMQ 在收到类型为 prepare 的消息时, 会首先备份消息的原主题与原消息消费队列,然后将消息存储在主题为 RMQ_SYS_TRANS_HALF_TOPIC 的消息消费队列中。
3.RocketMQ 消息服务器开启一个定时任务,消费 RMQ_SYS_TRANS_HALF_TOPIC 的消息,向消息发送端(应用程序)发起消息事务状态回查,应用程序根据保存的事务状态回馈消息服务器事务的状态(提交、回滚、未知),如果是提交或回滚, 则消息服务器提交或回滚消息,如果是未知,待一次回查,RocketMQ 允许设置一条消息的回查间隔与回查次数,如果在超过回查次数后依然无法获知消息的事务状态, 则默认回滚消息。
在这里插入图片描述

事务消息 订单服务、积分服务 为例示例:

//初始化事务消息Producer

@Component
public class TransactionProducer {

    private String producerGroup = "order_trans_group";
    private TransactionMQProducer producer;

    //用于执行本地事务和事务状态回查的监听器
    @Autowired
    OrderTransactionListener orderTransactionListener;
    //执行任务的线程池
    ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 60,
            TimeUnit.SECONDS, new ArrayBlockingQueue<>(50));

    @PostConstruct
    public void init(){
        producer = new TransactionMQProducer(producerGroup);
        producer.setNamesrvAddr("127.0.0.1:9876");
        producer.setSendMsgTimeout(Integer.MAX_VALUE);
        producer.setExecutorService(executor);
        producer.setTransactionListener(orderTransactionListener);
        this.start();
    }
    private void start(){
        try {
            this.producer.start();
        } catch (MQClientException e) {
            e.printStackTrace();
        }
    }
    //事务消息发送 
    public TransactionSendResult send(String data, String topic) throws MQClientException {
        Message message = new Message(topic,data.getBytes());
        return this.producer.sendMessageInTransaction(message, null);
    }
}

//事务消息监听
@Component
public class OrderTransactionListener implements TransactionListener {

    @Autowired
    OrderService orderService;

    @Autowired
    TransactionLogService transactionLogService;

    Logger logger = LoggerFactory.getLogger(this.getClass());

    //执行本地事务
    @Override
    public LocalTransactionState executeLocalTransaction(Message message, Object o) {
        logger.info("开始执行本地事务....");
        LocalTransactionState state;
        try{
            String body = new String(message.getBody());
            OrderDTO order = JSONObject.parseObject(body, OrderDTO.class);
            orderService.createOrder(order,message.getTransactionId());
            state = LocalTransactionState.COMMIT_MESSAGE;
            logger.info("本地事务已提交。{}",message.getTransactionId());
        }catch (Exception e){
            logger.info("执行本地事务失败。{}",e);
            state = LocalTransactionState.ROLLBACK_MESSAGE;
        }
        return state;
    }

    //事务回查
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
        logger.info("开始回查本地事务状态。{}",messageExt.getTransactionId());
        LocalTransactionState state;
        String transactionId = messageExt.getTransactionId();
        if (transactionLogService.get(transactionId)>0){
            state = LocalTransactionState.COMMIT_MESSAGE;
        }else {
            state = LocalTransactionState.UNKNOW;
        }
        logger.info("结束本地事务状态查询:{}",state);
        return state;
    }
}

//业务实现类
@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    OrderMapper orderMapper;
    @Autowired
    TransactionLogMapper transactionLogMapper;
    @Autowired
    TransactionProducer producer;

    Snowflake snowflake = new Snowflake(1,1);
    Logger logger = LoggerFactory.getLogger(this.getClass());

    //执行本地事务时调用,将订单数据和事务日志写入本地数据库
    @Transactional
    @Override
    public void createOrder(OrderDTO orderDTO,String transactionId){

        //1.创建订单
        Order order = new Order();
        BeanUtils.copyProperties(orderDTO,order);
        orderMapper.createOrder(order);

        //2.写入事务日志
        TransactionLog log = new TransactionLog();
        log.setId(transactionId);
        log.setBusiness("order");
        log.setForeignKey(String.valueOf(order.getId()));
        transactionLogMapper.insert(log);

        logger.info("订单创建完成。{}",orderDTO);
    }

    //前端调用,只用于向RocketMQ发送事务消息
    @Override
    public void createOrder(OrderDTO order) throws MQClientException {
        order.setId(snowflake.nextId());
        order.setOrderNo(snowflake.nextIdStr());
        producer.send(JSON.toJSONString(order),"order");
    }
}

//调用
@RestController
public class OrderController {

    @Autowired
    OrderService orderService;
    Logger logger = LoggerFactory.getLogger(this.getClass());

    @PostMapping("/create_order")
    public void createOrder(@RequestBody OrderDTO order) throws MQClientException {
        logger.info("接收到订单数据:{}",order.getCommodityCode());
        orderService.createOrder(order);
    }
}

```java
//积分服务消费
@Component
public class OrderListener implements MessageListenerConcurrently {

    @Autowired
    PointsService pointsService;
    Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
        logger.info("消费者线程监听到消息。");
        try{
            for (MessageExt message:list) {
                logger.info("开始处理订单数据,准备增加积分....");
                OrderDTO order  = JSONObject.parseObject(message.getBody(), OrderDTO.class);
                pointsService.increasePoints(order);
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }catch (Exception e){
            logger.error("处理消费者数据发生异常。{}",e);
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
    }
}


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值