2. rabbitmq(消息中间件)

一、引言

什么RabbitMQ?

RabbitMQ是基于amqp协议,实现的一种MQ理念的服务。类似的服务 RocketMQ、ActiveMQ、Kafka等

为什么在分布式项目中需要一款消息中间件?

消息中间件能够实现一些Feign(同步调用)无法实现的效果:

1、服务的异步调用
2、消息的广播(事件总线)
3、消息的延迟处理
4、分布式事务
5、请求削峰(处理高并发)

二、RabbitMQ的Docker安装

1)拉取镜像

docker pull rabbitmq:3.8.5-management

2)准备docker-compose模板

....
rabbitmq:
    image: rabbitmq:3.8.5-management
    container_name: rabbitmq
    ports:
      - 5672:5672
      - 15672:15672
    restart: always

3)启动rabbitmq容器

docker-compose up -d rabbitmq

4)访问rabbitmq的管理页面

在这里插入图片描述

5)登录rabbitmq的管理页面 (账号:guest 密码:guest)

三、RabbitMQ常用模型

1)模型一

P -> Provider(提供者)
红色方块 -> 队列(存储消息)
C -> Consumer(消费者)

在这里插入图片描述

2)模型二

一个提供者对应多个消费者,消息会轮训发送给两个消费者

起到一个消费端负载均衡的目的,减轻消费端的消费压力

在这里插入图片描述

3)模型三

发布/订阅模式 - 消息广播

多个消费者会同时收到提供者发布的消息

X -> Exchange(交换机,消息的复制转发,不能存储消息)交换机类型有4种fanout,direct,topic,header 模型3用的就是fanout,不含路由键的无条件广播

在这里插入图片描述

4)模型四 (交换机用的是direct,含路由键的,选择)

路由键 -> 交换机和队列绑定若干路由键,发布的消息可以指定路由键发送

在这里插入图片描述

5)模型五

通配符的路由键

在这里插入图片描述

6)模型六

Rabbitmq的同步调用模型

在这里插入图片描述

四、JavaAPI调用RabbitMQ

添加依赖

<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.9.0</version>
</dependency>

1)模型一的实现

服务的提供者:

public static void main(String[] args) throws IOException, TimeoutException {
    //1、连接RabbitMQ
    Connection connection = ConnectionUtil.getConnection();

    //2、通过连接获得管道对象(后面所有的操作都是通过管道对象操作)
    Channel channel = connection.createChannel();

    //3、创建队列
    channel.queueDeclare("test_queue1", false, false, false, null);

    //4、给队列中发布消息
    String msg = "Hello RabbitMQ!!!!";
    channel.basicPublish("", "test_queue1", null, msg.getBytes("utf-8"));

    //关闭连接
    connection.close();
}

服务的消费者:

public static void main(String[] args) throws IOException {
        //1、连接RabbitMQ
        Connection connection = ConnectionUtil.getConnection();

        //2、获得连接的channel对象
        Channel channel = connection.createChannel();

        //3、监听队列
        channel.basicConsume("test_queue1", true, new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("接收到消息:" + new String(body, "utf-8"));
            }
        });

        //关闭连接???
//        connection.close();
    }

思考:

1、队列应该在消费者端创建还是提供者端创建?- 消费者通常创建队列,提供者创建交换机

因为提供者创建交互机没有消费者顶多把消息丢失,而不会报错,消费者创建队列没有人给他发消息他也不会报错。

2、消费端是同步消费消息还是异步消费消息?- 同步消费,必须消费完一条消息,才能继续消费下一条消息,在实际开发过程中,为了提高消费者的消费速率,往往会引入线程池的方式,进行多线程消费。

3.匿名内部类使用外部类的局部变量需要加上final

