MQ的消费模式可以大致分为两种,一种是 推Push,一种是 拉Pull。
Push模式也是基于Pull模式的,所以不管是Push模式还是Pull模式,都是Pull模式。一般情况下,优先选择Pull模式
同步消息 发送过后会有一个返回值,也就是mq服务器接收到消息后返回的一个确认,这种方式非常安全,但是性能上并没有这么高,而且在mq集群中,也是要等到所有的从机都复制了消息以后才会返回,所以针对重要的消息可以选择这种方式。
可靠的同步传输被广泛应用于各种场景,如重要的通知消息、短消息通知等。
原生依赖引入:
org.apache.rocketmq
rocketmq-client
4.9.0
同步消息生产者:
public class Producer {
public static void main(String[] args) throws Exception {
/*
1. 谁来发?
2. 发给谁?
3. 怎么发?
4. 发什么?
5. 发的结果是什么?
6. 关闭连接
**/
//1.创建一个发送消息的对象Producer,并指定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("sync-producer-group");
//2.设定发送的命名服务器地址
producer.setNamesrvAddr("ip:9876");
producer.setSendMsgTimeout(1000000);
//3.1启动发送的服务
producer.start();
//4.创建要发送的消息对象,指定topic,指定内容body
Message msg = new Message("sync-topic", "hello-rocketmq".getBytes(StandardCharsets.UTF_8));
//3.2发送消息
SendResult result = producer.send(msg);
System.out.println("返回结果:" + result);
//5.关闭连接
producer.shutdown();
}
}
同步消息消费者:
public class Consumer {
public static void main(String[] args) throws Exception {
//1.创建一个接收消息的对象Consumer,并指定消费者组名
//两种模式:①消费者定时拉取模式 ②建立长连接让Broker推送消息(选择第二种)
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("sync-producer-group");
//2.设定接收的命名服务器地址
consumer.setNamesrvAddr("ip:9876");
//3.订阅一个主题,* 表示订阅这个主题的所有消息,后期会有消息过滤
consumer.subscribe("sync-topic","*");
//设置当前消费者的消费模式(默认模式:负载均衡)
consumer.setMessageModel(MessageModel.CLUSTERING);
//3.设置监听器,用于接收消息(一直监听,异步回调,异步线程)
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
//消费消息
//消费上下文:consumeConcurrentlyContext
public ConsumeConcurrentlyStatus consumeMessage(List list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
// 这个就是消费的方法 (业务处理)
System.out.println("我是消费者");
System.out.println(msgs.get(0).toString());
System.out.println("消息内容:" + new String(msgs.get(0).getBody()));
System.out.println("消费上下文:" + context);
//签收消息,消息会从mq出队
//如果返回 RECONSUME_LATER 或 null 或 产生异常 那么消息会重新 回到队列 过一会重新投递出来 ,给当前消费者或者其他消费者消费的
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//4.启动接收消息的服务
consumer.start();
System.out.println("接受消息服务已经开启!");
//5 不要关闭消费者!因为需要监听!
//挂起
System.in.read();
}
}
异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。发送完以后会有一个异步消息通知。
例如,视频上传后通知启动转码服务,转码完成后通知推送转码结果等。
异步消息生产者:
public class AsyncProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("async-producer-group");
producer.setNamesrvAddr("ip:9876");
producer.start();
Message message = new Message("async-topic", "我是一个异步消息".getBytes());
//没有返回值的
producer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("发送成功");
}
@Override
public void onException(Throwable e) {
System.err.println("发送失败:" + e.getMessage());
}
});
System.out.println("我先执行");
//需要接收异步回调,这里需要挂起
System.in.read();
}
}
消费者无特殊变化:
public class SimpleConsumer {
public static void main(String[] args) throws Exception{
// 创建一个消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("async-producer-group");
// 连接namesrv
consumer.setNamesrvAddr("ip:9876");
// 订阅一个主题 * 标识订阅这个主题中所有的消息 后期会有消息过滤
consumer.subscribe("async-topic", "*");
// 设置一个监听器 (一直监听的, 异步回调方式)
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) {
// 这个就是消费的方法 (业务处理)
System.out.println("我是消费者");
System.out.println(msgs.get(0).toString());
System.out.println("消息内容:" + new String(msgs.get(0).getBody()));
System.out.println("消费上下文:" + context);
// 返回值 CONSUME_SUCCESS成功,消息会从mq出队
// RECONSUME_LATER(报错/null) 失败 消息会重新回到队列 过一会重新投递出来 给当前消费者或者其他消费者消费的
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 启动
consumer.start();
// 挂起当前的jvm
System.in.read();
}
}
这种方式主要用在不关心发送结果的场景,这种方式吞吐量很大,但是存在消息丢失的风险,一般用于结果不重要的场景,例如日志信息的发送
单向消息生产者:
public class SingleWayProducer {
public static void main(String[] args) throws Exception{
// 创建默认的生产者
DefaultMQProducer producer = new DefaultMQProducer("single-way-producer-group");
// 设置nameServer地址
producer.setNamesrvAddr("ip:9876");
// 启动实例
producer.start();
Message msg = new Message("single-way-topic", ("单向消息").getBytes());
// 发送单向消息
producer.sendOneway(msg);
// 关闭实例
producer.shutdown();
}
}
产生日志的服务利用MQ发送单向消息,不用等回复,大大减少了发送日志的时间,由log-service统一写入日志表中。并且由于日志过于庞大,可以对日志进行冷热分离,近一个月的为热数据,近一年的为冷数据(实际情况据业务而定),存储的位置不同,时间过于久远的日志可以删掉
消息放入MQ后,过一段时间,才会被监听到,然后消费
比如下订单业务,提交了一个订单就可以发送一个延时消息,15min后去检查这个订单的状态,如果还是未付款就取消订单释放库存(订单超时)。
在分布式定时调度触发、任务超时处理等场景,使用 RocketMQ 的延时消息可以简化定时调度任务的开发逻辑,实现高性能、可扩展、高可靠的定时触发能力。
延迟消息生产者:
public class DelayProducer {
public static void main(String[] args) throws Exception{
// 创建默认的生产者
DefaultMQProducer producer = new DefaultMQProducer("delay-producer-group");
// 设置nameServer地址
producer.setNamesrvAddr("ip:9876");
// 启动实例
producer.start();
Message msg = new Message("delay-topic", ("延迟消息").getBytes());
// 给这个消息设定一个延迟等级
// messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
msg.setDelayTimeLevel(3);
// 发送单向消息
producer.send(msg);
// 打印时间
System.out.println(new Date());
// 关闭实例
producer.shutdown();
}
}
延迟消息消费者(无特殊变化):
public class MSConsumer {
public static void main(String[] args) throws Exception{
// 创建一个消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("delay-producer-group");
// 连接namesrv
consumer.setNamesrvAddr("ip:9876");
// 订阅一个主题 * 标识订阅这个主题中所有的消息 后期会有消息过滤
consumer.subscribe("delay-topic", "*");
// 设置一个监听器 (一直监听的, 异步回调方式)
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) {
System.out.println(msgs.get(0).toString());
System.out.println("消息内容:" + new String(msgs.get(0).getBody()));
System.out.println("收到时间:"+new Date());
// 返回值 CONSUME_SUCCESS成功,消息会从mq出队
// RECONSUME_LATER(报错/null) 失败 消息会重新回到队列 过一会重新投递出来 给当前消费者或者其他消费者消费的
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 启动
consumer.start();
// 挂起当前的jvm
System.in.read();
}
}
可以通过打印一下时间差来检测一下(第一次有误差很正常)
Rocketmq可以一次性发送一组消息,那么这一组消息会被当做一个消息消费。
在对吞吐率有一定要求的情况下,可以将一些消息聚成一批以后进行发送,可以增加吞吐率,并减少API和网络调用次数。
将消息打包成 Collection
批量消息生产者:
public class BatchProducer {
public static void main(String[] args) throws Exception{
// 创建默认的生产者
DefaultMQProducer producer = new DefaultMQProducer("batch-producer-group");
// 设置nameServer地址
producer.setNamesrvAddr("ip:9876");
// 启动实例
producer.start();
List msgs = Arrays.asList(
//需要是同一种主题
new Message("batch-topic", "我是一组消息的A消息".getBytes()),
new Message("batch-topic", "我是一组消息的B消息".getBytes()),
new Message("batch-topic", "我是一组消息的C消息".getBytes())
);
SendResult send = producer.send(msgs);
System.out.println(send);
// 关闭实例
producer.shutdown();
}
}
通过查看面板我们可以发现,批量消息全部放到了一个队列
批量消息消费者(无特殊变化):
public class BatchConsumer {
public static void main(String[] args) throws Exception{
// 创建默认消费者组
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("batch-producer-group");
// 设置nameServer地址
consumer.setNamesrvAddr("ip:9876");
// 订阅一个主题来消费 表达式,默认是*
consumer.subscribe("batch-topic", "*");
// 注册一个消费监听 MessageListenerConcurrently是并发消费
// 默认是20个线程一起消费,可以参看 consumer.setConsumeThreadMax()
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) {
// 这里执行消费的代码 默认是多线程消费
System.out.println("msgs.size():"+msgs.size());
System.out.println(Thread.currentThread().getName() + "----" + new String(msgs.get(0).getBody()));
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.in.read();
}
}
另外 consumeMessage 方法里的 List
打印结果如下:
在默认的情况下普通消息的发送会采取 Round Robin 轮询方式 把 消息 发送到不同的queue(分区队列);而消费消息的时候从多个queue上拉取消息,这未满足 生产顺序性 和 消费顺序性。比方说一个订单的生成、付款和发货,这三个操作需要被顺序执行,但如果是普通消息,订单A的消息可能会被轮询发送到不同的队列中,不同队列的消息将无法保持顺序。
消息有序指的是可以按照消息的发送顺序来消费(FIFO),消息有序可以分为:分区有序 或者 全局有序。
程序模拟,封装实体类MsgModel:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MsgModel {
private String orderSn;
private Integer userId;
private String desc; // 下单 付款 发货
}
顺序消息生产者:
public class OrderProducer {
private static final List msgModels = Arrays.asList(
new MsgModel("qwer", 1, "下单"),
new MsgModel("qwer", 1, "付款"),
new MsgModel("qwer", 1, "发货"),
new MsgModel("zxcv", 2, "下单"),
new MsgModel("zxcv", 2, "付款"),
new MsgModel("zxcv", 2, "发货")
);
public static void main(String[] args) throws Exception{
DefaultMQProducer producer = new DefaultMQProducer("orderly-producer-group");
producer.setNamesrvAddr("ip:9876");
producer.start();
// 发送顺序消息 发送时要确保有序 并且要发到同一个队列下面去
msgModels.forEach(msgModel -> {
Message message = new Message("orderly-topic", msgModel.toString().getBytes());
try {
// 发 相同的订单号去相同的队列
//MessageQueueSelector() 消息队列选择器
producer.send(message, new MessageQueueSelector() {
@Override
//Object arg 即为 select的第三个参数 msgModel.getOrderSn()
//作为消息发送分区的分类标准
public MessageQueue select(List mqs, Message msg, Object arg) {
// 在这里 选择队列
// 保证 订单号 相同的消息在同一个 queue
int hashCode = arg.toString().hashCode();
// 周期性函数
int i = hashCode % mqs.size();
return mqs.get(i);
}
}, msgModel.getOrderSn());
} catch (Exception e) {
e.printStackTrace();
}
});
producer.shutdown();
System.out.println("发送完成");
}
}
我们详细看看MessageQueueSelector的接口:
public interface MessageQueueSelector {
MessageQueue select(final List mqs, final Message msg, final Object arg);
}
其中 mqs 是可以发送的队列,msg是消息,arg是上述send接口中传入的Object对象(第三个参数),返回的是该消息需要发送到的队列。本例是以orderSn(订单编号)作为分区分类标准,对所有队列个数取余,来对将相同orderId的消息发送到同一个队列中。
生产环境中建议选择最细粒度的分区键进行拆分,例如,将订单ID、用户ID作为分区键关键字,可实现同一终端用户的消息按照顺序处理,不同用户的消息无需保证顺序。
顺序消息消费者:
public class OrderConsumer {
public static void main(String[] args) throws Exception{
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("orderly-consumer-group");
consumer.setNamesrvAddr("ip:9876");
consumer.subscribe("orderly-topic", "*");
// MessageListenerConcurrently 并发模式 多线程的 重试16次 后会将其放入 死信队列
// MessageListenerOrderly 顺序模式 单线程的 无限重试Integer.Max_Value
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) {
System.out.println("线程id:" + Thread.currentThread().getId());
System.out.println(new String(msgs.get(0).getBody()));
//若返回 ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT 则会被挂起,等待一段时间再重试
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
System.in.read();
}
}
若返回 ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT 则会被挂起,等待一段时间再重试
打印结果如下: