1、JMS概述:Java Message Service规范,规定不同Java客户端之间如何进行消息传输
2、JMS消息传输模型:P2P(点对点)和Pub/Sub(发布订阅)
(1)P2P模型
组成:消息队列(Queue)、发送者(Sender)、接收者(Receiver)
特征:每个消息只有一个消费者,一旦被消费,即从消息队列中删除,如果希望每个消息都被成功处理,则应该使用P2P模型
(2)Pub/Sub模型
组成:主题(Topic)、发布者(Publisher)、订阅者(Subscriber)
特征:如果希望发送的消息可以不做任何处理,或者被一个或多个消费者消费,则应该采用Pub/Sub模型
3、Rocketmq架构
Rocketmq使用了Pub/Sub模型设计实现
Rocketmq设计时参考了kafka,消息可靠性比后者更好
4、Rocketmq启动
进入bin目录
cmd运行 mqnamesrv -n 127.0.0.1:9876 启动nameserver服务
cmd运行 mqbroker -n 127.0.0.1:9876 启动broker服务
5、Rocketmq基本概念
(1)Topic:消息逻辑管理单位
一个Producer可以生产多个Topic
一个Consumer可以订阅多个Topic
(2)Tag:用于消息细分,一个Topic下可以有多个Tag
(3)MessageQueue:消息物理管理单位,一个Topic可以对应多个Queue,默认情况下一个Topic分配4个队列,Producer生产的消息随机?发送到队列中
(4)NameServer:相当于ZooKeeper的作用
---- NameServer用来保存活跃的 broker 列表,包括 Master 和 Slave
---- NameServer用来保存所有 topic 和该 topic 所有队列的列表
---- NameServer用来保存所有 broker 的 Filter 列表
(5) Broker:不负责推送消息,只负责存储消息,Consumer主动从broker请求消息
Rocketmq提供了两种存储方式来保留消费记录:一种是保留在consumer所在的服务器上;另一种是保存在broker服务器上。用户还可以自己实现相应的消费进度存储接口。默认情况下,采用集群消费(CLUSTERING),会将记录保存在broker端;而采用广播消费(BROADCASTING)则会将消费记录保存在本地
(6)Message:消息对象模型,由Topic、Tag、Keys、body(byte[])构建
(7)ConsumerGroup:
(8)ProducerGroup:
6、消息过滤
7、消息可靠性
8、消费者消费两种方式
集群消费:同一个ConsumerGroup下有多个消费实例,生产的消息将被随机(轮询)方式发送到各个实例,单个实例只消费总量的一部分,默认方式
广播消费:类似ActiveMQ中的发布订阅模式,每一条消息都会发给Consume Group中的每一个消费者进行消费
8、消息顺序消费
9、消息持久化
10、消息获取方式
DefaultMQPushConsumer
DefaultMQPullConsumer
11、Rocketmq Console
12、代码实例
public static void main(String[] args) throws MQClientException, InterruptedException {
/**
* 一个应用创建一个Producer,由应用来维护此对象,可以设置为全局对象或者单例<br>
* 注意:ProducerGroupName需要由应用来保证唯一<br>
* ProducerGroup这个概念发送普通的消息时,作用不大,但是发送分布式事务消息时,比较关键,
* 因为服务器会回查这个Group下的任意一个Producer
*/
DefaultMQProducer producer = new DefaultMQProducer("producer_test");
//在本地搭建好broker后,记得指定nameServer的地址
producer.setNamesrvAddr("127.0.0.1:9876");
// producer.setRetryTimesWhenSendFailed(3); // 发送消息到broker失败,重试次数,默认是2次
/**
* Producer对象在使用之前必须要调用start初始化,初始化一次即可<br>
* 注意:切记不可以在每次发送消息时,都调用start方法
*/
producer.start();
/**
* 下面这段代码表明一个Producer对象可以发送多个topic,多个tag的消息。
* 注意:send方法是同步调用,只要不抛异常就标识成功。但是发送成功也可会有多种状态,<br>
* 例如消息写入Master成功,但是Slave不成功,这种情况消息属于成功,但是对于个别应用如果对消息可靠性要求极高,<br>
* 需要对这种情况做处理。另外,消息可能会存在发送失败的情况,失败重试由应用来处理。
*/
//用一个short标识生产者停止生产数据
byte [] zero = new byte[]{0,0};
Message endMessage = new Message("EndSignal", zero);
Message msg = new Message("TopicTest1",// topic
"TagA",// tag
"OrderID001",// key
("Hello MetaQ111").getBytes());// body
Message msg2 = new Message("TopicTest2",// topic
"TagB",// tag
"OrderID0034",// key
("Hello MetaQ222").getBytes());// body
Message msg3 = new Message("TopicTest3",// topic
"TagC",// tag
"OrderID0035",// key
("Hello MetaQ333").getBytes());// body
try {
// SendResult sendResult = producer.send(msg, timeout); // 发送消息超时时间配置,默认是3000毫秒
for(int i=0;i<30;i++){
Message msgA = new Message("TopicTest1","TagA","OrderID" + i,("Hello MetaQ"+i).getBytes());
producer.send(msgA);
}
System.out.println("TopicTest1" + "----" + "TagA 发送成功");
SendResult sendResult2 = producer.send(msg2);
System.out.println("TopicTest2" + "----" + "TagB 发送成功");
SendResult sendResult3 = producer.send(msg3);
System.out.println("TopicTest3" + "----" + "TagC 发送成功");
SendResult sendResult_over = producer.send(endMessage);
System.out.println("EndSignal 发送成功");
} catch (Exception e) {
e.printStackTrace();
// 生产者发送消息失败,自动重新发送
Thread.sleep(1000);
}
/**
* 应用退出时,要调用shutdown来清理资源,关闭网络连接,从MetaQ服务器上注销自己
* 注意:我们建议应用在JBOSS、Tomcat等容器的退出钩子里调用shutdown方法
*/
producer.shutdown();
}
(2)使用长连接轮询方式拉取消息
public static void main(String[] args) throws MQClientException {
/**
* 当前例子是PushConsumer用法,使用方式给用户感觉是消息从RocketMQ服务器推到了应用客户端。<br>
* 但是实际PushConsumer内部是使用长轮询Pull方式从Broker拉消息,然后再回调用户Listener方法<br>
*/
/**
* 一个应用创建一个Consumer,由应用来维护此对象,可以设置为全局对象或者单例<br>
* 注意:ConsumerGroupName需要由应用来保证唯一
*/
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_test");
/**
* 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br>
* 如果非第一次启动,那么按照上次消费的位置继续消费
*/
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// 在本地搭建好broker后,指定nameServer地址
consumer.setNamesrvAddr("127.0.0.1:9876");
// 设置消费方式,集群消费还是广播消费,默认是集群消费,即消息随机轮询发送到同一个ConsumerGroup下所有消费者
consumer.setMessageModel(MessageModel.BROADCASTING);
/**
* 订阅指定topic下tags分别等于TagA或TagC或TagD
*/
consumer.subscribe("TopicTest1", "TagA || TagC || TagD");
/**
* 订阅指定topic下所有消息<br>
* 注意:一个consumer对象可以订阅多个topic
*/
consumer.subscribe("TopicTest2", "*");
/**
* 没有这样的写法!!!!!
*/
// consumer.subscribe("*", "*");
consumer.subscribe("EndSignal", "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
/**
* * 默认msgs里只有一条消息,可以通过设置consumeMessageBatchMaxSize参数来批量接收消息
*/
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
for(MessageExt msg : msgs){
// 开始消费
try {
System.out.println(msg);
byte [] body = msg.getBody();
if (body.length == 2 && body[0] == 0 && body[1] == 0) {
System.out.println("消息接收完毕");
continue;
}
if (msg.getTopic().equals("TopicTest1")) {
// 执行TopicTest1的消费逻辑
if (msg.getTags() != null && msg.getTags().equals("TagA")) {
// 执行TagA的消费
} else if (msg.getTags() != null && msg.getTags().equals("TagC")) {
// 执行TagC的消费
} else if (msg.getTags() != null && msg.getTags().equals("TagD")) {
// 执行TagD的消费
}
} else if (msg.getTopic().equals("TopicTest2")) {
// 执行TopicTest2的消费逻辑
}
} catch(Exception e){
// 消费时发生异常
// messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m ......1h 2h
if(msg.getReconsumeTimes() == 3){
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
/**
* Consumer对象在使用之前必须要调用start初始化,初始化一次即可<br>
*/
consumer.start();
System.out.println("consumer start");
}
(3)使用主动pull模式拉取消息
public static void main(String[] args) throws MQClientException {
final DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("consumerPullLiu");
String topic = "TopicTest1";
consumer.setNamesrvAddr("127.0.0.1:9876");
consumer.start();
try {
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues(topic);
System.out.println("mqs.size() " + mqs.size());
// 必须加上此监听才能在消费过后,自动回写消费进度
consumer.registerMessageQueueListener(topic, null);
// 循环每一个队列
for(MessageQueue mq: mqs){
System.out.println("consume message from queue: " + mq + " mqsize = " + mqs.size());
int cnter = 0;
// 每个队列里无限循环,分批拉取未消费的消息,直到拉取不到新消息为止
SINGLE_MQ:while(cnter++ < 100){
long offset = consumer.fetchConsumeOffset(mq, false);
offset = offset < 0 ? 0:offset;
System.out.println("消费进度 offset: " + offset);
PullResult result = consumer.pull(mq, null, offset, 10);
System.out.println("接收到的消息集合 " + result);
switch(result.getPullStatus()){
case FOUND:
if(result.getMsgFoundList() != null){
int prSize = result.getMsgFoundList().size();
System.out.println("pullResult.getMsgFoundList().size()====" + prSize);
if(prSize != 0){
for(MessageExt me : result.getMsgFoundList()) {
// 消费每条消息,如果消费失败,比如更新数据库失败,就重新再拉取一次消息
System.out.println("pullResult.getMsgFoundList()消息体内容===="+ new String(me.getBody()));
}
}
}
// 获取下一个下标位置
offset = result.getNextBeginOffset();
// 消费完后,更新消费进度
consumer.updateConsumeOffset(mq, offset);
break;
case NO_MATCHED_MSG:
System.out.println("没有匹配的消息");
break;
case NO_NEW_MSG:
System.out.println("没有未消费的新消息");
// 拉取不到新消息,跳出SINGLE_MQ当前队列循环,开始下一队列循环
break SINGLE_MQ;
case OFFSET_ILLEGAL:
System.out.println("下标错误");
break;
default:
break;
}
}
}
} catch(Exception ex){
System.out.println(ex.getMessage());
}
// 定义一个定时器,用于测试pull方法时,模拟延时以便自动更新消费进度操作,生产环境中,因consumer一直在运行,因此不需要此步操作。
final Timer timer = new Timer("TimerThread", true);
// 定时器延时30秒后,关闭cousumer,因为客户端从首次启动时在1000*10ms即10秒后,后续每5秒定期执行一次(由参数:persistConsumerOffsetInterval控制)向本机及broker端回写记录消费进度,
// 因此consumer启动后需要延时至少15秒才能执行回写操作,否则下次运行pull方法时,因上次未能及时更新消费进度,程序会重复取出上次消费过的消息重新消费,所以此处延时30秒,留出回写的时间
timer.schedule(new TimerTask() {
@Override
public void run() {
consumer.shutdown();
// 如果只要这个延迟一次,用cancel方法取消掉.
this.cancel();
}
}, 30000);
}