RocketMQ核心技术精讲与高并发抗压实战

1:特点

比较吃内存 内存至少1g 默认8g

1:支持集群模型,强调集群无单点,负载均衡以及水平扩展能力
2:亿级别的消息堆积能力
3:采用零拷贝原理Consumer 消费消息过程,使用了零拷贝 顺序写盘随机读

4:丰富的api 同步消息,异步消息 顺序消息,延迟消息,事务消息
5:底层通信框架采用netty nio
6: nameserver 代替zookpeer
7:消息消费失败重试机制,消息可查询



灵活可扩展性
RocketMQ 天然支持集群,其核心四组件(Name Server、Broker、Producer、Consumer)每一个都可以在没有单点故障的情况下进行水平扩展。

海量消息堆积能力
RocketMQ 采用零拷贝原理实现超大的消息的堆积能力,据说单机已可以支持亿级消息堆积,而且在堆积了这么多消息后依然保持写入低延迟。

支持顺序消息
可以保证消息消费者按照消息发送的顺序对消息进行消费。顺序消息分为全局有序和局部有序,一般推荐使用局部有序,即生产者通过将某一类消息按顺序发送至同一个队列来实现。

多种消息过滤方式
消息过滤分为在服务器端过滤和在消费端过滤。服务器端过滤时可以按照消息消费者的要求做过滤,优点是减少不必要消息传输,缺点是增加了消息服务器的负担,实现相对复杂。消费端过滤则完全由具体应用自定义实现,这种方式更加灵活,缺点是很多无用的消息会传输给消息消费者。

支持事务消息
RocketMQ 除了支持普通消息,顺序消息之外还支持事务消息,这个特性对于分布式事务来说提供了又一种解决思路。

回溯消费
回溯消费是指消费者已经消费成功的消息,由于业务上需求需要重新消费,RocketMQ 支持按照时间回溯消费,时间维度精确到毫秒,可以向前回溯,也可以向后回溯。

 

下面是一张 RocketMQ 的部署结构图,里面涉及了 RocketMQ 核心的四大组件:Name Server、Broker、Producer、Consumer ,每个组件都可以部署成集群模式进行水平扩展。

 

 

 

 

 

 

同步发送
同步发送指消息发送方发出数据后会在收到接收方发回响应之后才发下一个数据包。一般用于重要通知消息,例如重要通知邮件、营销短信。

异步发送
异步发送指发送方发出数据后,不等接收方发回响应,接着发送下个数据包,一般用于可能链路耗时较长而对响应时间敏感的业务场景,例如用户视频上传后通知启动转码服务。

单向发送
单向发送是指只负责发送消息而不等待服务器回应且没有回调函数触发,适用于某些耗时非常短但对可靠性要求并不高的场景,例如日志收集。

  • 同步发送
  • 异步发送
  • OneWay 发送

示例代码如下:

// 1、同步发送
SendResult sendResult = producer.send(msg);

//2、异步发送
producer.send(msg, new SendCallback() {
    @Override
    public void onSuccess(SendResult sendResult) {
    }
    @Override
    public void onException(Throwable e) {
    }
});

//3、 Oneway发送
producer.sendOneway(msg);
  • 1、同步发送
    Producer 向 broker 发送消息,阻塞当前线程等待 broker 响应 发送结果。
  • 2、异步发送
    Producer 首先构建一个向 broker 发送消息的任务,把该任务提交给线程池,等执行完该任务时,回调用户自定义的回调函数,执行处理结果。
  • 3、Oneway 发送
    Oneway 方式只负责发送请求,不等待应答,Producer 只负责把请求发出去,而不处理响应结果。

生产者组
生产者组(Producer Group)是一类 Producer 的集合,这类 Producer 通常发送一类消息并且发送逻辑一致,所以将这些 Producer 分组在一起。从部署结构上看生产者通过 Producer Group 的名字来标记自己是一个集群。

消费者
消费者(Consumer)负责消费消息,消费者从消息服务器拉取信息并将其输入用户应用程序。站在用户应用的角度消费者有两种类型:拉取型消费者、推送型消费者。

拉取型消费者
拉取型消费者(Pull Consumer)主动从消息服务器拉取信息,只要批量拉取到消息,用户应用就会启动消费过程,所以 Pull 称为主动消费型。

推送型消费者
推送型消费者(Push Consumer)封装了消息的拉取、消费进度和其他的内部维护工作,将消息到达时执行的回调接口留给用户应用程序来实现。所以 Push 称为被动消费类型,但从实现上看还是从消息服务器中拉取消息,不同于 Pull 的是 Push 首先要注册消费监听器,当监听器处触发后才开始消费消息。

消费者组
消费者组(Consumer Group)一类 Consumer 的集合名称,这类 Consumer 通常消费同一类消息并且消费逻辑一致,所以将这些 Consumer 分组在一起。消费者组与生产者组类似,都是将相同角色的分组在一起并命名,分组是个很精妙的概念设计,RocketMQ 正是通过这种分组机制,实现了天然的消息负载均衡。消费消息时通过 Consumer Group 实现了将消息分发到多个消费者服务器实例,比如某个 Topic 有9条消息,其中一个 Consumer Group 有3个实例(3个进程或3台机器),那么每个实例将均摊3条消息,这也意味着我们可以很方便的通过加机器来实现水平扩展。

消息服务器
消息服务器(Broker)是消息存储中心,主要作用是接收来自 Producer 的消息并存储, Consumer 从这里取得消息。它还存储与消息相关的元数据,包括用户组、消费进度偏移量、队列信息等。从部署结构图中可以看出 Broker 有 Master 和 Slave 两种类型,Master 既可以写又可以读,Slave 不可以写只可以读。从物理结构上看 Broker 的集群部署方式有四种:单 Master 、多 Master 、多 Master 多 Slave(同步刷盘)、多 Master多 Slave(异步刷盘)。

单 Master
这种方式一旦 Broker 重启或宕机会导致整个服务不可用,这种方式风险较大,所以显然不建议线上环境使用。

多 Master
所有消息服务器都是 Master ,没有 Slave 。这种方式优点是配置简单,单个 Master 宕机或重启维护对应用无影响。缺点是单台机器宕机期间,该机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受影响。

多 Master 多 Slave(异步复制)
每个 Master 配置一个 Slave,所以有多对 Master-Slave,消息采用异步复制方式,主备之间有毫秒级消息延迟。这种方式优点是消息丢失的非常少,且消息实时性不会受影响,Master 宕机后消费者可以继续从 Slave 消费,中间的过程对用户应用程序透明,不需要人工干预,性能同多 Master 方式几乎一样。缺点是 Master 宕机时在磁盘损坏情况下会丢失极少量消息。

多 Master 多 Slave(同步双写)
每个 Master 配置一个 Slave,所以有多对 Master-Slave ,消息采用同步双写方式,主备都写成功才返回成功。这种方式优点是数据与服务都没有单点问题,Master 宕机时消息无延迟,服务与数据的可用性非常高。缺点是性能相对异步复制方式略低,发送消息的延迟会略高。

名称服务器
名称服务器(NameServer)用来保存 Broker 相关元信息并给 Producer 和 Consumer 查找 Broker 信息。NameServer 被设计成几乎无状态的,可以横向扩展,节点之间相互之间无通信,通过部署多台机器来标记自己是一个伪集群。每个 Broker 在启动的时候会到 NameServer 注册,Producer 在发送消息前会根据 Topic 到 NameServer 获取到 Broker 的路由信息,Consumer 也会定时获取 Topic 的路由信息。所以从功能上看应该是和 ZooKeeper 差不多,据说 RocketMQ 的早期版本确实是使用的 ZooKeeper ,后来改为了自己实现的 NameServer 。

消息
消息(Message)就是要传输的信息。一条消息必须有一个主题(Topic),主题可以看做是你的信件要邮寄的地址。一条消息也可以拥有一个可选的标签(Tag)和额处的键值对,它们可以用于设置一个业务 key 并在 Broker 上查找此消息以便在开发期间查找问题。

主题
主题(Topic)可以看做消息的规类,它是消息的第一级类型。比如一个电商系统可以分为:交易消息、物流消息等,一条消息必须有一个 Topic 。Topic 与生产者和消费者的关系非常松散,一个 Topic 可以有0个、1个、多个生产者向其发送消息,一个生产者也可以同时向不同的 Topic 发送消息。一个 Topic 也可以被 0个、1个、多个消费者订阅。

标签
标签(Tag)可以看作子主题,它是消息的第二级类型,用于为用户提供额外的灵活性。使用标签,同一业务模块不同目的的消息就可以用相同 Topic 而不同的 Tag 来标识。比如交易消息又可以分为:交易创建消息、交易完成消息等,一条消息可以没有 Tag 。标签有助于保持您的代码干净和连贯,并且还可以为 RocketMQ 提供的查询系统提供帮助。

消息队列
消息队列(Message Queue),主题被划分为一个或多个子主题,即消息队列。一个 Topic 下可以设置多个消息队列,发送消息时执行该消息的 Topic ,RocketMQ 会轮询该 Topic 下的所有队列将消息发出去。下图 Broker 内部消息情况:

Broker 内部消息

消息消费模式
消息消费模式有两种:集群消费(Clustering)和广播消费(Broadcasting)。默认情况下就是集群消费,该模式下一个消费者集群共同消费一个主题的多个队列,一个队列只会被一个消费者消费,如果某个消费者挂掉,分组内其它消费者会接替挂掉的消费者继续消费。而广播消费消息会发给消费者组中的每一个消费者进行消费。

消息顺序
消息顺序(Message Order)有两种:顺序消费(Orderly)和并行消费(Concurrently)。顺序消费表示消息消费的顺序同生产者为每个消息队列发送的顺序一致,所以如果正在处理全局顺序是强制性的场景,需要确保使用的主题只有一个消息队列。并行消费不再保证消息顺序,消费的最大并行数量受每个消费者客户端指定的线程池限制。

工程实例

 3:

调整
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.0.0</version>
</plugin> 的位置
按照官方文档编译软件包生成apache-rocketmq.tar.gz

4:在vi/etc/hosts Hosts添加信息
192.168.11.128  rocketmq-nameserver1
192.168.11.128 rocketmq-master1
上传文件

创建存储路径

 

 配置文件

 

 

修改日志配置文

修改启动脚本参

启动NameServer

启动BrokerServer A

5:RocketMQ-Console

  • RocketMQ-Console是RocketMQ项目的扩展插件,是一个图形化管理控制台,提供Broker集群状态查看,Topic管理,Producer、Consumer状态展示,消息查询等常用功能,这个功能在安装好RocketMQ后需要额外单独安装、运行。

 进入rocketmq-externals项目GitHub地址,如下图,可看到RocketMQ项目的诸多扩展项目,其中就包含我们需要下载的rocketmq-console。 

进入项目文件夹并修改配置文件

#Name Server地址,修改成你自己的服务地址 rocketmq.config.namesrvAddr=10.0.74.198:9876;10.0.74.199:9876

将项目打成jar包,并运行jar文件。

$ mvn clean package -Dmaven.test.skip=true
$ java -jar target/rocketmq-console-ng-1.0.0.jar
#如果配置文件没有填写Name Server
$ java -jar target/rocketmq-console-ng-1.0.0.jar --rocketmq.config.namesrvAddr='10.0.74.198:9876;10.0.74.199:9876'

启动成功后,访问地址http://localhost:8080/rocketmq, 即可进入管理后台操作。
OPS
Cluster

命令行管理工具

6:引入依赖包

<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>${rocketmq.version}</version>
</dependency>

product 端代码
public class Producer {

public static void main(String[] args) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {

DefaultMQProducer producer = new DefaultMQProducer("test_quick_producer_name");

producer.setNamesrvAddr(Const.NAMESRV_ADDR_MASTER_SLAVE);

producer.start();

for(int i = 0 ; i <5; i ++)
{
// 1. 创建消息
Message
message = new Message("test_quick_topic", // 主题
"TagA", // 标签
"key" + i, // 用户自定义的key ,唯一的标识
("Hello RocketMQ" + i).getBytes()); // 消息内容实体(byte[])

SendResult sr = producer.send(message);
System.err.println("消息发出: " + sr);
}
producer.shutdown();
}}

 

 

 

7:consumer端代码
public class Consumer {


public static void main(String[] args) throws MQClientException {

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test_quick_consumer_name");

consumer.setNamesrvAddr(Const.NAMESRV_ADDR_MASTER_SLAVE);

consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);

consumer.subscribe("test_quick_topic", "*");第二个参数标识tag

consumer.registerMessageListener(new MessageListenerConcurrently() {

@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
MessageExt me = msgs.get(0);
try {
String topic = me.getTopic();
String tags = me.getTags();
String keys = me.getKeys();
// if(keys.equals("key1")) {
// System.err.println("消息消费失败..");
// int a = 1/0;
// }

String msgBody = new String(me.getBody(), RemotingHelper.DEFAULT_CHARSET);
System.err.println("topic: " + topic + ",tags: " + tags + ", keys: " + keys + ",body: " + msgBody);
} catch (Exception e) {
e.printStackTrace();
// int recousumeTimes = me.getReconsumeTimes();
// System.err.println("recousumeTimes: " + recousumeTimes);
// if(recousumeTimes == 3) {
// // 记录日志....
// // 做补偿处理
// return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
// }

return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});

consumer.start();
System.err.println("consumer start...");

}
}


亦可以这样写
consumer.registerMessageListener(new Listener());
class Listener implements MessageListenerConcurrently {
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
try {
for(MessageExt msg : msgs){
String topic = msg.getTopic();
String msgBody = new String(msg.getBody(),"utf-8");
String tags = msg.getTags();
//if(tags.equals("TagB")) {
System.out.println("收到消息:" + " topic :" + topic + " ,tags : " + tags + " ,msg : " + msgBody);
//}
}
} catch (Exception e) {
e.printStackTrace();
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}

}


8:集群类型

单节点
主从
双主
双主双从
多主多从

主节点消息收发,同步到从节点,主节点挂了,从节点可以保证消息不丢失
投递一条消息后,关闭主节点,故障演练,数据一致性能否保证
从节点可以继续提供消费者继续消费,不能接收消息
主节点重新上线后进行消费进度的offset同步