//创建一个线程池 - 线程数量为5
private static ExecutorService executorService = Executors.newFixedThreadPool(5);
....
channel.basicConsume("test_queue1", true, new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {

          executorService.submit(new Runnable() {
              public void run() {
                  try {
                      System.out.println("接收到消息:"
                                               + new String(body, "utf-8"));
                  } catch (UnsupportedEncodingException e) {
                      e.printStackTrace();
                  }
                  try {
                      Thread.sleep(2000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
          });

      }
  });    

五、常用方法的参数

/**
 * Declare a queue
 * @see com.rabbitmq.client.AMQP.Queue.Declare
 * @see com.rabbitmq.client.AMQP.Queue.DeclareOk
 * @param queue the name of the queue
 * @param durable true if we are declaring a durable queue (the queue will survive a server restart)
 * @param exclusive true if we are declaring an exclusive queue (restricted to this connection)
 * @param autoDelete true if we are declaring an autodelete queue (server will delete it when no longer in use)
 * @param arguments other properties (construction arguments) for the queue
 * @return a declaration-confirm method to indicate the queue was successfully declared
 * @throws java.io.IOException if an error is encountered
 */
Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, 				boolean autoDelete,Map<String, Object> arguments) throws IOException;

参数一:queue 队列的名称
参数二:durable 是否持久化,默认为false,非持久化
参数三:exclusive 是否为排他队列,排他队列只有创建这个队列的连接可以操作,其他连接不能操作该队列
参数四:autoDelete 是否自动删除,默认为false,如果为true,那么当所有监听这个队列的消费端断开监听后,队列会自动删除
参数五:arguments 用来设置一个额外的参数

/**
 * Declare an exchange, via an interface that allows the complete set of
 * arguments.
 * @see com.rabbitmq.client.AMQP.Exchange.Declare
 * @see com.rabbitmq.client.AMQP.Exchange.DeclareOk
 * @param exchange the name of the exchange
 * @param type the exchange type
 * @param durable true if we are declaring a durable exchange (the exchange will survive a server restart)
 * @param autoDelete true if the server should delete the exchange when it is no longer in use
 * @param internal true if the exchange is internal, i.e. can't be directly
 * published to by a client.
 * @param arguments other properties (construction arguments) for the exchange
 * @return a declaration-confirm method to indicate the exchange was successfully declared
 * @throws java.io.IOException if an error is encountered
 */
Exchange.DeclareOk exchangeDeclare(String exchange,
                                          String type,
                                          boolean durable,
                                          boolean autoDelete,
                                          boolean internal,
                                          Map<String, Object> arguments) throws 																		IOException;

参数一:exchange 交换机的名称
参数二:type 交换机的类型(fanout|direct|topic|header)
参数三:durable 持久化,默认false
参数四:autoDelete 是否自动删除,默认为false,如果未true,所以绑定到交换机的队列,解绑后,交换机就会自动删除
参数五:internal 是否为内置交换机,默认为false,如果为true表示,当前交换机只能绑定其他交换机,提供者不能直接发消息给该交换机
参数六:arguments 用来设置一个额外的参数

六、TTL - 过期时间

6.1 消息的过期时间(重要)
6.1.1 通过队列的方式设置消息的过期时间
Map<String, Object> map = new HashMap<>();
//通过队列,设置5秒的消息过期时间
map.put("x-message-ttl", 5000);
channel.queueDeclare("demo_queue", true, false, false, map);
6.1.2 通过消息本事设置消息的过期时间
String msg = "Hello RabbitMQ!!!!";
//给当前消息本身设置5秒的过期时间
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties().builder();
builder.expiration("5000");
AMQP.BasicProperties basicProperties = builder.build();

channel.basicPublish("demo_exchange", "", basicProperties, msg.getBytes("utf-8"));

注意:
1、RabbitMQ的队列,只会移除队头的过期元素
2、通过队列的方式设置消息的过期时间,过期的元素一定是在队头
3、通过消息本身设置过期时间,过期的消息就不一定在队头

6.2 队列本身的过期时间(了解)
Map<String, Object> map = new HashMap<>();
//设置队列本身的过期时间
map.put("x-expires", 5000);
channel.queueDeclare("demo_queue", true, false, false, map);

七、死信队列

7.1 什么是死信队列?

死信队列本身其实是一个普通队列,但是专门用来存放死信消息,这种队列就称之为死信队列

7.2 什么是死信消息?

死信消息的产生方式:

1、消息过期后,就会变成死信消息
2、队列满了之后,继续添加元素,就会产生死信消息
3、消息被消费者拒绝,并且requeue设置为false时,消息变成死信消息

在这里插入图片描述

7.3 设置死信交换机、死信队列
//创建死信交换机、死信队列、绑定
//---------------------------------------------
channel.exchangeDeclare("dead_exchange", "fanout");
channel.queueDeclare("dead_queue", true, false,false, null);
channel.queueBind("dead_queue", "dead_exchange", "");
//---------------------------------------------

//3、创建交换机
channel.exchangeDeclare("demo_exchange", "fanout"); //fanout|direct|topic|header
//4、创建队列
Map<String, Object> map = new HashMap<>();
//设置普通队列绑定死信交换机
map.put("x-dead-letter-exchange", "dead_exchange");
//设置5秒的过期时间,方便参数死信消息
map.put("x-message-ttl", 5000);
channel.queueDeclare("demo_queue", true, false, false, map);
//绑定
channel.queueBind("demo_queue", "demo_exchange", "");

八、延迟队列

8.1 什么是延迟队列?

提供者发送的消息,要通过一段时间的延迟,才会被消息者消费。

注意:RabbitMQ本身没有提供延迟队列,但是开发者可以通过TTL + 死信队列的方式,实现延迟的效果

8.2 延迟队列的实现

在这里插入图片描述

8.3 延迟队列的实际运用场景(但是延迟队列有一个缺点因为RabbitMQ的队列,只会移除队头的过期元素,死信队列也是一样,所以在面对不确定过期时间就不好了,比如队头是8个小时过期,后面是半个小时,得先等8个小时的消费完了才能消费半个小时的)

下单后,半小时之内,未支付的订单需要自动关闭

在这里插入图片描述

8.4 延迟队列的实际开发结构

在这里插入图片描述

1、延迟队列没办法实现随意时间的延迟
2、延迟队列不能取代定时任务
延迟队列非常适合处理,过xxxx时间需要做xxx事情的场景,不适合处理每天中午12点做xxx事情

九、消息的持久化

RabbitMQ中,队列的持久化,不意味着消息持久化,默认消息都是非持久化的。

9.1 设置持久化消息

方式一:

AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties().builder();
//给当前消息本身设置5秒的过期时间
//builder.expiration("5000");
//设置当前消息未持久化消息 - 写入RabbitMQ的硬盘中的
builder.deliveryMode(2);
AMQP.BasicProperties basicProperties = builder.build();
channel.basicPublish("demo_exchange", "", basicProperties, msg.getBytes("utf-8"));

方式二:

channel.basicPublish("demo_exchange", "", 
                     MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes("utf-8"));

注意:因为持久化消息,意味着消息要写入RabbitMQ的硬盘,所以在相同数量消息的情况下,持久化也就意味着需要消耗更多的发布时间和RabbitMQ服务器的性能,会降低RabbitMQ服务的吞吐量。实际开发过程中,尽量只对需要持久化的消息设置持久化。

思考:交换机持久化 + 队列持久化 + 消息持久化 能否保证消息一定到达消费端? - 不能

十、发送端的确认机制

10.1 RabbitMQ的事务机制
//将当前的管道模式设置成事务模式
channel.txSelect();

try {
    String msg = "Hello RabbitMQ!!!!";
    channel.basicPublish("demo_exchange", "", 										                MessageProperties.PERSISTENT_TEXT_PLAIN,
                         msg.getBytes("utf-8"));

    //提交事务
    channel.txCommit();
} catch (IOException e) {
    e.printStackTrace();
    //回滚事务
    channel.txRollback();

    //消息的重试机制(3次) + 消息的补偿机制
}
10.2 RabbitMQ的Publish confirmer机制
10.2.1 同步模式
//publish confirm - 同步模式
//设置channel为confirm模式
channel.confirmSelect();

//发送消息
String msg = "Hello RabbitMQ!!!!";
channel.basicPublish("demo_exchange", "", MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes("utf-8"));

boolean flag = true;
try {
    if (!channel.waitForConfirms()) {
        //消息发布失败
        flag = false;
    } 
} catch (InterruptedException e) {
    e.printStackTrace();
    //消息发送失败
    flag = false;
}

if (!flag){
    //进行消息重试 + 消息补偿机制
}
10.2.2 异步模式
//publish confirm - 异步模式
//设置channel为confirm模式
channel.confirmSelect();

//设置confirm的异步模式的监听回调
channel.addConfirmListener(new ConfirmListener() {
    @Override
    public void handleAck(long deliveryTag, boolean multiple) throws IOException {
        //确认达到rabbitmq
        //deliveryTag - 消息id
        //multiple - 批量 true批量确认 false单条确认
        System.out.println("达到rabbitmq的消息:" + deliveryTag + " 批量?" + multiple);
    }

    @Override
    public void handleNack(long deliveryTag, boolean multiple) throws IOException {
        //未确认消息达到rabbitmq
        //deliveryTag - 消息id
        //multiple - 批量 true批量未到达 false单条未到达
        //进行消息的重试和补偿
    }
});

//发送消息
for (int i = 0; i < 10000; i++) {
    String msg = "Hello RabbitMQ!!!!" + i;
    channel.basicPublish("demo_exchange", "", MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes("utf-8"));

}

十一、消费端确认、拒绝、限制机制

手动确认

channel.basicConsume("dead_queue", false, new DefaultConsumer(channel){
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        System.out.println("接收消息的时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
        System.out.println("消费者接收到消息:" + new String(body, "utf-8"));

        //处理一个复杂的业务

        //消息的手动确认
        channel.basicAck(envelope.getDeliveryTag(), false);
    }
});

手动拒绝

//拒绝机制
//参数三:requeue,如果为true,表示消息继续放回rabbitmq队列中,如果未false,表示不放回去,消息变成死信消息
channel.basicNack(envelope.getDeliveryTag(), false, true);

消息的数量限制

//限制消息的数量
channel.basicQos(100);
channel.basicConsume("dead_queue", false, new DefaultConsumer(channel){
    ....
}

十二、RabbitMQ的消息补偿机制

RabbitMQ是没有提供补偿机制的,需要开发者手动维护

在这里插入图片描述

十三、消费端的幂等问题

各种确认机制、补偿机制、重试机制是完全有可能造成消息重复发送的,所以实际开发过程中,一定要保证消费端的幂等性。
解决方法:可以是悲观锁和乐观锁最常用的还是分布式锁

十四、RabbitMQ和SpringBoot整合

消费者和提供者都需要添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
消息的提供者:

1)配置application.yml

spring:
  rabbitmq:
    host: 192.168.195.188
    port: 5672
    username: guest
    password: guest
    virtual-host: /

2)创建RabbitMQ的交换机

@Configuration
public class RabbitMQConfiguration {
    
    @Bean
    public DirectExchange getExchange(){
        return new DirectExchange("hotal_exchange", true, false);
    }
}

3)发布消息到交换机

@Autowired
private RabbitTemplate rabbitTemplate;

//广播“酒店添加”的事件
rabbitTemplate.convertAndSend("hotal_exchange", "hotal_insert", entity);

注意:Spring管理的RabbitMQ对象,默认是懒加载,如果不发送消息,交换机就不会创建

消息的接收者:

1)配置RabbitMQ的连接配置

2)创建队列、交换机并且进行绑定

@Configuration
public class RabbitMQConfiguration {

    @Bean
    public DirectExchange getExchange(){
        return new DirectExchange("hotal_exchange", true, false);
    }

    @Bean
    public Queue getQueue(){
        return new Queue("city_queue", true, false, false);
    }

    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding getBinding(Queue getQueue, DirectExchange getExchange){
//        return new Binding("city_queue", Binding.DestinationType.QUEUE, "hotal_exchange", "hotal_insert", null);
        return BindingBuilder.bind(getQueue).to(getExchange).with("hotal_insert");
    }
}

3)监听队列

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值