RabbitMQ学习笔记

147 阅读8分钟

rabbitMQ学习笔记

  • rabbitMQ是一款MQ(消息队列)产品,除了rabbitMQ之外,常见的MQ产品还有kafka、socketMQ、activeMQ等...消息队列作为一种中间件,广泛应用于软件开发领域,主要的作用有异步、解耦、削峰。
  • 消息队列产品都将程序分为生产者和消费者,消息的生产者将消息发送给消息队列,再有消息队列将消息发送给消费者。
  • rabbitMQ支持七种模式:
    1. "Hello World":helloworld是最简单的模式,一个生产者通过队列对应一个消费者
    2. Work queues:(工作队列)一个生产者通过队列对应多个消费者
    3. Publish/Subscribe:(发布/订阅)一个生产者通过交换机对应多个队列,每个队列对应一个消费者
    4. Routing:(路由)
    5. Topics:(话题)
    6. RPC:(请求/回复)
    7. Publisher Confirms:(发布确认)

Hello World模式

  • hello world是最简单的消息队列模式 helloworld

代码实现

  • 获取MQ连接的工具类
public class MqUtils {
    private static final ConnectionFactory CONNECTION_FACTORY;

    static {
        //获取与MQ的连接工厂
        CONNECTION_FACTORY = new ConnectionFactory();
        //设置主机ip
        CONNECTION_FACTORY.setHost("**.**.**.**");
        //设置端口
        CONNECTION_FACTORY.setPort(5672);
        //设置连接的虚拟主机
        CONNECTION_FACTORY.setVirtualHost("/edu");
        //设置用户名和密码
        CONNECTION_FACTORY.setUsername("ye");
        CONNECTION_FACTORY.setPassword("1234qweR");
    }

    public static Connection getConnection() {
        try {
            //获取连接对象
            return CONNECTION_FACTORY.newConnection();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void close(Connection conn, Channel channel) {
        try {
            if (Objects.nonNull(channel)) {
                channel.close();
            }
            if (Objects.nonNull(conn)) {
                conn.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 消息生产者示例
    //生产消息
    @Test
    public void testSendMessage() throws IOException, TimeoutException {
        Connection conn = MqUtils.getConnection();
        assert conn != null;
        Channel channel = conn.createChannel();
        //队列声明
        //第一个参数:队列的名称		第二个参数:是否需要将队列持久化
        //第三个参数:是否独占队列		第四个参数:消费完成后是否删除队列
        //第五个参数:额外的参数设置
        channel.queueDeclare("hello",false,false,false,null);
        //发布消息
        //第一个参数:使用的交换机名称		第二个参数:使用的队列名称
        //第三个参数:传递消息的额外设置		第四个参数:传递的消息体(字节)
        Objects.requireNonNull(channel).basicPublish("", "hello", null, "hello rabbitMQ!".getBytes());
        MqUtils.close(conn, channel);
    }

注意

  1. 队列声明的第二个参数只能将队列持久化,并不能将队列中的消息持久化,要想将队列中的消息持久化,需要在消息发送时指定一个额外设置:MessageProperties.PERSISTENT_TEXT_PLAIN。

  • 消息消费者示例
Connection conn = MqUtils.getConnection();
        assert conn != null;
        Channel channel = conn.createChannel();
        //第二个参数:是否开启自动确认
        Objects.requireNonNull(channel).basicConsume("hello", 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));
            }
        });
        //mq消费者采用异步模型,如果不关闭连接,就会一直监听消息队列,当队列里有消息之后就会被消费
        //在异步模型中,由于main线程和消费者线程是属于两个线程,所以当在main线程中关闭连接后,可能消费线程还没有执行回调
        //MqUtils.close(conn, channel);

  • 注意
    1. 消费者采用异步模型,不能在单元测试中测试(单元测试中只支持同步模型,不支持多线程)。
    2. 在关闭连接时,要先关闭Channel再关闭Connection,因为关闭Connection时会清空Channel,这时如果再关闭Channel就会爆出com.rabbitmq.client.alreadyclosedexception异常。
    3. 生产者和消费者所使用的队列参数要严格对应。

Work queues

  • Work queues(工作队列):在消费者之间分配任务(默认平均的消费者模式) Work queues

代码实现

消费者默认是一次从队列中拿出多条消息,按照消费者数量进行平均分配,如果开启了消费自动确认,那么当消费者从队列中拿到分配给自己的消息之后,会告知队列自己已经确认拿到了消息,队列将会把这些消息删除。如果消费者拿到了五条消息,但是在处理完第三条消息之后宕机,那么其余两条消息就会丢失。并且这种一次性拿取所有消息的方式会降低程序的性能,更建议用竞争的方式去获取消息。

  • 竞争的消费者实现
		...
        Channel channel = conn.createChannel();
        channel.basicQos(1);//一次只获取一条消息
        //第二个参数:是否开启消息自动确认,这里需要手动确认消息
        Objects.requireNonNull(channel).basicConsume("hello", false, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                System.out.println("Consumer2==========" + new String(body));
                //手动确认消息,第一个参数:当前消费的消息的标识	第二个参数:是否开启多个消息同时确认
                channel.basicAck(envelope.getDeliveryTag(),false);
            }
        });