9:
rocketmq服务关闭

关闭namesrv服务:sh bin/mqshutdown namesrv

关闭broker服务 :sh bin/mqshutdown broker

//执行 jps 查看进程
> jps
25913 NamesrvStartup
10:rocketmq生产者核心参数讲解
producerGroup*
配置说明:生产组的名称,一类Producer的标识 一个应用必须是唯一的
createTopicKey
配置说明:发送消息的时候,如果没有找到topic,若想自动创建该topic,需要一个key topic,这个值即是key topic的值
defaultTopicQueueNums
配置说明:自动创建topic的话,默认queue数量是多少
默认值:4

sendMsgTimeout
配置说明:默认的发送超时时间 3000
默认值:单位毫秒
若发送的时候不显示指定timeout,则使用此设置的值作为超时时间。
对于异步发送,超时后会进入回调的onException,对于同步发送,超时则会得到一个RemotingTimeoutException。
compressMsgBodyOverHowmuch
配置说明:消息body需要压缩的阈值
默认值:1024 * 4,4K
retryTimesWhenSendFailed
配置说明:同步发送失败的话,rocketmq内部重试多少次,默认值:2
retryTimesWhenSendAsyncFailed
配置说明:异步发送失败的话,rocketmq内部重试多少次,默认值:2

maxMessageSize
配置说明:客户端验证,允许发送的最大消息体大小
默认值:1024 * 1024 * 4,4M
若消息体大小超过此,会得到一个响应码13(MESSAGE_ILLEGAL)的MQClientException异常
retryAnotherBrokerWhenNotStoreOK
product和consumer在启动的时候会通过nameserver 拉取元数据信息 
所以发送大于配置的消息体的时候其实是在生产端就会被拒绝掉的

配置说明:发送的结果如果不是SEND_OK状态,是否当作失败处理而尝试重发
默认值:false
发送结果总共有4钟:

SEND_OK, //状态成功,无论同步还是存储
FLUSH_DISK_TIMEOUT, // broker刷盘策略为同步刷盘(SYNC_FLUSH)的话时候,等待刷盘的时候超时
FLUSH_SLAVE_TIMEOUT, // master role采取同步复制策略(SYNC_MASTER)的时候,消息尝试同步到slave超时
SLAVE_NOT_AVAILABLE, //slave不可用
注:从源码上看,此配置项只对同步发送有效,异步、oneway(由于无法获取结果,肯定无效)均无效

11:master slave 主从同步机制
主要同步的是元数据和消息两大实体
元数据采用的是定时任务同步底层采用netty技术实现
消息commitlog主要是采用实时同步底层采用的时候socket

12:延迟消息

 

 
RocketMQ 支持发送延迟消息,但不支持任意时间的延迟消息的设置,
仅支持内置预设值的延迟时间间隔的延迟消息。
预设值的延迟时间间隔为:1s、 5s、 10s、 30s、 1m、 2m、 3m、 4m、 5m、 6m、 7m、 8m、 9m、 10m、 20m、 30m、 1h、 2h
在消息创建的时候,调用 setDelayTimeLevel(int level) 方法设置延迟时间。
broker在接收到延迟消息的时候会把对应延迟级别的消息先存储到对应的延迟队列中,
等延迟消息时间到达时,会把消息重新存储到对应的topic的queue里面。

(1)延迟消息正常提交给 CommitLog 保存

(2)因为是延迟消息,所以变更为延时队列指定的 Topic 和 queueId,这样就转换为 ConsumerQueue(Scheduler),从而不会像 ConsumerQueue(Normal)被正常消费

(3)延时队列调度器,轮询查看相应的队列中消息是否到了要执行的时间

(4)到了执行时间的消息,恢复原来消息的 topic 和 queueId,发给 broker 就变为 ConsumerQueue(nornal)。这样就能正常消费了

使用了 Level 的方式,不同时间放进不同 queue,这样就避免了排序问题,成为了一个 O(1) 的队列插入

Message msg = new Message("DelayTopicTest","TagA",("Hello RocketMQ ").getBytes(RemotingHelper.DEFAULT_CHARSET) );
//设置延迟级别,注意这里的3不是代表延迟3s
msg.setDelayTimeLevel(3);
SendResult sendResult = producer.send(msg);



13:发送给指定的队列

否则生产端轮流消息发送给队列

SendResult sr = producer.send(message, new MessageQueueSelector() {

@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Integer queueNumber = (Integer)arg;
return mqs.get(queueNumber);
}
}, 2);
System.err.println(sr);
14:consumer核心参数配置

consumeFromWhere*
配置说明:启动消费点策略
默认值:ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET
可选值有三个:
CONSUME_FROM_LAST_OFFSET //队列尾消费
CONSUME_FROM_FIRST_OFFSET //队列头消费
CONSUME_FROM_TIMESTAMP //按照日期选择某个位置消费
注:此策略只生效于新在线测consumer group,如果是老的已存在的consumer group,
broker已经上报过了 offset
都降按照已经持久化的consume offset进行消费

后续启动就会从broker端记录的offerset 消费  因为以前的会上报到namespace



consumeTimestamp:
配置说明:CONSUME_FROM_LAST_OFFSET的时候使用,从哪个时间点开始消费
默认值:半小时前
格式为yyyyMMddhhmmss 如 20131223171201

allocateMessageQueueStrategy*
配置说明:负载均衡策略算法
默认值:AllocateMessageQueueAveragely(取模平均分配)
这个算法可以自行扩展以使用自定义的算法,目前内置的有以下算法可以使用
AllocateMessageQueueAveragely //取模平均
AllocateMessageQueueAveragelyByCircle //环形平均
AllocateMessageQueueByConfig // 按照配置,传入听死的messageQueueList
AllocateMessageQueueByMachineRoom //按机房,从源码上看,必须和阿里的某些broker命名一致才行
AllocateMessageQueueConsistentHash //一致性哈希算法,本人于4.1提交的特性。用于解决“惊群效应”。
需要自行扩展的算法的,需要实现org.apache.rocketmq.client.consumer.rebalance.AllocateMessageQueueStrategy

subscription
配置说明:订阅关系(topic->sub expression)

messageListener
配置说明:消息处理监听器(回调)
默认值:null
不建议设置,注册监听的时候应调用registerMessageListener


offsetStore
配置说明:消息消费进度存储器
默认值:null
不建议设置,offsetStore 有两个策略:LocalFileOffsetStore 和 RemoteBrokerOffsetStore。
若没有显示设置的情况下,广播模式将使用LocalFileOffsetStore,集群模式将使用RemoteBrokerOffsetStore,不建议修改。


consumeThreadMin*
配置说明:消费线程池的core size 拉取消息的线程数量


默认值:20
PushConsumer会内置一个消费线程池,这个配置控制此线程池的core size
consumeThreadMax*
配置说明:消费线程池的max size
默认值:64
PushConsumer会内置一个消费线程池,这个配置控制此线程池的max size

messageModel*
配置说明:消费模式
默认值:MessageModel.CLUSTERING
可选值有两个:
CLUSTERING //集群消费模式
BROADCASTING //广播消费模式


