分布式消息中间件-Rocketmq

简述        

        今天要给大家分享的是分布式消息中间件。消息中间件主要是实现分布式系统中解耦、异步消息、流量销锋、日志处理等场景,后面我也会结合一些场景进行探讨。现在生产中用的最多的消息队列有Activemq,rabbitmq,kafka,rocketmq等。

        不过这个题目写的有点大。为什么这样说呢,因为虽然这样写,但实际上我这边是以Jms规范和rocketmq为主来分享的。大家一般都是遵从的是公司技术规范和选择(也就是公司用什么基础组件基本上都是选择好的)。基于我的一些工作经历,所以我也是将重心放在了rocketmq,并且认为大家可能水平不一致,所以也从一些基础等方面对rocketmq做了一个较全面的分享。我们分享的版本也是基于3.2.6进行的。我上面也会贴一些api实例。可能是因为我是做技术的,所以我一直认为技术分享,如果只是天方夜谭没有实质性,也就是有些同学常说的没有干活,浪费时间。

        我这边呢主要是基于这几个大块来进行的分享,JMS规范、Rocketmq的介绍、部署方式、特性的一些使用。

JMS规范

        我们首先看下jms规范,我上面也说到了本次分享的重点是rocketmq,rocketmq虽然不完全基于jms规范,但是他参考了jms规范和 CORBA Notification 规范等,可以说是青出于蓝而胜于蓝,jms规范我这边会快速的给大家过一下,我们把更多的时间留给rocketmq,后面讲rocketmq的时候也会将两者做一个简单的比较。我会给大家介绍一下什么是jms,相关的概念,对象模型、详细消费、编程实例。

什么是jms呢

        jms其实就是类似于jdbc的一套接口规范,但不同的是他是面向的消息服务,提供一套标准API接口,大部分厂商都会参考jms规范,不过我们后面要讲到的rocketmq却没有严格遵守jms规范,后面我们会讲到。

       一些常见的jms厂商有:IBM 的 MQSeries、BEA的 Weblogic JMS service和 Progress 的 SonicMQ,还有APACHE开源的ActiveMQ。这里面Activemq这个也是我接触到的第一个mq,现在市场份额也是很大的,京东商城采用的就是这个。

基本概念

        他的一些概念主要是有这些,我们挨个儿看一下:

  • 发送者( Sender)

       也就是消息的生产者,俗的将就是创建并发送消息的JMS客户端。

  • 接收者( Receiver)

        也就是消息消费者,接收订制消息的并按照相应的业务逻辑进行处理,最终将结果反馈给mq的服务端。

  • 点对点( Point-to-Point(P2P) )

        点对点就是一对一的关系,一个消息发出只有一个接受者所处理。每个消息都被发送到一个特定的队列,接收者从队列中获取消息。队列保留着消息,直到他们被消费或超时。

  • 发布订阅( Publish/Subscribe(Pub/Sub) )

        1、客户端将消息发送到主题。多个发布者将消息发送到Topic,系统将这些消息传递给多个订阅者。

        2、如果你希望发送的消息可以不被做任何处理、或者被一个消息者处理、或者可以被多个消费者处理的话,那么可以采用Pub/Sub模型

  • 消息队列(Queue)

        一个容纳那些被发送的等待阅读的消息的区域。与队列名字所暗示的意思不同,消息的接受顺序并不一定要与消息的发送顺序相同。一旦一个消息被阅读,该消息将被从队列中移走。

  • 主题(Topic)

        一种支持发送消息给多个订阅者的机制。

  • 发布者(Publisher)

        同生产者

  • 订阅者(Subscriber)

        针对同一主题的多个消费者

 点对点

大家看这个图,这里就是发布订阅的关系

发布订阅

大家可以通过这个图在熟悉一下发布订阅的关系

对象模型

  • (1) ConnectionFactory

        创建Connection对象的工厂,针对两种不同的jms消息模型,分别有QueueConnectionFactory和TopicConnectionFactory两种(很显然是基于点对点和和发布订阅的两种方式分别创建连接工厂的)。可以通过JNDI来查找ConnectionFactory对象。

  • (2) Destination

        Destination的意思是消息生产者的消息发送目标或者说消息消费者的消息来源。对于消息生产者来说,它的Destination是某个队列(Queue)或某个主题(Topic);对于消息消费者来说,它的Destination也是某个队列或主题(即消息来源)。所以,Destination实际上就是两种类型的对象:Queue、Topic可以通过JNDI来查找Destination。

  • (3) Connection

        Connection表示在客户端和JMS系统之间建立的链接(对TCP/IP socket的包装)。Connection可以产生一个或多个Session。跟ConnectionFactory一样,Connection也有两种类型:QueueConnection和TopicConnection。

  • (4) Session

        Session是我们操作消息的接口。可以通过session创建生产者、消费者、消息等。Session提供了事务的功能。当我们需要使用session发送/接收多个消息时,可以将这些发送/接收动作放到一个事务中。同样,也分QueueSession和TopicSession。

  • (5) 消息的生产者

        消息生产者由Session创建,并用于将消息发送到Destination。同样,消息生产者分两种类型:QueueSender和TopicPublisher。可以调用消息生产者的方法(send或publish方法)发送消息。

  • (6) 消息消费者

        消息消费者由Session创建,用于接收被发送到Destination的消息。两种类型:QueueReceiver和TopicSubscriber。可分别通过session的createReceiver(Queue)或createSubscriber(Topic)来创建。当然,也可以session的creatDurableSubscriber方法来创建持久化的订阅者。

  • (7) MessageListener

        消息监听器。如果注册了消息监听器,一旦消息到达,将自动调用监听器的onMessage方法。我们后面消息消费还会看到。

消息消费

在JMS中,消息的产生和消息是异步的。对于消费来说,JMS的消息者可以通过两种方式来消费消息。

○ 同步

订阅者或接收者调用receive方法来接收消息,receive方法在能够接收到消息之前(或超时之前)将一直阻塞

○ 异步

订阅者或接收者可以注册为一个消息监听器。当消息到达之后,系统自动调用监听器的onMessage方法。

编程实例

举个例子:

上面说了这么多概念性的东西可能大家都觉得没啥意思,个人感觉程序员还是用程序来说比较清楚,我这边通过activemq的部分代码来简单说明一下上面说道的一些JMS规范

 
  1. public void init(){

  2. try {

  3. //创建一个链接工厂(用户名,密码,broker的url地址)

  4. connectionFactory = new ActiveMQConnectionFactory(USERNAME,PASSWORD,BROKEN_URL);

  5. //从工厂中创建一个链接

  6. connection = connectionFactory.createConnection();

  7. //开启链接

  8. connection.start();

  9. //创建一个会话

  10. session = connection.createSession(true,Session.SESSION_TRANSACTED);

  11. } catch (JMSException e) {

  12. e.printStackTrace();

  13. }

  14. }

    公共部分:也就是说不管你是消息的生产者还是消息的消费者都需要这些步骤

  1. 首先我们需要创建一个连接工厂,当然这里我们需要输入用户性和密码还有就是broker的url
  2. 然后我们根据连接工厂创建了一个连接,此刻这个工厂并没有和broker简历连接
  3. 调用start方法就和broker建立了连接,这里我大概解释一下broker
    1. broker:消息队列核心,相当于一个控制中心,负责路由消息、保存订阅和连接、消息确认和控制事务,activemq可以配置多个
  4. 创建一个session,上面我们提到过所有的消息操作都是与session进行的
 
  1. public void sendMsg(String queueName){

  2. try {

  3. //创建一个消息队列(此处也就是在创建Destination)

  4. Queue queue = session.createQueue(queueName);

  5. //消息生产者

  6. MessageProducer messageProducer = null;

  7. if(threadLocal.get()!=null){

  8. messageProducer = threadLocal.get();

  9. }else{

  10. messageProducer = session.createProducer(queue);

  11. threadLocal.set(messageProducer);

  12. }

  13. while(true){

  14. Thread.sleep(1000);

  15. int num = count.getAndIncrement();

  16. //创建一条消息

  17. TextMessage msg = session.createTextMessage(Thread.currentThread().getName()+

  18. "productor:生产消息,count:"+num);

  19. //发送消息

  20. messageProducer.send(msg);

  21. //提交事务

  22. session.commit();

  23. }

  24. } catch (JMSException e) {

  25. e.printStackTrace();

  26. } catch (InterruptedException e) {

  27. e.printStackTrace();

  28. }

  29. }

生产:配置完上面的公共部分我们就迫不及待的把消息生产出来吧,我这边说的是点对点的方式

  1. 通过session创建一个Destination,我这边直接就用了queue了
  2. 接下来我们需要创建一个消息的生产者
  3. 我这边就循环每1s发送一条消息
  4. 这边看到我们的消息也是用session来创建的,这里面我们用的是文本的消息类型
  5. 发送消息
  6. 提交这次发送,至此我们的消息就发送到了broker上了,用过activemq的同学都知道,activemq提供了一个很好用的界面可以查到你的消息的状态,包括是否消费等

