【中间件】RabbitMQ

195 阅读3分钟

1. 介绍

  RabbitMQ是使用Erlang语言开发的开源消息队列系统,基于AMQP协议。 029-AMQP.png

RabbitMQ目前支持7种模型,还有以下特点:

  • 开源、性能优秀,稳定性保障
  • 提供可靠性消息投递模式、返回模式
  • 与Spring AMQP完美整合,API丰富
  • 集群模式丰富,表达式配置,HA模式,镜像队列模型
  • 保证数据不丢失的前提做到高可靠性、可用性

2. 安装(Windows版本)

  • 下载安装RabbitMQ安装包Erlang语言

  • 下载完Erlang语言后,设置环境变量

    1. 新建系统变量ERLANG_HOME,值为安装路径

    2. 编辑系统变量的path变量,新建%ERLANG_HOME%\bin

  • 安装完RabbitMQ后,进入安装目录sbin文件夹,使用以下命令开启管理界面

    rabbitmq-plugins.bat enable rabbitmq_management
    
  • 在开始菜单里可以开启RabbitMQ服务,进入http://127.0.0.1:15672网址,默认账号密码为guest/guest

2.1 配置虚拟主机

029-配置虚拟主机.png

3. Hello World模型

3.1 引入依赖

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

3.2 生产者

@Test
public void testSendMessage() throws IOException, TimeoutException {
    // 创建连接mq的工厂对象
    ConnectionFactory connectionFactory = new ConnectionFactory();
    connectionFactory.setHost("127.0.0.1");
    connectionFactory.setPort(5672);
    // 设置连接哪个虚拟主机
    connectionFactory.setVirtualHost("/ems");
    // 设置访问虚拟主机的用户
    connectionFactory.setUsername("guest");
    connectionFactory.setPassword("guest");

    // 获取连接对象
    Connection connection = connectionFactory.newConnection();
    // 获取连接的通道
    Channel channel = connection.createChannel();

    // 通道绑定对应消息队列
    // 参数1:队列名称,队列不存在则会自动创建
    // 参数2:队列特性是否持久化
    // 参数3:是否为独占队列(即只有当前连接可用)
    // 参数4:是否消费完成后删除队列
    // 参数5:额外参数
    channel.queueDeclare("hello", false, false, false, null);

    // 发布消息
    // 参数1:交换机
    // 参数2:队列名称
    // 参数3:传递消息的额外设置,MessageProperties.PERSISTENT_TEXT_PLAIN:消息持久化
    // 参数4:消息的字节数组
    channel.basicPublish("", "hello", null, "hello RabbitMq".getBytes());

    channel.close();
    connection.close();
}

发送完后管理页面会看到有一条消息,且未被消费 029-生产者发送消息.png

3.3 消费者

public static void main(String[] args) throws IOException, TimeoutException {
    ConnectionFactory connectionFactory = new ConnectionFactory();
    connectionFactory.setHost("127.0.0.1");
    connectionFactory.setPort(5672);
    connectionFactory.setVirtualHost("/ems");
    connectionFactory.setUsername("guest");
    connectionFactory.setPassword("guest");

    Connection connection = connectionFactory.newConnection();
    Channel channel = connection.createChannel();
    channel.queueDeclare("hello", false, false, false, null);

    Consumer consumer = new DefaultConsumer(channel) {
        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            // 最后一个参数是队列中取出的消息
            System.out.println("msg = " + new String(body));
        }
    };
    // 消费消息,会消费全部消息
    // 参数1:队列名称
    // 参数2:消息的自动确认机制,见章节4.4
    // 参数3:消费消息时的回调接口
    channel.basicConsume("hello", true, consumer);

    // 如果这里把连接关闭,会出现来不及用回调函数打印的情况
    //        channel.close();
    //        connection.close();
}

3.4 提炼工具类

public class RabbitMqUtils {
    private static ConnectionFactory factory;
    static {
        factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setPort(5672);
        factory.setVirtualHost("/ems");
        factory.setUsername("guest");
        factory.setPassword("guest");
    }

    public static Connection getLocalConnection() {
        try {
            return factory.newConnection();
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void closeConnectionAndChannel(Connection connection, Channel channel) {
        try {
            if (channel != null) {
                channel.close();
            }
            if (connection != null) {
                connection.close();
            }
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
    }
}

4. Work queues模型

4.1 生产者

public static void main(String[] args) throws IOException {
    Connection localConnection = RabbitMqUtils.getLocalConnection();
    Channel channel = localConnection.createChannel();
    channel.queueDeclare("work", true, false, false, null);
    for (int i = 1; i <= 10; i++) {
        String msg = "我是消息" + i;
        channel.basicPublish("", "work", null, msg.getBytes());
    }
    RabbitMqUtils.closeConnectionAndChannel(localConnection, channel);
}

4.2 消费者

// 多个消费者的代码一样,只是消费者名字不同
public static void main(String[] args) throws IOException {
    Connection localConnection = RabbitMqUtils.getLocalConnection();
    Channel channel = localConnection.createChannel();
    channel.queueDeclare("work", true, false, false, null);
    channel.basicConsume("work", 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));
        }
    });
}

4.3 运行结果

  • 默认情况下,各个消费者消费的消息是一样多的,即各个消费者之间是轮询关系 029-wq消费者1.png 029-wq消费者2.png

  • 在SpringBoot整合的AMQP中,默认是公平消费,如需实现"能者多劳"则要额外配置

4.4 消费者的消息自动确认机制

  即消费者自动向RabbitMQ确认消息消费了。这种机制下,不会关心消费者拿到消息后是否处理完业务代码,只告诉RabbitMQ消费者已经消费了消息,这个时候RabbitMQ就会删除掉分配给该消费者的消息。就会出现以下场景:假如消费者分配了5条消息,但是在处理第3条的时候宕机了,但由于自动确认机制,RabbitMQ已经删除掉了这5条消息,那么就会造成消息丢失,所以一般都会关掉该机制。

public static void main(String[] args) throws IOException {
    Connection localConnection = RabbitMqUtils.getLocalConnection();
    Channel channel = localConnection.createChannel();
    // 一次消费一条消息
    channel.basicQos(1);
    channel.queueDeclare("work", true, false, false, null);
    // 第二个参数置为false,关闭自动确认机制
    channel.basicConsume("work", false, 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));
            // 执行完业务代码后,手动告诉RabbitMQ已经消费了该消息
            // 参数1:手动确认消息标识,确认队列的哪个具体消息
            // 参数2:是否开启多个消息同时确认
            channel.basicAck(envelope.getDeliveryTag(), false);
        }
    });
}

5. Publish/Subscribe模型

即发布/订阅(广播)模式,在该模式下:

  • 可以有多个消费者,每个消费者都有自己的队列
  • 每个队列都要绑定到交换机
  • 生产者发布的消息,只能发送到交换机,由交换机决定发给哪个队列,生产者无法决定
  • 交换机把消息发送给绑定过的队列,队列对应的消费者消费消息,实现一条消息被多个消费者消费

5.1 生产者

public static void main(String[] args) throws IOException {
    Connection connection = RabbitMqUtils.getLocalConnection();
    Channel channel = connection.createChannel();

    // 指定交换机
    // 参数1:交换机名称
    // 参数2:交换机类型(fanout:广播类型)
    channel.exchangeDeclare("logs", BuiltinExchangeType.FANOUT);
    // 发布消息
    // 参数1:交换机名称
    // 参数2:路由key
    // 参数3:传递消息的额外设置,MessageProperties.PERSISTENT_TEXT_PLAIN:消息持久化
    // 参数4:消息的字节数组
    channel.basicPublish("logs", "", null, "fanout type msg".getBytes());
    RabbitMqUtils.closeConnectionAndChannel(connection, channel);
}

5.2 消费者

// 多个消费者的代码基本一致,只是消费消息的业务代码不同
public static void main(String[] args) throws IOException {
    Connection connection = RabbitMqUtils.getLocalConnection();
    Channel channel = connection.createChannel();
    // 通道绑定交换机
    channel.exchangeDeclare("logs", BuiltinExchangeType.FANOUT);
    // 临时队列的名字
    String tempQueueName = channel.queueDeclare().getQueue();
    // 绑定交换机和临时队列
    // 参数1:队列名字
    // 参数2:交换机名字
    // 参数3:路由key
    channel.queueBind(tempQueueName, "logs", "");
    // 消费消息
    channel.basicConsume(tempQueueName, 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));
        }
    });
}

6. Routing模型

6.1 Direct(直连)

  在广播模式下,一条消息会被所有订阅的队列消费。但是在某些场景下,我们希望不同的消息被不同的队列消费,这个时候就需要用到Direct类型的交换机。在该模式下:

  • 交换机与队列的绑定,不再是任意绑定,而是要指定一个RoutingKey(路由key)
  • 生产者向交换机发送消息时,也需要指定RoutingKey
  • 交换机不会再把消费发送给每个订阅的队列,而是根据RoutingKey进行判断,当队列的RoutingKey和消息的RoutingKey一致时,才会接收消息

6.1.1 生产者

public static void main(String[] args) throws IOException {
    Connection connection = RabbitMqUtils.getLocalConnection();
    Channel channel = connection.createChannel();
    // 参数分别是:交换机名称,交换机类型
    channel.exchangeDeclare("logs_direct", BuiltinExchangeType.DIRECT);
    // 发布消息
    String routingKey = "error";
    channel.basicPublish("logs_direct", routingKey, null, ("这是" + routingKey + "级别的日志").getBytes());
    RabbitMqUtils.closeConnectionAndChannel(connection, channel);
}

6.1.2 消费者