consumeConcurrentlyMaxSpan
配置说明:并发消费下,单条consume queue队列允许的最大offset跨度,达到则触发流控
默认值:2000
注:只对并发消费(ConsumeMessageConcurrentlyService)生效

每次发起pull请求到broker,客户端需要指定一个最大batch size,表示这次拉取消息最多批量拉取多少条。
consumeMessageBatchMaxSize
配置说明:批量消费的最大消息条数
默认值:1

consumeTimeout
配置说明:消费的最长超时时间
默认值:15,单位分钟
如果消费超时,RocketMQ会等同于消费失败来处理
consumerGroup*
配置说明:消费组的名称,用于标识一类消费者,
用于把多个consumer组织到一起,消费某一类型的消费达到天然的负载均衡


15:
RocketMQ有两种消费模式
RocketMQ有两种消费模式:BROADCASTING广播模式,CLUSTERING集群模式,默认的是 集群消费模式。
广播消费指的是:一条消息被多个consumer消费,即使这些consumer属于同一个ConsumerGroup,消息也会被ConsumerGroup中的每个Consumer都消费一次
集群消费模式:一个ConsumerGroup中的Consumer实例平均分摊消费消息。例如某个Topic有9条消息,
其中一个ConsumerGroup有3个实例(可能是3个进程,或者3台机器),那么每个实例只消费其中部分,消费完的消息不能被其他实例消费。
16:
offset是消息消费进度的核心,指的是某个topic下的某一条消息在messagequeque中的位置
通过offset可以定位到这条消息
offset的存储可以分为远程文件存储和本地文件存储
默认的集群模式采用远程文件存储 本质上是多个consumer消费某一个主题,这种情况需要broker控制offset,
使用rmotebrokeroffsetstore
广播模式采用本地文件存储,每个consumer相互独立没任何干扰,所以可以把offset存储在本地



17:MQ中Pull和Push的两种消费方式
对于任何一款消息中间件而言,消费者客户端一般有两种方式从消息中间件获取消息并消费:
(1)Push方式:由消息中间件(MQ消息服务器代理)主动地将消息推送给消费者;采用Push方式,
可以尽可能实时地将消息发送给消费者进行消费。但是,在消费者的处理消息的能力较弱的时候(比如,消费者端的业务系统处理一条消息的流程比较复杂,
其中的调用链路比较多导致消费时间比较久。概括起来地说就是“慢消费问题”),
而MQ不断地向消费者Push消息,消费者端的缓冲区可能会溢出,导致异常;
(2)Pull方式:由消费者客户端主动向消息中间件(MQ消息服务器代理)拉取消息;采用Pull方式,
如何设置Pull消息的频率需要重点去考虑,举个例子来说,可能1分钟内连续来了1000条消息,然后2小时内没有新消息产生(概括起来说就是“消息延迟与忙等待”)。
如果每次Pull的时间间隔比较久,会增加消息的延迟,即消息到达消费者的时间加长,MQ中消息的堆积量变大;若每次Pull的时间间隔较短,但是在一段时间内MQ中并没有任何消息可以消费,那么会产生很多无效的Pull请求的RPC开销,影响MQ整体的网络性能;
1.2 RocketMQ消息消费的长轮询机制

思考题:
上面简要说明了Push和Pull两种消息消费方式的概念和各自特点。如果长时间没有消息,而消费者端又不停的发送Pull请求不就会导致RocketMQ中Broker端负载很高吗?那么在RocketMQ中如何解决以做到高效的消息消费呢?
通过研究源码可知,RocketMQ的消费方式都是基于拉模式拉取消息的,而在这其中有一种长轮询机制(对普通轮询的一种优化),来平衡上面Push/Pull模型的各自缺点。基本设计思路是:消费者如果第一次尝试Pull消息失败(比如:Broker端没有可以消费的消息),
并不立即给消费者客户端返回Response的响应,而是先hold住并且挂起请求(将请求保存至pullRequestTable本地缓存变量中),然后Broker端的后台独立线程—PullRequestHoldService会从pullRequestTable本地缓存变量中不断地去取,
具体的做法是查询待拉取消息的偏移量是否小于消费队列最大偏移量,如果条件成立则说明有新消息达到Broker端(这里,在RocketMQ的Broker端会有一个后台独立线程—ReputMessageService不停地构建ConsumeQueue/IndexFile数据,同时取出hold住的请求并进行二次处理),则通过重新调用一次业务处理器—PullMessageProcessor的处理请求方法—processRequest()来重新尝试拉取消息(此处,每隔5S重试一次,默认长轮询整体的时间设置为30s)。
RocketMQ消息Pull的长轮询机制的关键在于Broker端的PullRequestHoldService和ReputMessageService两个后台线程。对于RocketMQ的长轮询(LongPolling)消费模式后面会专门详细介绍。

DefaultPublishConsumer 就是长轮询机制

DefaultPullConsumer 消息拉取 需要自己维护offset 很少使用


18:消息存储
RocketMQ的消息存储是由consume queue和commit log配合完成的。
1、Consume Queue
consume queue是消息的逻辑队列,相当于字典的目录,用来指定消息在物理文件commit log上的位置。
我们可以在配置中指定consumequeue与commitlog存储的目录
每个topic下的每个queue都有一个对应的consumequeue文件,比如:
${rocketmq.home}/store/consumequeue/${topicName}/${queueId}/${fileName}
1.根据topic和queueId来组织文件,图中TopicA有两个队列0,1,那么TopicA和QueueId=0组成一个ConsumeQueue,TopicA和QueueId=1组成另一个ConsumeQueue。
Consume Queue中存储单元是一个20字节定长的二进制数据,顺序写顺序读
consumequeue文件存储单元格式
CommitLog Offset是指这条消息在Commit Log文件中的实际偏移量
Size存储中消息的大小
Message Tag HashCode存储消息的Tag的哈希值:主要用于订阅时消息过滤(订阅时如果指定了Tag,会根据HashCode来快速查找到订阅的消息)

2、Commit Log
CommitLog:消息存放的物理文件,每台broker上的commitlog被本机所有的queue共享,不做任何区分。
文件的默认位置如下,仍然可通过配置文件修改:
${user.home} \store\${commitlog}\${fileName}
CommitLog的消息存储单元长度不固定,文件顺序写,随机读。消息的存储结构如下表所示,按照编号顺序以及编号对应的内容依次存储。

3、消息的索引文件
如果一个消息包含key值的话,会使用IndexFile存储消息索引,文件的内容结构如图:
消息索引
索引文件主要用于根据key来查询消息的

 



19:异步复制,同步双写
异步复制和同步双写主要是主和从的关系。消息需要实时消费的,就需要采用主从模式部署
异步复制:比如这里有一主一从,我们发送一条消息到主节点之后,这样消息就算从producer端发送成功了,然后通过异步复制的方法将数据复制到从节点
同步双写:比如这里有一主一从,我们发送一条消息到主节点之后,这样消息就并不算从producer端发送成功了,需要通过同步双写的方法将数据同步到从节点后, 才算数据发送成功。


