RocketMQ发送消息

一.消费模式RocketMQ发送消息_第1张图片

MQ的消费模式可以大致分为两种,一种是 推Push,一种是 拉Pull。 

  • Push 是 服务端 (MQ) 主动推送消息给客户端,优点是及时性较好,但如果客户端没有做好流控,一旦服务端推送大量消息到客户端时,就会导致客户端消息堆积甚至崩溃。
  • Pull 是 客户端 需要主动到 服务端 (MQ) 取数据,优点是客户端可以依据自己的消费能力进行消费,但拉取的频率也需要用户自己控制,拉取频繁容易造成服务端和客户端的压力,拉取间隔长又容易造成消费不及时。RocketMQ发送消息_第2张图片

Push模式也是基于Pull模式的,所以不管是Push模式还是Pull模式,都是Pull模式。一般情况下,优先选择Pull模式

二.普通消息

1.同步消息(***) 

同步消息 发送过后会有一个返回值,也就是mq服务器接收到消息后返回的一个确认,这种方式非常安全,但是性能上并没有这么高,而且在mq集群中,也是要等到所有的从机都复制了消息以后才会返回,所以针对重要的消息可以选择这种方式。

可靠的同步传输被广泛应用于各种场景,如重要的通知消息、短消息通知等。

RocketMQ发送消息_第3张图片

原生依赖引入:

        
        
            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();
    }
}

2.异步消息(***)

异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。发送完以后会有一个异步消息通知。

例如,视频上传后通知启动转码服务,转码完成后通知推送转码结果等。

RocketMQ发送消息_第4张图片

 异步消息生产者:

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();
    }
}

3.单向消息(*)

这种方式主要用在不关心发送结果的场景,这种方式吞吐量很大,但是存在消息丢失的风险,一般用于结果不重要的场景,例如日志信息的发送

RocketMQ发送消息_第5张图片

单向消息生产者:

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统一写入日志表中。并且由于日志过于庞大,可以对日志进行冷热分离,近一个月的为热数据,近一年的为冷数据(实际情况据业务而定),存储的位置不同,时间过于久远的日志可以删掉

RocketMQ发送消息_第6张图片

三.延迟消息(***)

消息放入MQ后,过一段时间,才会被监听到,然后消费

比如下订单业务,提交了一个订单就可以发送一个延时消息,15min后去检查这个订单的状态,如果还是未付款就取消订单释放库存(订单超时)

在分布式定时调度触发、任务超时处理等场景,使用 RocketMQ 的延时消息可以简化定时调度任务的开发逻辑,实现高性能、可扩展、高可靠的定时触发能力。

RocketMQ发送消息_第7张图片

延迟等级 

RocketMQ发送消息_第8张图片

延迟消息生产者:

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 msgs 传入方法中即可,需要注意的是批量消息的大小不能超过 1MiB(否则需要自行分割),其次同一批 batch 中 topic 必须相同。

批量消息生产者:

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();
    }
}

通过查看面板我们可以发现,批量消息全部放到了一个队列

RocketMQ发送消息_第9张图片

 批量消息消费者(无特殊变化): 

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 msgs 参数 每次只有 1条,因为MessageListenerConcurrently 默认是并发消费模式 

打印结果如下:

RocketMQ发送消息_第10张图片

五.顺序消息(*)

在默认的情况下普通消息的发送会采取 Round Robin 轮询方式 把 消息 发送到不同的queue(分区队列);而消费消息的时候从多个queue上拉取消息,这未满足 生产顺序性 和 消费顺序性。比方说一个订单的生成、付款和发货,这三个操作需要被顺序执行,但如果是普通消息,订单A的消息可能会被轮询发送到不同的队列中,不同队列的消息将无法保持顺序。

消息有序指的是可以按照消息的发送顺序来消费(FIFO),消息有序可以分为:分区有序 或者 全局有序

  • 全局有序:发送和消费参与的queue只有一个。即控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取。要保证全局顺序,需要把 Topic 的读写队列数设置为 1,然后生产者和消费者的并发设置也是 1,不能使用多线程。所以这样的话 高并发,高吞吐量的功能完全用不上。在这里插入图片描述
  • 分区有序:发送和消费参与的queue有多个。但相对每个queue,消息都是有序的。比方说 订单A的生成、付款 和 发货 的消息由于订单号相同(设立标准)所以分在同一个queue(如queue1)中,订单B的三个操作的消息也会因为这个标准在同一个queue中,但不一定是queue1,因为我们希望它们分散在不同的queue中以提高吞吐量。

程序模拟,封装实体类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();
    }
}
  • MessageListenerConcurrently 并发模式 多线程的  重试16次 后会将其放入 死信队列
  • MessageListenerOrderly 顺序模式 单线程的   无限重试(Integer.Max_Value 次)

若返回 ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT 则会被挂起,等待一段时间再重试

打印结果如下:

RocketMQ发送消息_第11张图片

你可能感兴趣的:(RocketMQ,rocketmq,分布式)