public static void main(String[] args) throws IOException {
    Connection connection = RabbitMqUtils.getLocalConnection();
    Channel channel = connection.createChannel();
    channel.exchangeDeclare("logs_direct", BuiltinExchangeType.DIRECT);
    String queueName = channel.queueDeclare().getQueue();
    // 基于路由key绑定交换机和队列
    channel.queueBind(queueName, "logs_direct", "info");
    channel.queueBind(queueName, "logs_direct", "warning");
    channel.queueBind(queueName, "logs_direct", "error");
    channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            System.out.println("消费者-2: " + new String(body));
        }
    });
}

示例中的消费者绑定了3种路由key,其他消费者可以根据具体的业务需求来绑定对应的路由key,来实现不同的消息可以被不同的队列消费。

6.2 Topics

  Topics和Direct相似,都是可以根据路由key把消息发到不同的队列,但是Topics类型的交换机可以让队列在绑定路由key的时候使用通配符。这种通配符一般由一个或多个单词组成,多个单词之间以.分割,如item.insert*可以匹配一个单词,#可以匹配一个或多个单词。

6.2.1 生产者

生产者代码与Direct类型的相似 ,只是声明的交换机的类型变了

channel.exchangeDeclare("logs_direct", BuiltinExchangeType.TOPIC);
// 路由key使用多个单词的形式
String routingKey = "user.save";

6.2.2 消费者

消费者的改动也一样

channel.exchangeDeclare("logs_direct", BuiltinExchangeType.TOPIC);
String queueName = channel.queueDeclare().getQueue();
// 基于路由key绑定交换机和队列
channel.queueBind(queueName, "logs_direct", "user.*");

7. SpringBoot整合

7.1 引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

7.2 配置application.yml

spring:
  application:
    name: rabbitmq_app
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /ems

7.3 各模型生产者

@SpringBootTest(classes = RabbitMQApp.class)
@RunWith(SpringRunner.class)
public class TestRabbitMQ {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testHelloWorld() {
        rabbitTemplate.convertAndSend("hello", "HelloWorld模型");
    }

    @Test
    public void testWorkQueues() {
        for (int i = 1; i <= 10; i++) {
            rabbitTemplate.convertAndSend("work", "WorkQueues模型" + i);
        }
    }

    @Test
    public void testFanout() {
        rabbitTemplate.convertAndSend("logs", "", "Fanout模型");
    }

    @Test
    public void testRoutingDirect() {
        String routingKey = "error";
        String msg = "RoutingDirect模型:这是" + routingKey + "级别的日志";
        rabbitTemplate.convertAndSend("routingDirect", routingKey, msg);
    }

    @Test
    public void testRoutingTopics() {
        String routingKey = "user.delete";
        String msg = "RoutingTopics模型:" + routingKey;
        rabbitTemplate.convertAndSend("routingTopics", routingKey, msg);
    }
}

7.4 各模型消费者

@Component
@RabbitListener(queuesToDeclare = @Queue(value = "hello"))
public class HelloCustomer {
    @RabbitHandler
    public void receive(String message) {
        System.out.println("message = " + message);
    }
}

@Component
public class WorkCustomer {
    @RabbitListener(queuesToDeclare = @Queue(value = "work"))
    public void receive1(String message) {
        System.out.println("消费者1:" + message);
    }
    @RabbitListener(queuesToDeclare = @Queue(value = "work"))
    public void receive2(String message) {
        System.out.println("消费者2:" + message);
    }
}

@Component
public class FanoutCustomer {
    @RabbitListener(bindings = {
            // @Queue不加名字则表明创建临时队列
            @QueueBinding(value = @Queue, exchange = @Exchange(value = "logs", type = "fanout")),
    })
    public void receive1(String message) {
        System.out.println("消费者-1:" + message);
    }
    @RabbitListener(bindings = {
            @QueueBinding(value = @Queue, exchange = @Exchange(value = "logs", type = "fanout")),
    })
    public void receive2(String message) {
        System.out.println("消费者-2:" + message);
    }
}

@Component
public class RoutingDirectCustomer {
    @RabbitListener(bindings = {
            @QueueBinding(value = @Queue,
                    exchange = @Exchange(value = "routingDirect", type = "direct"),
                    key = {"info", "warning", "error"})
    })
    public void receive1(String message) {
        System.out.println("消费者-1:" + message);
    }
    @RabbitListener(bindings = {
            @QueueBinding(value = @Queue,
                    exchange = @Exchange(value = "routingDirect", type = "direct"),
                    key = {"error"})
    })
    public void receive2(String message) {
        System.out.println("消费者-2:" + message);
    }
}

@Component
public class RoutingTopicsCustomer {
    @RabbitListener(bindings = {
            @QueueBinding(value = @Queue,
                    exchange = @Exchange(value = "routingTopics", type = "topic"),
                    key = {"user.delete"})
    })
    public void receive1(String message) {
        System.out.println("消费者-1:" + message);
    }
    @RabbitListener(bindings = {
            @QueueBinding(value = @Queue,
                    exchange = @Exchange(value = "routingTopics", type = "topic"),
                    key = {"user.*"})
    })
    public void receive2(String message) {
        System.out.println("消费者-2:" + message);
    }
}

8. 相关链接