消费:消费我们上面也提到了两种方式,同步和异步,我这边准备了两份代码分别说明了一下

 
  1. public void doMessage(String queueName){

  2. try {

  3. //创建Destination

  4. Queue queue = session.createQueue(queueName);

  5. MessageConsumer consumer = null;

  6.  
  7. while(true){

  8. Thread.sleep(1000);

  9. TextMessage msg = (TextMessage) consumer.receive();

  10. if(msg!=null) {

  11. msg.acknowledge();

  12. System.out.println(Thread.currentThread().getName()+": Consumer:我是消费者,我正在消费Msg"+msg.getText()+"--->"+count.getAndIncrement());

  13. }else {

  14. break;

  15. }

  16. }

  17. } catch (JMSException e) {

  18. e.printStackTrace();

  19. } catch (InterruptedException e) {

  20. e.printStackTrace();

  21. }

  22. }

同步:可以看到消息会一直阻塞到有消息才会继续

  1. 通过session创建一个Destination,我这边直接就用了queue了
  2. 创建了一个Consumer
  3. 做了一个死循环,类似于ServerSocket的accept方法,我们的receive会阻塞到这里,直到有消息
  4. 如果消息不为空告知消息消费成功
 
  1. consumer.setMessageListener(MessageListener {

  2. public void onMessage(Message msg) {

  3. try {

  4. String message = ((TextMessage) msg).getText();

  5. if(msg != null){

  6. msg.acknowledge

  7. System.out.println("成功消费消息:"+message);

  8. }

  9. } catch (JMSException e) {

  10. // TODO Auto-generated catch block

  11. e.printStackTrace();

  12. }

  13. }

  14. );

异步:前两部和上面是一样的,我们从第三步说起

3、注册了一个监听接口的实现,当有消息时就调用onMessage的实现,后面就一样了

RocketMQ介绍

简介

        rocketmq是阿里巴巴开源的一款分布式的消息中间件,他源于jms规范但是不遵守jms规范。对于分布式只一点,如果你了用过其他mq并且了解过rocketmq,就知道rocketmq天生就是分布式的,可以说是broker、provider、consumer等各种分布式。

        他的大概特点我们可以看下,后面的讲解也是基于这些特点的:

  • 能够保证严格的消息顺序(需要集群的支持)
  • 提供丰富的消息拉取模式(可以任意定义你的拉取方式,exmaple中也提供了一个很好的例子)
  • 高效的订阅者水平扩展能力(通过一个consumerGroup的方式做到consumer的方便扩容)
  • 实时的消息订阅机制(消息的实时推送,类似于上面咱们的异步消费的方式)
  • 亿级消息堆积能力(轻松完成系统销锋)

发展历史

这块我觉得还是有必要说一下的,这样可以帮助我们全面看一个事务的发展规律

一、 Metaq(Metamorphosis) 1.x

由开源社区 killme2008 维护,开源社区非常活跃。

https://github.com/killme2008/Metamorphosis

二、 Metaq 2.x

于 2012 年 10 月份上线,在淘宝内部被广泛使用

三、改名为RocketMQ

公司内部开源共建的原则,rocketmq只维护了核心功能,可以方面每个SUB(业务单元)定制,当然阿里内部之所以提供高效的新能出了rocketmq本身之外还依赖于另外一个产品(oceanbase阳振坤)

https://github.com/apache/rocketmq

当前版本为4.2.0-SNAPSHOT

选择的理由

    这里我们对rocketmq的特性进行一些介绍,

  • 强调集群无单点,可扩展,任意一点高可用,水平可扩展

        方便集群配置,而且容易扩展(横向和纵向),通过slave的方式每一点都可以实现高可用

  • 支持上万个队列,顺序消息

        顺序消费是实现在同一队列的,如果高并发的情况就需要队列的支持,rocketmq可以满足上万个队列同事存在

  • 任性定制你的消息过滤

        rocketmq提供了两种类型的消息过滤,也可以说三种可以通过topic进行消息过滤、可以通过tag进行消息过滤、还可以通过filter的方式任意定制过滤

  • 消息的可靠性(无Buffer,持久化,容错,回溯消费)

        消息无buffer就不用担心buffer回满的情况,rocketmq的所有消息都是持久化的,生产者本身可以进行错误重试,发送者也会按照时间阶梯的方式进行消息重发,消息回溯说的是可以按照指定的时间进行消息的重新消费,既可以向前也可以向后(前提条件是要注意消息的擦除时间)

  • 海量消息堆积能力,消息堆积后,写入低延迟

        针对于provider需要配合部署方式,对于consumer,如果是集群方式一旦master返现消息堆积会向consumer下发一个重定向指令,此时consumer就可以从slave进行数据消费了

  • 分布式事务

        我个人感觉rocketmq3.2.6对这一块说的不是很清晰,而且官方也说现在这块存在缺陷(会令系统pagecache过多),所以线上建议还是少用为好,这块我也是后面给大家看一下列子

  • 消息失败重试机制

        针对provider的重试,当消息发送到选定的broker时如果出现失败会自动选择其他的broker进行重发,默认重试三次,当然重试次数要在消息发送的超时时间范围内。

        针对consumer的重试,如果消息因为各种原因没有消费成功,会自动加入到重试队列,一般情况如果是因为网络等问题连续重试也是照样失败,所以rocketmq也是采用阶梯重试的方式。

  • 定时消费

出了上面的配置,在发送消息是也可以针对message设置setDelayTimeLevel

  • 活跃的开源社区

现在rocketmq成为了apache的一款开源产品,活跃度也是不容怀疑的

  • 成熟度(经过双十一考验)

针对本身的成熟度,我们看看这么多年的双十一就可想而知了

专有术语

  • NameServer

        这里我们可以理解成类似于zk的一个注册中心,而且rocketmq最初也是基于zk作为注册中心的,现在相当于为rocketmq自定义了一个注册中心,代码不超过1000行。RocketMQ 有多种配置方式可以令客户端找到 Name Server, 然后通过 Name Server 再找到 Broker,分别如下,优先级由高到低,高优兇级会覆盖低优兇级。客户端提供http和ip+端口号的两种方式,推荐使用http的方式可以实现nameserver的热部署

  • Push Consumer

        Consumer 的一种,应用通常通过 Consumer 对象注册一个 Listener 接口,一旦收到消息,Consumer 对象立刻回调 Listener 接口方法,类似于activemq的方式

  • Pull Consume

        Consumer 的一种,应用通常主动调用 Consumer 的拉消息方法从 Broker 拉消息,主动权由应用控制

  • Producer Group

        一类producer的集合名称,这类producer通常发送一类消息,且发送逻辑一致

  • Consumer Group

        同上,consumer的集合名称

  • Broker

        消息中转的角色,负责存储消息(实际的存储是调用的store组件完成的),转发消息,一般也成为server,通jms中的provider

  • Message Filter

        可以实现高级的自定义的消息过滤,java编写

  • Master/Slave

        集群的主从关系,broker的name相同,brokerid=0的为主,大于0的为从

部署方式

物理部署

NameServer :类似云zk的集群,主要是维护了broker的相关内容,进行存取;节点之间无任何数据同步

1、接收broker的注册,注销请求

2、Producer获取topic下所有的BrokerQueue,put消息

3、Consumer获取topic下所有的BrokerQueue,get消息

Broker :

部署相对复杂,Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应Master,Master和Slave的对应关系通过制定相同的BrokerName来确定,通过制定BrokerId来区分主从,如果是0则为Master,如果大于0则为Slave。Master也可以部署多个。每个Broker与Name Server集群中的所有节点建立长连接,定时注册Topic信息到所有的NameServer

Producer:

与Name sever集群中的其中一个节点(随意选择)建立长连接,定期的从Name Server取Topic路由信息,并向提供Topic服务的Master简历长连接,且定时向Master发送心跳。Producer完全无状态,可以集群部署。

Consumer:

与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic的Master、Slave简历长连接,且定时向Master、Slave发送心跳,Consumer既可以从Master订阅消息,也可以从Slave订阅消息,订阅规则有Broker配置决定。

逻辑部署

Producer Group:

用来表示一个发送消息应用,一个Producer Group下办好多个Producer实例,可是多台机器,也可以是一台机器的多个线程,或者一个进程的多个Producer对象,一个Producer Group可以发送多个Topic消息,Producer Group的作用如下:

1、标识一类Producer(分布式)

2、可以通过运维工具查询这个发送消息应用有多少个Producer是咧

3、发送分布式事务消息时,如果Producer中途意外当即,Broker会主动回调Producer Group内的任意一台机器来确认事务状态。

Consumer Group:

用来表示一个消费消息应用,一个Consumer Group下包含多个Consumer实例,可以是多台机器,也可以是多个进程,或者是一个进程的多个Consumer对象。一个Consumer Group下的多个Consumer以均摊方式消费消息。如果设置为广播方式,那么这个Consumer Group下的每个实例都消费全量数据。

单Master模式

      只有一个 Master节点

  • 优点:配置简单,方便部署
  • 缺点:这种方式风险较大,一旦Broker重启或者宕机时,会导致整个服务不可用,不建议线上环境使用

多Master模式

      一个集群无 Slave,全是 Master,例如 2 个 Master 或者 3 个 Master

  • 优点:配置简单,单个Master 宕机或重启维护对应用无影响,在磁盘配置为RAID10 时,即使机器宕机不可恢复情况下,由与 RAID10磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢)。性能最高。多 Master 多 Slave 模式,异步复制
  • 缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到受到影响