发布/订阅模式(fanout)

fanout(扇出)模式,一个生产者对应一个交换机对应多个临时队列。每个队列对应一个消费者。

在发布/订阅模式中,是一个消息的提供者对应多个消息的消费者。就是一条消息会被多个消费者进行消费。 发布/订阅模式

代码实现

  • 消息生产者示例
        //获取连接
        Connection connection = MqUtils.getConnection();
        Channel channel = connection.createChannel();
        //声明一个交换机,名字是logs   类型是fanout
        channel.exchangeDeclare("logs","fanout");
        //消息发布,fanout模式下不需要设置队列名
        channel.basicPublish("logs","",null,"hello".getBytes());
        MqUtils.close(connection,channel);
  • 消息消费者示例
      	//绑定交换机
        channel.exchangeDeclare("logs","fanout");
        //创建临时队列
        String queue = channel.queueDeclare().getQueue();
        //将临时队列绑定交换机
        channel.queueBind(queue,"logs","");
        //消息处理
        channel.basicConsume(queue,true,new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                System.out.println("消费者1*******"+new String(body));
            }
        });

消费者1和消费者2都会收到provider的消息,并且各自进行处理。


路由模式(routing)

路由模式是在发布/订阅模式的基础上(将消息全部发送给消费者),修改为将消息通过路由分别发送给不同的消费者。

路由模式

代码实现

  • 消息发送者示例
        //声明交换机,类型是direct
        channel.exchangeDeclare("logs_direct","direct");
        //发送消息
        channel.basicPublish("logs_direct","error",null,"this is error...".getBytes());
  • 消息消费者示例1
        //创建一个临时队列
        String queue = channel.queueDeclare().getQueue();

        //声明交换机
        channel.exchangeDeclare("logs_direct", "direct");
        //绑定队列交换机和routingKey
        channel.queueBind(queue, "logs_direct", "error");
        //消费消息
        channel.basicConsume(queue, 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));
            }
        });
此消费者1通过绑定交换机、路由和routingKey,只接收routingKey为"error"的消息
  • 消息消费者示例2
      	//创建一个临时队列
        String queue = channel.queueDeclare().getQueue();
        //声明交换机
        channel.exchangeDeclare("logs_direct", "direct");
        //绑定交换机和routingKey
        channel.queueBind(queue, "logs_direct", "error");
        channel.queueBind(queue, "logs_direct", "info");
        channel.queueBind(queue, "logs_direct", "warning");
        //消费消息
        channel.basicConsume(queue, 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));
            }
        });
此消费者2通过队列绑定,接收routingKey为"error""info""warning"的消息

动态路由模型(topic)

动态路由模型(topic)是在路由模型(routing)的基础上加入了通配符'*'和'#'

* :可以代替一个单词。

# :可以代替多个单词。

动态路由模型

代码实现

  • 消息生产者示例
      	//声明一个交换机,类型是topic
        channel.exchangeDeclare("topics","topic");
        //发送的routingKey
        //此消息会被1,2共同消费
        String routingKey = "user.save";
        //此消息只会被2消费
        //String routingKey = "user.save.findAll"
        //发送消息
        channel.basicPublish("topics",routingKey,null,("这里是topics动态路由模型,routingKey:"+routingKey).getBytes());
  • 消息消费者示例1
        //声明交换机
        channel.exchangeDeclare("topics", "topic");
        //创建一个临时队列
        String queue = channel.queueDeclare().getQueue();
        //绑定动态路由,消费以user.开头,并且后面只有一个单词的所有消息
        channel.queueBind(queue, "topics", "user.*");
        //处理消息
        channel.basicConsume(queue, true, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                System.out.println("消费者1:"+new String(body));
            }
        });

消费以user.开头,并且后面只有一个单词的消息

  • 消息消费者示例2
        ***
        //绑定动态路由,消费routingKey以user.开头的所有消息
        channel.queueBind(queue, "topics", "user.#");
        ***

springboot整合rabbitMQ

  • 导入rabbitMQ的启动器依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
  • 在配置文件中配置连接MQ的各种参数
        # rabbitMQ的参数配置
        spring.rabbitmq.host=**.**.**.**
        spring.rabbitmq.port=5672
        spring.rabbitmq.virtual-host=/edu
        spring.rabbitmq.username=*****
        spring.rabbitmq.password=*******
  • 在消息生产端注入MQ的模板对象
		@Autowired RabbitTemplate rabbitTemplate;

注意

  1. 模板对象是启动器自动提供的,不需要手动定义,只要在配置文件中配置了MQ的连接参数,就会自动加入IOC容器。
  2. 在springboot中,交换机和队列声明是由消费者端完成的,生产端只需要指定队列(交换机)名,然后发送消息即可。
  • 编写消息生产代码
		//向名为topic的交换机中发送路由key为user.save的消息,消息体为:user.save
		rabbitTemplate.convertAndSend("topics","user.save","user.save");
  • 编写消息消费者代码
  1. hello world模型
@Component
@RabbitListener(queuesToDeclare = @Queue(value = "hello"))
public class HelloConsumer {

    @RabbitHandler
    public void receive(String message) {
        System.out.println("消费者消费了消息:\t"+message);
    }
}
  1. work模型
@Component
public class WorkConsumers {

    @RabbitListener(queuesToDeclare = @Queue(value = "work"))
    public void consumer1(String message) {
        System.out.println("work的消费者1消费了消息:\t"+message);
    }

    @RabbitListener(queuesToDeclare = @Queue(value = "work"))
    public void consumer2(String message) {
        System.out.println("work的消费者2消费了消息:\t"+message);
    }
}
  1. fanout模型
@Component
public class FanoutConsumer {

    @RabbitListener(bindings = {
        @QueueBinding(
            value = @Queue,  //绑定临时队列
            exchange = @Exchange(value = "log",type = "fanout")    //绑定名为log,类型是fanout的交换机
        )
    })
    public void consumer1(String message) {
        System.out.println("消费者1消费了消息:\t"+message);
    }

    @RabbitListener(bindings = {
        @QueueBinding(
            value = @Queue,  //绑定临时队列
            exchange = @Exchange(value = "log",type = "fanout")    //绑定名为log,类型是fanout的交换机
            )
    })
    public void consumer2(String message) {
        System.out.println("消费者2消费了消息:\t"+message);
    }
}
  1. route模型
@Component
public class RouteConsumer {

    @RabbitListener(bindings = {
        @QueueBinding(
            value = @Queue,
            exchange = @Exchange(value = "direct",type = "direct"),
            key = {"info","error","warn"}
        )
    })
    public void consumer1(String message) {
        System.out.println("消费者1消费了消息:\t"+message);
    }

    @RabbitListener(bindings = {
        @QueueBinding(
            value = @Queue,
            exchange = @Exchange(value = "direct",type = "direct"),
            key = {"info"}
        )
    })
    public void consumer2(String message) {
        System.out.println("消费者2消费了消息:\t"+message);
    }
}
  1. topics模型
    @RabbitListener(bindings ={
        @QueueBinding(
            value = @Queue,
            exchange = @Exchange(value = "topics",type = "topics"),
            key = {"user.*"}
        )
    })
    public void consumer1(String message) {
        System.out.println("消费者1:\t"+message);
    }

    @RabbitListener(bindings ={
        @QueueBinding(
            value = @Queue,
            exchange = @Exchange(value = "topics",type = "topics"),
            key = {"user.#"}
        )
    })
    public void consumer2(String message) {
        System.out.println("消费者2:\t"+message);
    }

注意:@RabbitListener注解可以作用于类上,也可以作用于方法上,当作用于类上时,有@RabbitHandler注解的方法作为消费者。当作用于方法上时,方法作为消费者