同步刷盘:在消息到达MQ后,RocketMQ需要将数据持久化,同步刷盘是指数据到达内存之后,调用写线程写进磁盘, 必须刷到commitlog日志之后才算成功,然后返回producer数据已经发送成功。
异步刷盘:,同步刷盘是指数据到达内存之后,返回producer说数据已经发送成功。,然后再有操作系统调用写入commitlog日志。

实际建议同步双写 异步刷盘

commitlog
commitlog就是来存储所有的元信息,包含消息体,类似于Mysql、Oracle的redolog,所以主要有CommitLog在,Consume Queue即使数据丢失,仍然可以恢复出来。
consumequeue:记录数据的位置,以便Consume快速通过consumequeue找到commitlog中的数据
20:RocketMQ 高可用机制

master slave 配合,master 支持读、写,slave 只读,producer 只能和 master 连接写入消息,consumer 可以连接 master 和 slave。
consumer 高可用
当 master 不可用或者繁忙时,consumer 会被自动切换到 slave 读。所以,即使 master 出现故障,consumer 仍然可以从 slave 读消息,不受影响。
producer 高可用
创建 topic 时,把 message queue 创建在多个 broker 组上(brokerName 一样,brokerId 不同),当一个 broker 组的 master 不可用后,其他组的 master 仍然可以用,producer 可以继续发消息。
21:namesrv存在意义
  • NameServer:在系统中是做命名服务,更新和发现 broker服务。
是整个集群的状态服务器 nameserver部署相互独立 zookpeer太笨重了 所以才使用nameserver来管理状态

在RocketMQ网络部署图中,broker相当于服务端,而Producer、Consumer都是相当于其客户端,如果broker固定死永远不变,那么namesrv存在就没有任何一样的,但是由于服务端自动伸缩、故障以及升级等,服务端会变动,因此namesrv就有存在的意义了。

下面简单说明:

因此需要一个类似namesrv的东西存在,一般存在两种机制:客户端发现机制服务端发现机制。

客户端发现机制

当发出请求服务时,客户端通过注册中心服务知道所有的服务实例。客户端接着使用负载均衡算法选择可用的服务实例中的一个并进行发送

服务端发现机制

发出请求服务时,客户端通过请求负载平衡器,负载均衡器通过注册中心服务知道所有的服务实例。负载均衡器接着使用负载均衡算法选择可用的服务实例中的一个并进行发送

备注: Nginx HTTP服务器和反向代理服务器就是这种。

两种机制总结

客户端发现机制:客户端有所有可用的服务实例,可以灵活方便的特定应用进行特定的负载均衡决策。

服务端发现机制:客户端只需要给负载均衡器发请求即可,客户端屏蔽掉了一些细节。


22:双十一抗压
前端dns解析,软硬负载均衡设施进行分流,限流
lvs nginx haproxy负载均衡
openresty  防刷限流

限制ip调用接口总数量

限制ip漏桶算法

限制ip令牌桶算法


缓存按照业务维度拆分
微服务流控
Guava RateLimiter,jdk Semaphore ,Netflix Hystrix
微服务熔断 降级 兜底
微服务接口的幂等性保证 redis分布式锁 主表唯一id  redis 判断是否存在key


数据库分库分表策略
冷热数据读写分离

23:


24:Dubbo,zookeeper与SpringBoot2.x进行实战整合见代码

 25:hysrtix 降级代码

    超时降级
@HystrixCommand(
commandKey = "createOrder",
commandProperties = {
@HystrixProperty(name="execution.timeout.enabled", value="true"),
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds", value="3000"),
},
fallbackMethod = "createOrderFallbackMethod4Timeout"
)

限流策略:线程池方式
@HystrixCommand(
commandKey = "createOrder",
commandProperties = {
@HystrixProperty(name="execution.isolation.strategy", value="THREAD")
},
threadPoolKey = "createOrderThreadPool",
threadPoolProperties = {
@HystrixProperty(name="coreSize", value="10"),
@HystrixProperty(name="maxQueueSize", value="20000"),
@HystrixProperty(name="queueSizeRejectionThreshold", value="30")
有界队列一旦设置好容量 就不能调整大小了,queueSizeRejectionThreshold 动态设置大小
},
fallbackMethod="createOrderFallbackMethod4Thread"
)

限流策略:信号量方式

@HystrixCommand(
commandKey="createOrder",
commandProperties= {
@HystrixProperty(name="execution.isolation.strategy", value="SEMAPHORE"),
@HystrixProperty(name="execution.isolation.semaphore.maxConcurrentRequests", value="3")
},
fallbackMethod = "createOrderFallbackMethod4semaphore"
)
@RequestMapping("/createOrder")
public String createOrder(@RequestParam("cityId")String cityId,
@RequestParam("platformId")String platformId,
@RequestParam("userId")String userId,
@RequestParam("supplierId")String supplierId,
@RequestParam("goodsId")String goodsId) throws Exception {

return orderService.createOrder(cityId, platformId, userId, supplierId, goodsId) ? "下单成功!" : "下单失败!";
}

public String createOrderFallbackMethod4Timeout(@RequestParam("cityId")String cityId,
@RequestParam("platformId")String platformId,
@RequestParam("userId")String userId,
@RequestParam("suppliedId")String suppliedId,
@RequestParam("goodsId")String goodsId) throws Exception {
System.err.println("-------超时降级策略执行------------");
return "hysrtix timeout !";
}


@Configuration 
public class HystrixConfig {

// 用来拦截处理HystrixCommand注解
@Bean
public HystrixCommandAspect hystrixAspect() {
return new HystrixCommandAspect();
}

// 用来像监控中心Dashboard发送stream信息
@Bean
public ServletRegistrationBean hystrixMetricsStreamServlet() {
ServletRegistrationBean registration = new ServletRegistrationBean(new HystrixMetricsStreamServlet());
registration.addUrlMappings("/hystrix.stream");
return registration;
}
}


26:请求合并

 

27:分布式事务

 


分布式事务执行流程

product并行执行向broker发送消息和执行本地事务,发送到broker的消息为不可见状态
当本地事务执行成功后product会向broker发送一个成功确认消息,此时会把broker的消息更改为可见状态,以供消费者消费
当本地事务执行失败后product会向broker发送一个失败确认消息,此时会把broker的消息更改为失败状态,broker会有个定时任务删除失败的消息
如果其中某个环节出现问题broker没接收到product发送的确认消息,broker会间隔回调一个check回调函数,
以便查看product本地事务执行的结果来再次发送确认消息到broker
28:分布式事务代码
 product端代码

TransactionListenerImpl代码


测试本地事务返回LocalTransactionState.UNKNOW 执行checkLocalTransaction代码

消费端代码

29:FastJsonConvertUtil对象转换json工具类