多Master多Slave模式(异步复制)

        每个 Master 配置一个 Slave,有多对Master-Slave, HA,采用异步复制方式,主备有短暂消息延迟,毫秒级。

  • 优点:即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,因为Master 宕机后,消费者仍然可以从 Slave消费,此过程对应用透明。不需要人工干预。性能同多 Master 模式几乎一样。
  • 缺点: Master 宕机,磁盘损坏情况,会丢失少量消息。

多Master多Slave模式(同步双写)

        每个 Master 配置一个 Slave,有多对Master-Slave, HA采用同步双写方式,主备都写成功,向应用返回成功。

  • 优点:数据与服务都无单点, Master宕机情况下,消息无延迟,服务可用性与数据可用性都非常高
  • 缺点:性能比异步复制模式略低,大约低 10%左右,发送单个消息的 RT会略高。目前主宕机后,备机不能自动切换为主机,后续会支持自动切换功能

特性使用

Quick start

Producer:

 
  1. mport com.alibaba.rocketmq.client.exception.MQClientException;

  2. import com.alibaba.rocketmq.client.producer.DefaultMQProducer;

  3. import com.alibaba.rocketmq.client.producer.SendResult;

  4. import com.alibaba.rocketmq.common.message.Message;

  5.  
  6.  
  7. /**

  8. * Producer,发送消息

  9. *

  10. */

  11. public class Producer {

  12. public static void main(String[] args) throws MQClientException, InterruptedException {

  13. DefaultMQProducer producer = new DefaultMQProducer("pay_topic_01");

  14. producer.setNamesrvAddr("100.8.8.88:9876");

  15. producer.start();

  16.  
  17. for (int i = 0; i < 1000; i++) {

  18. try {

  19. Message msg = new Message("TopicTest",// topic

  20. "TagA",// tag

  21. ("Hello RocketMQ " + i).getBytes()// body

  22. );

  23. SendResult sendResult = producer.send(msg);

  24. System.out.println(sendResult);

  25. }

  26. catch (Exception e) {

  27. e.printStackTrace();

  28. Thread.sleep(1000);

  29. }

  30. }

  31. producer.shutdown();

  32. }

  33. }

1、创建一个Producer的,这里我们看到rocketmq的创建producer很简单只输入一个Group Name名字就可以,不向activemq那么复杂

2、第二步就是制定Name Server的地址,这里注意两点,一个就是nameserver的默认端口是9876,另一个就是多个nameserver集群用分号来分割

3、我这边循环发送了1000个消息

4、消息创建也很简单,第一个参数是topic,第二个就是tags(多个tag用||连接),第三个参宿是消息内容

5、调用send方法就能发送成功了,不用想actimemq那样需要commit

整个的过程是很清晰的

Consumer:

 
  1. import java.util.List;

  2.  
  3. import com.alibaba.rocketmq.client.consumer.DefaultMQPushConsumer;

  4. import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;

  5. import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;

  6. import com.alibaba.rocketmq.client.consumer.listener.MessageListenerConcurrently;

  7. import com.alibaba.rocketmq.client.exception.MQClientException;

  8. import com.alibaba.rocketmq.common.consumer.ConsumeFromWhere;

  9. import com.alibaba.rocketmq.common.message.MessageExt;

  10.  
  11.  
  12. /**

  13. * Consumer,订阅消息

  14. */

  15. public class Consumer {

  16.  
  17. public static void main(String[] args) throws InterruptedException, MQClientException {

  18. DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");

  19. consumer.setNamesrvAddr("100.8.8.88:9876");

  20. /**

  21. * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br>

  22. * 如果非第一次启动,那么按照上次消费的位置继续消费

  23. */

  24. consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

  25.  
  26. consumer.subscribe("TopicTest", "*");

  27.  
  28. consumer.registerMessageListener(new MessageListenerConcurrently() {

  29.  
  30. @Override

  31. public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,

  32. ConsumeConcurrentlyContext context) {

  33. System.out.println(Thread.currentThread().getName() + " Receive New Messages: " + msgs);

  34. return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;

  35. }

  36. });

  37.  
  38. consumer.start();

  39.  
  40. System.out.println("Consumer Started.");

  41. }

  42. }

1、前两步和Producer是一样的

2、这里可以设置从那个位置开始读取消息,一般我们会从头部开始读取消费,系统中注意去重,也就是幂等

3、订阅topic,第一个参数是topic名字,第二个是tag,如果为*的就是全部消息

4、注册一个监听,如果有消息就会实时的推送到Consumer,调用consumeMessage进行消费,这里我们看到msgs是一个List,默认每次推送的是一条消息。

5、进行消息的消费逻辑,消费成功后会返回CONSUME_SUCCESS状态

消息过滤

RocketMq的消息过滤是从订阅的时候开始的,我们看到刚才的例子都是通过topic的tags进行的过滤,这个要求Producer发送的事后指定tags,这个和前面有点矛盾,但是前面只是进行了分组,并未进行过滤。Consumer在订阅消费的时候指定的tags才会对消息进行过滤,这种是简单的过滤方式,不过也可以满足我们大部分的消息过滤。更高级的过滤就是我们这张片子所展示的这种