public class FastJsonConvertUtil {

private static final SerializerFeature[] featuresWithNullValue = { SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullBooleanAsFalse,
SerializerFeature.WriteNullListAsEmpty, SerializerFeature.WriteNullNumberAsZero, SerializerFeature.WriteNullStringAsEmpty };

/**
* <B>方法名称:</B>将JSON字符串转换为实体对象<BR>
* <B>概要说明:</B>将JSON字符串转换为实体对象<BR>
* @param data JSON字符串
* @param clzss 转换对象
* @return T
*/
public static <T> T convertJSONToObject(String data, Class<T> clzss) {
try {
T t = JSON.parseObject(data, clzss);
return t;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

/**
* <B>方法名称:</B>将JSONObject对象转换为实体对象<BR>
* <B>概要说明:</B>将JSONObject对象转换为实体对象<BR>
* @param data JSONObject对象
* @param clzss 转换对象
* @return T
*/
public static <T> T convertJSONToObject(JSONObject data, Class<T> clzss) {
try {
T t = JSONObject.toJavaObject(data, clzss);
return t;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

/**
* <B>方法名称:</B>将JSON字符串数组转为List集合对象<BR>
* <B>概要说明:</B>将JSON字符串数组转为List集合对象<BR>
* @param data JSON字符串数组
* @param clzss 转换对象
* @return List<T>集合对象
*/
public static <T> List<T> convertJSONToArray(String data, Class<T> clzss) {
try {
List<T> t = JSON.parseArray(data, clzss);
return t;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

/**
* <B>方法名称:</B>将List<JSONObject>转为List集合对象<BR>
* <B>概要说明:</B>将List<JSONObject>转为List集合对象<BR>
* @param data List<JSONObject>
* @param clzss 转换对象
* @return List<T>集合对象
*/
public static <T> List<T> convertJSONToArray(List<JSONObject> data, Class<T> clzss) {
try {
List<T> t = new ArrayList<T>();
for (JSONObject jsonObject : data) {
t.add(convertJSONToObject(jsonObject, clzss));
}
return t;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

/**
* <B>方法名称:</B>将对象转为JSON字符串<BR>
* <B>概要说明:</B>将对象转为JSON字符串<BR>
* @param obj 任意对象
* @return JSON字符串
*/
public static String convertObjectToJSON(Object obj) {
try {
String text = JSON.toJSONString(obj);
return text;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

/**
* <B>方法名称:</B>将对象转为JSONObject对象<BR>
* <B>概要说明:</B>将对象转为JSONObject对象<BR>
* @param obj 任意对象
* @return JSONObject对象
*/
public static JSONObject convertObjectToJSONObject(Object obj){
try {
JSONObject jsonObject = (JSONObject) JSONObject.toJSON(obj);
return jsonObject;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}


/**
* <B>方法名称:</B><BR>
* <B>概要说明:</B><BR>
* @param obj
* @return
*/
public static String convertObjectToJSONWithNullValue(Object obj) {
try {
String text = JSON.toJSONString(obj, featuresWithNullValue);
return text;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}


}
 
30:实战代码
支付生产端代码paya
TransactionProducer  
 
@Component
public class TransactionProducer implements InitializingBean {

private TransactionMQProducer producer;

private ExecutorService executorService;

@Autowired
private TransactionListenerImpl transactionListenerImpl;

private static final String NAMESERVER = "192.168.11.121:9876;192.168.11.122:9876;192.168.11.123:9876;192.168.11.124:9876";

private static final String PRODUCER_GROUP_NAME = "tx_pay_producer_group_name";

private TransactionProducer() {
this.producer = new TransactionMQProducer(PRODUCER_GROUP_NAME);
this.executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName(PRODUCER_GROUP_NAME + "-check-thread");
return thread;
}
});
this.producer.setExecutorService(executorService);
this.producer.setNamesrvAddr(NAMESERVER);
}

@Override
public void afterPropertiesSet() throws Exception {
this.producer.setTransactionListener(transactionListenerImpl);
start();
}

private void start() {
try {
this.producer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
}

public void shutdown() {
this.producer.shutdown();
}

public TransactionSendResult sendMessage(Message message, Object argument) {
TransactionSendResult sendResult = null;
try {
sendResult = this.producer.sendMessageInTransaction(message, argument);
} catch (Exception e) {
e.printStackTrace();
}
return sendResult;
}
}

TransactionListenerImpl

@Component
public class TransactionListenerImpl implements TransactionListener {

@Autowired
private CustomerAccountMapper customerAccountMapper;

@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
System.err.println("执行本地事务单元------------");
CountDownLatch currentCountDown = null;
try {
Map<String, Object> params = (Map<String, Object>) arg;
String userId = (String)params.get("userId");
String accountId = (String)params.get("accountId");
String orderId = (String)params.get("orderId");
BigDecimal payMoney = (BigDecimal)params.get("payMoney"); // 当前的支付款
BigDecimal newBalance = (BigDecimal)params.get("newBalance"); // 前置扣款成功的余额
int currentVersion = (int)params.get("currentVersion");
currentCountDown = (CountDownLatch)params.get("currentCountDown");

//updateBalance 传递当前的支付款 数据库操作:
Date currentTime = new Date();
int count = this.customerAccountMapper.updateBalance(accountId, newBalance, currentVersion, currentTime);
if(count == 1) {
currentCountDown.countDown();
return LocalTransactionState.COMMIT_MESSAGE;
} else {
currentCountDown.countDown();
return LocalTransactionState.ROLLBACK_MESSAGE;
}
} catch (Exception e) {
e.printStackTrace();
currentCountDown.countDown();
return LocalTransactionState.ROLLBACK_MESSAGE;
}

}

@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// TODO Auto-generated method stub
return null;
}

}



支付流程代码
@Service
public class PayServiceImpl implements PayService {

public static final String TX_PAY_TOPIC = "tx_pay_topic";

public static final String TX_PAY_TAGS = "pay";

@Autowired
private CustomerAccountMapper customerAccountMapper;

@Autowired
private TransactionProducer transactionProducer;

@Autowired
private CallbackService callbackService;

@Override
public String payment(String userId, String orderId, String accountId, double money) {
String paymentRet = "";
try {
// 最开始有一步 token验证操作(重复提单问题)
BigDecimal payMoney = new BigDecimal(money);
//加锁开始(获取)
CustomerAccount old = customerAccountMapper.selectByPrimaryKey(accountId);
BigDecimal currentBalance = old.getCurrentBalance();
int currentVersion = old.getVersion();
// 要对大概率事件进行提前预判(小概率事件我们做放过,但是最后保障数据的一致性即可)
//业务出发:
//当前一个用户账户 只允许一个线程(一个应用端访问)
//技术出发:
//1 redis去重 分布式锁
//2 数据库乐观锁去重
// 做扣款操作的时候:获得分布式锁,看一下能否获得
BigDecimal newBalance = currentBalance.subtract(payMoney);
//加锁结束(释放)
if(newBalance.doubleValue() > 0 ) { // 或者一种情况获取锁失败
// 1.组装消息
// 1.执行本地事务
String keys = UUID.randomUUID().toString() + "$" + System.currentTimeMillis();
Map<String, Object> params = new HashMap<>();
params.put("userId", userId);
params.put("orderId", orderId);
params.put("accountId", accountId);
params.put("money", money); //100

Message message = new Message(TX_PAY_TOPIC, TX_PAY_TAGS, keys, FastJsonConvertUtil.convertObjectToJSON(params).getBytes());
// 可能需要用到的参数
params.put("payMoney", payMoney);
params.put("newBalance", newBalance);
params.put("currentVersion", currentVersion);

// 同步阻塞
CountDownLatch countDownLatch = new CountDownLatch(1);
params.put("currentCountDown", countDownLatch);
// 消息发送并且 本地的事务执行
TransactionSendResult sendResult = transactionProducer.sendMessage(message, params);

countDownLatch.await();

if(sendResult.getSendStatus() == SendStatus.SEND_OK
&& sendResult.getLocalTransactionState() == LocalTransactionState.COMMIT_MESSAGE) {
// 回调order通知支付成功消息
callbackService.sendOKMessage(orderId, userId);
paymentRet = "支付成功!";
} else {
paymentRet = "支付失败!";
}
} else {
paymentRet = "余额不足!";
}
} catch (Exception e) {
e.printStackTrace();
paymentRet = "支付失败!";
}
return paymentRet;
}
}


支付消费端代码payb
@Component
public class PayConsumer {

private DefaultMQPushConsumer consumer;

private static final String NAMESERVER = "192.168.11.121:9876;192.168.11.122:9876;192.168.11.123:9876;192.168.11.124:9876";

private static final String CONSUMER_GROUP_NAME = "tx_pay_consumer_group_name";

public static final String TX_PAY_TOPIC = "tx_pay_topic";

public static final String TX_PAY_TAGS = "pay";

@Autowired
private PlatformAccountMapper platformAccountMapper;

private PayConsumer() {
try {
this.consumer = new DefaultMQPushConsumer(CONSUMER_GROUP_NAME);
this.consumer.setConsumeThreadMin(10);
this.consumer.setConsumeThreadMax(30);
this.consumer.setNamesrvAddr(NAMESERVER);
this.consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
this.consumer.subscribe(TX_PAY_TOPIC, TX_PAY_TAGS);
this.consumer.registerMessageListener(new MessageListenerConcurrently4Pay());
this.consumer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
}

class MessageListenerConcurrently4Pay implements MessageListenerConcurrently {

@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
MessageExt msg = msgs.get(0);
try {
String topic = msg.getTopic();
String tags = msg.getTags();
String keys = msg.getKeys();
String body = new String(msg.getBody(), RemotingHelper.DEFAULT_CHARSET);
System.err.println("收到事务消息, topic: " + topic + ", tags: " + tags + ", keys: " + keys + ", body: " + body);

// 消息一单过来的时候(去重 幂等操作)
// 数据库主键去重<去重表 keys>
// insert table --> insert ok & primary key
Map<String, Object> paramsBody = FastJsonConvertUtil.convertJSONToObject(body, Map.class);
String userId = (String)paramsBody.get("userId"); // customer userId
String accountId = (String)paramsBody.get("accountId"); //customer accountId
String orderId = (String)paramsBody.get("orderId"); // 统一的订单
BigDecimal money = (BigDecimal)paramsBody.get("money"); // 当前的收益款

PlatformAccount pa = platformAccountMapper.selectByPrimaryKey("platform001"); // 当前平台的一个账号
pa.setCurrentBalance(pa.getCurrentBalance().add(money));
Date currentTime = new Date();
pa.setVersion(pa.getVersion() + 1);
pa.setDateTime(currentTime);
pa.setUpdateTime(currentTime);
platformAccountMapper.updateByPrimaryKeySelective(pa);
} catch (Exception e) {
e.printStackTrace();
//msg.getReconsumeTimes();
// 如果处理多次操作还是失败, 记录失败日志(做补偿 回顾 人工处理)
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}

}






当支付成功 回调订单修改订单状态代码

paya代码(所属)
@Service
public class CallbackService {

public static final String CALLBACK_PAY_TOPIC = "callback_pay_topic";

public static final String CALLBACK_PAY_TAGS = "callback_pay";

public static final String NAMESERVER = "192.168.11.121:9876;192.168.11.122:9876;192.168.11.123:9876;192.168.11.124:9876";

@Autowired
private SyncProducer syncProducer;

public void sendOKMessage(String orderId, String userId) {

Map<String, Object> params = new HashMap<>();
params.put("userId", userId);
params.put("orderId", orderId);
params.put("status", "2"); //ok

String keys = UUID.randomUUID().toString() + "$" + System.currentTimeMillis();
Message message = new Message(CALLBACK_PAY_TOPIC, CALLBACK_PAY_TAGS, keys, FastJsonConvertUtil.convertObjectToJSON(params).getBytes());

SendResult ret = syncProducer.sendMessage(message);
}



}
发送修改订单状态消息的代码
paya代码(所属)
@Component
public class SyncProducer {

private DefaultMQProducer producer;

private static final String NAMESERVER = "192.168.11.121:9876;192.168.11.122:9876;192.168.11.123:9876;192.168.11.124:9876";

private static final String PRODUCER_GROUP_NAME = "callback_pay_producer_group_name";

private SyncProducer() {
this.producer = new DefaultMQProducer(PRODUCER_GROUP_NAME);
this.producer.setNamesrvAddr(NAMESERVER);
this.producer.setRetryTimesWhenSendFailed(3);
start();
}

public void start() {
try {
this.producer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
}

public SendResult sendMessage(Message message) {
SendResult sendResult = null;
try {
sendResult = this.producer.send(message);
} catch (MQClientException e) {
e.printStackTrace();
} catch (RemotingException e) {
e.printStackTrace();
} catch (MQBrokerException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return sendResult;
}

public void shutdown() {
this.producer.shutdown();
}


}

消费端修改订单状态的代码
order端代码
@Component
public class OrderConsumer {

private DefaultMQPushConsumer consumer;

public static final String CALLBACK_PAY_TOPIC = "callback_pay_topic";

public static final String CALLBACK_PAY_TAGS = "callback_pay";

public static final String NAMESERVER = "192.168.11.121:9876;192.168.11.122:9876;192.168.11.123:9876;192.168.11.124:9876";

@Autowired
private OrderMapper orderMapper;

@Autowired
private OrderService orderService;

public OrderConsumer() throws MQClientException {
consumer = new DefaultMQPushConsumer("callback_pay_consumer_group");
consumer.setConsumeThreadMin(10);
consumer.setConsumeThreadMax(50);
consumer.setNamesrvAddr(NAMESERVER);
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
consumer.subscribe(CALLBACK_PAY_TOPIC, CALLBACK_PAY_TAGS);
consumer.registerMessageListener(new MessageListenerConcurrently4Pay());
consumer.start();
}

class MessageListenerConcurrently4Pay implements MessageListenerConcurrently {

@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
MessageExt msg = msgs.get(0);
try {
String topic = msg.getTopic();
String msgBody = new String(msg.getBody(), "utf-8");
String tags = msg.getTags();
String keys = msg.getKeys();
System.err.println("收到消息:" + " topic :" + topic + " ,tags : " + tags + "keys :" + keys + ", msg : " + msgBody);
String orignMsgId = msg.getProperties().get(MessageConst.PROPERTY_ORIGIN_MESSAGE_ID);
System.err.println("orignMsgId: " + orignMsgId);

//通过keys 进行去重表去重 或者使用redis进行去重???? --> 不需要

Map<String, Object> body = FastJsonConvertUtil.convertJSONToObject(msgBody, Map.class);
String orderId = (String) body.get("orderId");
String userId = (String) body.get("userId");
String status = (String)body.get("status");

Date currentTime = new Date();

if(status.equals(OrderStatus.ORDER_PAYED.getValue())) {
int count = orderMapper.updateOrderStatus(orderId, status, "admin", currentTime);
if(count == 1) {
orderService.sendOrderlyMessage4Pkg(userId, orderId);
}

}

} catch (Exception e) {
e.printStackTrace();
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}

}

}


31:顺序消息
顺序消息是指消息的消费顺序和消费的生产顺序相同
全局顺序:在某个topic下的所有消息都保持顺序
局部顺序:只保证某一个队列queue是按顺序的即可,这样可以有多个队列queue同时消费提高并发

顺序消费场景
在网购的时候,我们需要下单,那么下单需要假如有三个顺序,第一、创建订单 ,第二:订单付款,第三:订单完成。也就是这个三个环节要有顺序,这个订单才有意义。RocketMQ可以保证顺序消费。
rocketMq实现顺序消费的原理
produce在发送消息的时候,把消息发到同一个队列(queue)中,消费者注册消息监听器为MessageListenerOrderly,这样就可以保证消费端只有一个线程去消费消息
注意:是把把消息发到同一个队列(queue),不是同一个topic,默认情况下一个topic包括4个queue

顺序消费代码
public static final String PKG_TOPIC = "pkg_topic";

public static final String PKG_TAGS = "pkg";
@Override
public void sendOrderlyMessage4Pkg(String userId, String orderId) {
List<Message> messageList = new ArrayList<>();

Map<String, Object> param1 = new HashMap<String, Object>();
param1.put("userId", userId);
param1.put("orderId", orderId);
param1.put("text", "创建包裹操作---1");

String key1 = UUID.randomUUID().toString() + "$" +System.currentTimeMillis();
Message message1 = new Message(PKG_TOPIC, PKG_TAGS, key1, FastJsonConvertUtil.convertObjectToJSON(param1).getBytes());

messageList.add(message1);


Map<String, Object> param2 = new HashMap<String, Object>();
param2.put("userId", userId);
param2.put("orderId", orderId);
param2.put("text", "发送物流通知操作---2");

String key2 = UUID.randomUUID().toString() + "$" +System.currentTimeMillis();
Message message2 = new Message(PKG_TOPIC, PKG_TAGS, key2, FastJsonConvertUtil.convertObjectToJSON(param2).getBytes());

messageList.add(message2);

// 顺序消息投递 是应该按照 供应商ID 与topic 和 messagequeueId 进行绑定对应的
// supplier_id

Order order = orderMapper.selectByPrimaryKey(orderId);
int messageQueueNumber = Integer.parseInt(order.getSupplierId());

//对应的顺序消息的生产者 把messageList 发出去
orderlyProducer.sendOrderlyMessages(messageList, messageQueueNumber);
}

生产端代码


@Component
public class OrderlyProducer {

private DefaultMQProducer producer;

public static final String NAMESERVER = "192.168.11.121:9876;192.168.11.122:9876;192.168.11.123:9876;192.168.11.124:9876";

public static final String PRODUCER_GROUP_NAME = "orderly_producer_group_name";

private OrderlyProducer() {
this.producer = new DefaultMQProducer(PRODUCER_GROUP_NAME);
this.producer.setNamesrvAddr(NAMESERVER);
this.producer.setSendMsgTimeout(3000);
start();
}

public void start() {
try {
this.producer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
}

public void shutdown() {
this.producer.shutdown();
}

public void sendOrderlyMessages(List<Message> messageList, int messageQueueNumber) {
for(Message me : messageList) {
try {
this.producer.send(me, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Integer id = (Integer)arg;
return mqs.get(id);
}
}, messageQueueNumber);
} catch (MQClientException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (RemotingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (MQBrokerException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

}
消费端代码

@Component
public class PkgOrderlyConsumer {

private DefaultMQPushConsumer consumer;

public static final String PKG_TOPIC = "pkg_topic";

public static final String PKG_TAGS = "pkg";

public static final String NAMESERVER = "192.168.11.121:9876;192.168.11.122:9876;192.168.11.123:9876;192.168.11.124:9876";

public static final String CONSUMER_GROUP_NAME = "orderly_consumer_group_name";


private PkgOrderlyConsumer() throws MQClientException {
this.consumer = new DefaultMQPushConsumer(CONSUMER_GROUP_NAME);
this.consumer.setConsumeThreadMin(10);
this.consumer.setConsumeThreadMin(30);
this.consumer.setNamesrvAddr(NAMESERVER);
this.consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
this.consumer.subscribe(PKG_TOPIC, PKG_TAGS);
this.consumer.setMessageListener(new PkgOrderlyListener());
this.consumer.start();
}

class PkgOrderlyListener implements MessageListenerOrderly {

Random random = new Random();

@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {

for(MessageExt msg: msgs) {
try {
String topic = msg.getTopic();
String msgBody = new String(msg.getBody(), "utf-8");
String tags = msg.getTags();
String keys = msg.getKeys();
System.err.println("收到消息:" + " topic :" + topic + " ,tags : " + tags + "keys :" + keys + ", msg : " + msgBody);

Map<String, Object> body = FastJsonConvertUtil.convertJSONToObject(msgBody, Map.class);
String orderId = (String) body.get("orderId");
String userId = (String) body.get("userId");
String text = (String)body.get("text");

// 模拟实际的业务耗时操作
// PS: 创建包裹信息 、对物流的服务调用(异步调用)
TimeUnit.SECONDS.sleep(random.nextInt(3) + 1);

System.err.println("业务操作: " + text);

} catch (Exception e) {
e.printStackTrace();
return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
}

return ConsumeOrderlyStatus.SUCCESS;
}

}

32:rocketmq消息过滤机制

使用tag进行消息过滤 在broker端过滤
使用sql表达式进行消息过滤
使用filter server进行消息过滤 用cpu资源换取网卡流量在broker端过滤 最新版本不支持 比sql更为灵活

tag 可以帮助我们方便的选择我们想要的消息,例如:

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_EXAMPLE");
consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC");

但 tag 有个限制,一个消息只能设置一个tag,在某些场景下这就很不方便了,这时就可以使用 filter,rocketmq 支持使用 sql 语句的方式来进行消息过滤。

使用sql过滤代码

生产端加入如下代码即可
 Message msg = new Message("FilterTest", "tagA",
                  ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
          
          msg.putUserProperty("a", String.valueOf(i));

          SendResult sendResult = producer.send(msg);
消费端加入如下代码即可
 consumer.subscribe("FilterTest", MessageSelector.bySql("a between 0 and 3"));

broker的配置文件中需要指定对filter的支持,否则报错:

enablePropertyFilter = true
 
33:提高吞吐量和性能的方案
增加机器数量提供多个consumer消费实例或者增加同一个consumer内部线程的并行度
设置批量获取消息进行消费
topic下的队列queue数量应该和消费者数量契合
生产者发送oneway消息
多生产者同时发送消息
34:源码结构



posted @ 2019-06-04 13:55  zhangniuniu  阅读(4217)  评论(0编辑  收藏  举报