1、前面和后面部分不变,红色框部分需要指定一个过滤类,之前这里是tags

2、我们看到所有的过滤类都要直接或者间接实现MessageFilter接口,并且需要覆盖match方法

3、在方法里面就可以写自己的过滤逻辑了,这个地方出了用事先制定的属性也可以反序列化这些消息内容进行消息解析,针对消息体的过滤

顺序消息

因为一些消息可以需要按照顺序消费才有意义,比如某例子现在是异步去执行的当然现在是采用的定时的方式,比如我们把现在的模式套上来,看看顺序消费是一个什么样子。订单创建》分批》打包》外发。。。。,也就是必须严格按照顺序才有意义。那么我们如何保证这批消息的顺序消费就显得很重要了。rocketmq实现的方式也很简单,只要我们把这些消息都放到一个队列中就能够做到顺序消费了,实际上rocketmq的顺序消费有两种方式,一种是普通的顺序消费(多Master多Slave的异步复制),一种是严格的顺序消费(多Master多Slave的同步双写)。

 
  1. import java.util.List;

  2.  
  3. import com.alibaba.rocketmq.client.exception.MQBrokerException;

  4. import com.alibaba.rocketmq.client.exception.MQClientException;

  5. import com.alibaba.rocketmq.client.producer.DefaultMQProducer;

  6. import com.alibaba.rocketmq.client.producer.MQProducer;

  7. import com.alibaba.rocketmq.client.producer.MessageQueueSelector;

  8. import com.alibaba.rocketmq.client.producer.SendResult;

  9. import com.alibaba.rocketmq.common.message.Message;

  10. import com.alibaba.rocketmq.common.message.MessageQueue;

  11. import com.alibaba.rocketmq.remoting.exception.RemotingException;

  12.  
  13.  
  14. /**

  15. * Producer,发送顺序消息

  16. */

  17. public class Producer {

  18. public static void main(String[] args) {

  19. try {

  20. MQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");

  21. producer.setNamesrvAddr("100.8.8.88:9876");

  22. producer.start();

  23.  
  24. String[] tags = new String[] { "TagA", "TagB", "TagC", "TagD", "TagE" };

  25.  
  26. for (int i = 0; i < 100; i++) {

  27. // 订单ID相同的消息要有序

  28. int orderId = i % 10;

  29. Message msg = new Message("TopicTestjjj", tags[i % tags.length], "KEY" + i,

  30. ("Hello RocketMQ " + i).getBytes());

  31.  
  32. SendResult sendResult = producer.send(msg, new MessageQueueSelector() {

  33. @Override

  34. public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {

  35. Integer id = (Integer) arg;

  36. int index = id % mqs.size();

  37. return mqs.get(index);

  38. }

  39. }, orderId);

  40.  
  41. System.out.println(sendResult);

  42. }

  43.  
  44. producer.shutdown();

  45. }

  46. catch (MQClientException e) {

  47. e.printStackTrace();

  48. }

  49. catch (RemotingException e) {

  50. e.printStackTrace();

  51. }

  52. catch (MQBrokerException e) {

  53. e.printStackTrace();

  54. }

  55. catch (InterruptedException e) {

  56. e.printStackTrace();

  57. }

  58. }

  59. }

1、首先要保障消息要同时在一个topic中

2、要保障要发送的消息有相同的tag

3、在发送时要保障将数据发送到同一个队列(queue),我们这里采用的取模的方式

前面我们说过rocketmq可以同时支持上完个队列,这个也是为了顺序消费来考虑的

事务消息

说道事务,我先给大家举个例子,比如有两个账户张三、李四,张三要给李四转10块钱,以下都在同一个事务中进行,锁定是通过事务中完成的

1、锁定张三和李四的账户

2、判断张三的账户是否大于等于10块钱,如果大于等于则继续,小于则返回,我们这里只讨论大于等于的

3、从张三的账户上减去10块

4、向李四的账户增加10块

5、解锁账户完成交易

update account set amount = amount - 100 where userNo='zhangsan' and amount >=10

update account set amount = amount + 100 where userNo='lisi'

如果是分布式事务就要考虑到两个用户账户的一致性,我们就从分布式的角度来分析一下

1、锁定张三的账户,同时通过网络锁定李四的账户(可以理解成冻结金额)

2、判断张三的账户是否大于等于10块钱,如果大于等于则继续,小于则返回,我们这里只讨论大于等于的

3、从张三的账户上减去10块

4、通过网络向李四的账户增加10块

5、解锁张三账户完成交易,通过网络解锁李四的账户,时间基本上是累计的

通过rocketmq怎么做这个事儿呢,首先通过rocketmq做这个事儿我们就要分清一下角色,张三为事务的发起者也就是消息的发送者,相对李四就是消息的消费者了,rocketmq可以理解成中间账户,默认Consumer都会成功,如果不成功官方推荐人工介入。

1、判断张三的账户金额大于10

2、同时张三的账户减去10

3、同时丢出一个mq消息给rocketmq,两个要确保放在一个db事务中(此时的消息只是处于prapared阶段,不会被Consumer所消费)

4、如果本地事务执行成功则向rocketmq发送commit

5、如果第四部出现了本Consumer宕机,也就是rocketmq没有收到commit,此刻消息是是未知,所以他会向任意一台Producer来确认当前消息的状态

6、从此保障了本地账户和rocketmq的一致性

中控如下:

 
  1. import com.alibaba.rocketmq.client.exception.MQClientException;

  2. import com.alibaba.rocketmq.client.producer.SendResult;

  3. import com.alibaba.rocketmq.client.producer.TransactionCheckListener;

  4. import com.alibaba.rocketmq.client.producer.TransactionMQProducer;

  5. import com.alibaba.rocketmq.common.message.Message;

  6.  
  7.  
  8. /**

  9. * 发送事务消息例子

  10. *

  11. */

  12. public class TransactionProducer {

  13. public static void main(String[] args) throws MQClientException, InterruptedException {

  14.  
  15. TransactionCheckListener transactionCheckListener = new TransactionCheckListenerImpl();

  16. TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name");

  17. producer.setNamesrvAddr("100.8.8.88:9876");

  18. // 事务回查最小并发数

  19. producer.setCheckThreadPoolMinSize(2);

  20. // 事务回查最大并发数

  21. producer.setCheckThreadPoolMaxSize(2);

  22. // 队列数

  23. producer.setCheckRequestHoldMax(2000);

  24. producer.setTransactionCheckListener(transactionCheckListener);

  25. producer.start();

  26. String[] tags = new String[] { "TagA", "TagB", "TagC", "TagD", "TagE" };

  27. TransactionExecuterImpl tranExecuter = new TransactionExecuterImpl();

  28. for (int i = 0; i < 100; i++) {

  29. try {

  30. Message msg =

  31. new Message("TopicTest", tags[i % tags.length], "KEY" + i,

  32. ("Hello RocketMQ " + i).getBytes());

  33. SendResult sendResult = producer.sendMessageInTransaction(msg, tranExecuter, null);

  34. System.out.println(sendResult);

  35. }

  36. catch (MQClientException e) {

  37. e.printStackTrace();

  38. }

  39. }

  40.  
  41. for (int i = 0; i < 100000; i++) {

  42. Thread.sleep(1000);

  43. }

  44.  
  45. producer.shutdown();

  46.  
  47. }

  48. }

本地事务:

 
  1. import java.util.concurrent.atomic.AtomicInteger;

  2.  
  3. import com.alibaba.rocketmq.client.producer.LocalTransactionExecuter;

  4. import com.alibaba.rocketmq.client.producer.LocalTransactionState;

  5. import com.alibaba.rocketmq.common.message.Message;

  6.  
  7.  
  8. /**

  9. * 执行本地事务

  10. */

  11. public class TransactionExecuterImpl implements LocalTransactionExecuter {

  12. private AtomicInteger transactionIndex = new AtomicInteger(1);

  13.  
  14.  
  15. @Override

  16. public LocalTransactionState executeLocalTransactionBranch(final Message msg, final Object arg) {

  17. int value = transactionIndex.getAndIncrement();

  18.  
  19. if (value == 0) {

  20. throw new RuntimeException("Could not find db");

  21. }

  22. else if ((value % 5) == 0) {

  23. return LocalTransactionState.ROLLBACK_MESSAGE;

  24. }

  25. else if ((value % 4) == 0) {

  26. return LocalTransactionState.COMMIT_MESSAGE;

  27. }

  28.  
  29. return LocalTransactionState.UNKNOW;

  30. }

  31. }

回调检查点:

 
  1. import java.util.concurrent.atomic.AtomicInteger;

  2.  
  3. import com.alibaba.rocketmq.client.producer.LocalTransactionState;

  4. import com.alibaba.rocketmq.client.producer.TransactionCheckListener;

  5. import com.alibaba.rocketmq.common.message.MessageExt;

  6.  
  7.  
  8. /**

  9. * 未决事务,服务器回查客户端

  10. */

  11. public class TransactionCheckListenerImpl implements TransactionCheckListener {

  12. private AtomicInteger transactionIndex = new AtomicInteger(0);

  13.  
  14.  
  15. @Override

  16. public LocalTransactionState checkLocalTransactionState(MessageExt msg) {

  17. System.out.println("server checking TrMsg " + msg.toString());

  18.  
  19. int value = transactionIndex.getAndIncrement();

  20. if ((value % 6) == 0) {

  21. throw new RuntimeException("Could not find db");

  22. }

  23. else if ((value % 5) == 0) {

  24. return LocalTransactionState.ROLLBACK_MESSAGE;

  25. }

  26. else if ((value % 4) == 0) {

  27. return LocalTransactionState.COMMIT_MESSAGE;

  28. }

  29.  
  30. return LocalTransactionState.UNKNOW;

  31. }

  32. }

点对点/广播

点对点和发布订阅的两种模式上面我们已经说了很多,这里只要我们在consumer里面配置MessageModel就可以做到两种模式的消费,需要注意的是这里如果配置了发布订阅的模式那么上面提Consumer的负载均衡将不生效(Consumer Name)

 
  1. //发布订阅

  2. consumer.setMessageModel(MessageModel.BROADCASTING);

  3. //集群消费(默认)

  4. //consumer.setMessageModel(MessageModel.CLUSTERING);

推送/拉取

上面我们说了这么多其实都是采用消息推送的模式,注册监听,当有消息产生时就会实时的推送到Consumer进行消费,我这张图里面是消息拉取的方式。这个就相当与把主动权交给了应用自己来负责,当然这样也就给消费增加了复杂性,比如说offset的存储、定时拉取等,阿里这边也是给我们增加了一些遍历,提供了一个demo(文件夹名是simple),后续大家如果用到也可以参考一下。

 
  1. import java.util.HashMap;

  2. import java.util.Map;

  3. import java.util.Set;

  4.  
  5. import com.alibaba.rocketmq.client.consumer.DefaultMQPullConsumer;

  6. import com.alibaba.rocketmq.client.consumer.PullResult;

  7. import com.alibaba.rocketmq.client.exception.MQClientException;

  8. import com.alibaba.rocketmq.common.message.MessageQueue;

  9.  
  10.  
  11. /**

  12. * PullConsumer,订阅消息

  13. */

  14. public class PullConsumer {

  15. private static final Map<MessageQueue, Long> offseTable = new HashMap<MessageQueue, Long>();

  16.  
  17.  
  18. public static void main(String[] args) throws MQClientException {

  19. DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("please_rename_unique_group_name_5");

  20.  
  21. consumer.start();

  22.  
  23. Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("TopicTest");

  24. for (MessageQueue mq : mqs) {

  25. System.out.println("Consume from the queue: " + mq);

  26. SINGLE_MQ: while (true) {

  27. try {

  28. PullResult pullResult =

  29. consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);

  30. System.out.println(pullResult);

  31. putMessageQueueOffset(mq, pullResult.getNextBeginOffset());

  32. switch (pullResult.getPullStatus()) {

  33. case FOUND:

  34. // TODO

  35. break;

  36. case NO_MATCHED_MSG:

  37. break;

  38. case NO_NEW_MSG:

  39. break SINGLE_MQ;

  40. case OFFSET_ILLEGAL:

  41. break;

  42. default:

  43. break;

  44. }

  45. }

  46. catch (Exception e) {

  47. e.printStackTrace();

  48. }

  49. }

  50. }

  51.  
  52. consumer.shutdown();

  53. }

  54.  
  55.  
  56. private static void putMessageQueueOffset(MessageQueue mq, long offset) {

  57. offseTable.put(mq, offset);

  58. }

  59.  
  60.  
  61. private static long getMessageQueueOffset(MessageQueue mq) {

  62. Long offset = offseTable.get(mq);

  63. if (offset != null)

  64. return offset;

  65.  
  66. return 0;

  67. }

  68.  
  69. }

消息回溯

根据时间来设置消费进度,设置之前要关闭这个订阅组的所有consumer,设置完再启动,方可生效

  • 回溯消费是指 Consumer 已经消费成功的消息,由于业务上需求需要重新消费,Broker 在Consumer 投递成功消息后,消息仍然需要保留。并且重新消费一般是按照时间维度,例如由于 Consumer 系统故障,恢复后需要重新消费 1 小时前的数据,那举 Broker 要提供一种机制,可以按照时间维度来回退消费迕度
  • RocketMQ 支持按照时间回溯消费,时间维度精确到毫秒,可以向前回溯,也可以向后回溯
  • 操作: mqadmin resetOffsetByTime

截止到现在我们今天整个的分享就算是结束了,我这边只是对一些核心和一些常用的功能做了介绍,mq的东西还有很多比如:集群搭建的细节、使用场景、控制台命令、管理页面等,大家如果感兴趣也可以自行去学习一下,也可以找我来交流探讨。

猜你喜欢

转载自blog.csdn.net/scratlc/article/details/81038120