消息中间件—RabbitMQ(二)基础

一、RabbitMQ工作原理

1.RabbitMQ支持协议

RabbitMQ支持AMQP,STOMP,MQTT,HTTP,WebSockets协议

2.RabbitMQ工作模型

1)模型绘图

RabbitMQ工作模型

  • Broker主机:当消费者消费消息,生产者发送消息都需要和Broker主机进行tcp长连接,但每次发送消息都建立长连接过于浪费主机资源,因此出现了Channel信道这个概念
  • VHost虚拟主机:用来实现资源隔离,不同的业务可以用不同的虚拟主机,只需要用不同的用户与不同的虚拟主机进行绑定,就可以实现此功能。
  • Channel信道:信道实际上是一个虚拟连接,目的是为了减少建立长连接时的资源和时间的消耗
  • Exchange交换机:起到分发消息的作用,生产者的消息将不会直接发送给队列,而是发送到交换机中,交换机将与一个到多个Queue队列有Bingding绑定,当消息发送过来后,由交换机根据相应的规则和对应的绑定分发给对应队列,起到消息分发的作用,减少了生产者调用发送消息的资源浪费
  • Queue队列:用来存储消息,是消费者和生产者的关联纽带,实际上它使用了一个数据库对消息进行存储,只有队头的消息被消费,此消息才会从数据库中删除,在实际运用中,建议一个消费者只从一个队列里拉取消息,这样才能保证消息的消费不出现混乱情况,如果消息积压过多,也可以使用负载均衡进行消费。
  • 消费模型:即消费者消费消息的方式,在RabbitMQ中,提供了两种消费模型,一种为pull,是消费者主动去Broker主机拉取消息,接口为basicGet,一种为push,是Broker主机推送消息,接口为basicConsume。
  • Bingding绑定:将一个Exchange交换机和一个或多个Queue队列绑定起来,每一个从生产者发送来的消息都有特殊标识,Exchange交换机将根据特殊标识和Bingding绑定建立的绑定关系确定如何将消息发送给对应的Queue队列
2)常见交换机类型
  • direct:直连,使用明确的绑定建,适用与业务明确的场景。
  • fanout:广播,无需绑定键,适用于通用类业务消息。
  • topic:主题,使用支持通配符的绑定键,适用于根据业务主题过滤消息的场景。
    通配符:* ,代表一个单词, *.jvm的匹配路由为abc.jvm,def.jvm。abc.def.jvm则不行。
    通配符:#,代表任意单词,jdk.#的匹配路由为任意jdk开头的路由。
  • headers:根据请求头对消息进行分配,不常用。

二、RabbitMQ代码实例

1.原生代码

需要在项目中引入rabbitmq的jar包才能使用相关功能

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

消费端代码

		//创建连接工厂
		ConnectionFactory factory = new ConnectionFactory();
        // 连接IP
        factory.setHost("127.0.0.1");
        // 默认监听端口
        factory.setPort(5672);
        // 虚拟机
        factory.setVirtualHost("/");

        // 设置访问的用户
        factory.setUsername("guest");
        factory.setPassword("guest");
        // 建立连接
        Connection conn = factory.newConnection();
        // 创建消息通道
        Channel channel = conn.createChannel();

        // 声明交换机
        // String exchange(交换机名称)
        //String type(交换机类型)
        // boolean durable(是否持久化)
        //boolean autoDelete(无人连接时是否自动删除)
        // Map<String, Object> arguments
        channel.exchangeDeclare(EXCHANGE_NAME,"direct",false, false, null);

        // 声明队列
        // String queue(队列名称)
        //boolean durable(是否持久化)
        //boolean exclusive(是否排外,一:当连接关闭时该队列是否会自动删除;二:该队列是否是私有的private)
        //boolean autoDelete(无人连接时是否自动删除)
        //Map<String, Object> arguments
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 绑定队列和交换机
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"my.tets");

        // 创建消费者
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String msg = new String(body, "UTF-8");
            }
        };

        // 开始获取消息
        // String queue, boolean autoAck, Consumer callback
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }

生产端代码

		ConnectionFactory factory = new ConnectionFactory();
        // 连接IP
        factory.setHost("127.0.0.1");
        // 连接端口
        factory.setPort(5672);
        // 虚拟机
        factory.setVirtualHost("/");
        // 用户
        factory.setUsername("guest");
        factory.setPassword("guest");

        // 建立连接
        Connection conn = factory.newConnection();
        // 创建消息通道
        Channel channel = conn.createChannel();

        // 发送消息
        String msg = "Hello world, Rabbit MQ";

        // String exchange, String routingKey, BasicProperties props, byte[] body
        channel.basicPublish(EXCHANGE_NAME, "my.tets", null, msg.getBytes());

        channel.close();
        conn.close();

2.springboot中的应用

需要引入jar包

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-amqp</artifactId>
		</dependency>
1)消费端代码

连接rabbitmq,创建交换机和队列,并绑定

@Configuration
@PropertySource("classpath:rabbitmq.properties")
public class RabbitConfig {
    @Value("${firstqueue}")
    private String firstQueue;

    @Value("${secondqueue}")
    private String secondQueue;

    @Value("${thirdqueue}")
    private String thirdQueue;

    @Value("${fourthqueue}")
    private String fourthQueue;

    @Value("${directexchange}")
    private String directExchange;

    @Value("${topicexchange}")
    private String topicExchange;

    @Value("${fanoutexchange}")
    private String fanoutExchange;

    // 创建四个队列
    @Bean("firstQueue")
    public Queue getFirstQueue(){
        return new Queue(firstQueue);
    }

    @Bean("secondQueue")
    public Queue getSecondQueue(){
        return new Queue(secondQueue);
    }

    @Bean("thirdQueue")
    public Queue getThirdQueue(){
        return  new Queue(thirdQueue);
    }

    @Bean("fourthQueue")
    public Queue getFourthQueue(){
        return  new Queue(fourthQueue);
    }

    // 创建三个交换机
    @Bean("directExchange")
    public DirectExchange getDirectExchange(){
        return new DirectExchange(directExchange);
    }

    @Bean("topicExchange")
    public TopicExchange getTopicExchange(){
        return new TopicExchange(topicExchange);
    }

    @Bean("fanoutExchange")
    public FanoutExchange getFanoutExchange(){
        return new FanoutExchange(fanoutExchange);
    }

    // 定义四个绑定关系
    @Bean
    public Binding bindFirst(@Qualifier("firstQueue") Queue queue, @Qualifier("directExchange") DirectExchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("test.one");
    }

    @Bean
    public Binding bindSecond(@Qualifier("secondQueue") Queue queue, @Qualifier("topicExchange") TopicExchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("*.test.*");
    }

    @Bean
    public Binding bindThird(@Qualifier("thirdQueue") Queue queue, @Qualifier("fanoutExchange") FanoutExchange exchange){
        return BindingBuilder.bind(queue).to(exchange);
    }

    @Bean
    public Binding bindFourth(@Qualifier("fourthQueue") Queue queue, @Qualifier("fanoutExchange") FanoutExchange exchange){
        return BindingBuilder.bind(queue).to(exchange);
    }

    /**
     * 在消费端转换JSON消息
     * 监听类都要加上containerFactory属性
     * @param connectionFactory
     * @return
     */
    @Bean
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        factory.setAutoStartup(true);
        return factory;
    }
}

创建消费者,@RabbitListener注解:添加监听对应的队列,可设置多个,用逗号分隔开。

@Component
@PropertySource("classpath:rabbitmq.properties")
@RabbitListener(queues = "${firstqueue}", containerFactory="rabbitListenerContainerFactory")
public class FirstConsumer {

    @RabbitHandler
    public void process(@Payload Merchant merchant){
        System.out.println("First Queue received msg : " + merchant.getName());
    }

}
@Component
@PropertySource("classpath:gupaomq.properties")
@RabbitListener(queues = "${com.gupaoedu.secondqueue}", containerFactory="rabbitListenerContainerFactory")
public class SecondConsumer {
    @RabbitHandler
    public void process(String msgContent,Channel channel, Message message) throws IOException {
        System.out.println("Second Queue received msg : " + msgContent );
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
}
@Component
@PropertySource("classpath:gupaomq.properties")
@RabbitListener(queues = "${com.gupaoedu.thirdqueue}", containerFactory="rabbitListenerContainerFactory")
public class ThirdConsumer {
    @RabbitHandler
    public void process(String msg){
        System.out.println("Third Queue received msg : " + msg);
    }
}
@Component
@PropertySource("classpath:gupaomq.properties")
@RabbitListener(queues = "${com.gupaoedu.fourthqueue}", containerFactory="rabbitListenerContainerFactory")
public class FourthConsumer {
    @RabbitHandler
    public void process(String message) throws IOException {

        System.out.println("Fourth Queue received msg : " + message);
    }
}

其中第二个消费者略有不同,可以主动发送ACK信息。
这样就可以自动创建全部的交换机和队列,并自动建立绑定关系。

2)生产端代码
@Configuration
public class RabbitConfig {
    /**
     * 所有的消息发送都会转换成JSON格式发到交换机
     * @param connectionFactory
     * @return
     */
    @Bean
    public RabbitTemplate amqpTemplate(final ConnectionFactory connectionFactory) {
        final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
        return rabbitTemplate;
    }
}
@Component
@PropertySource("classpath:rabbitmq.properties")
public class RabbitSender {

    @Value("${directexchange}")
    private String directExchange;

    @Value("${topicexchange}")
    private String topicExchange;

    @Value("${fanoutexchange}")
    private String fanoutExchange;

    @Value("${directroutingkey}")
    private String directRoutingKey;

    @Value("${topicroutingkey1}")
    private String topicRoutingKey1;

    @Value("${topicroutingkey2}")
    private String topicRoutingKey2;


    // 自定义的模板,所有的消息都会转换成JSON发送,此为实现了amqp协议的模板
    @Autowired
    AmqpTemplate amqpTemplate;
    //也可以使用此rabbitmq专用模板,但仅支持rabbitmq
    @Autowired
    RabbitTemplate rabbitTemplate;

    public void send() throws JsonProcessingException {
        Merchant merchant =  new Merchant(1001,"a direct msg : 中原镖局","汉中省解放路266号");
        amqpTemplate.convertAndSend(directExchange,directRoutingKey, merchant);

        amqpTemplate.convertAndSend(topicExchange,topicRoutingKey1, "a topic msg : shanghai.test.teacher");
        amqpTemplate.convertAndSend(topicExchange,topicRoutingKey2, "a topic msg : changsha.test.student");

        // 发送JSON字符串
        ObjectMapper mapper = new ObjectMapper();
        String json = mapper.writeValueAsString(merchant);
        System.out.println(json);
        amqpTemplate.convertAndSend(fanoutExchange,"", json);
    }
}

3.延时推送消息的实现

1)方案一:死信队列

死信队列,rabbitmq-delayed-message-exchange,可以用来实现延时推送。
当一个消息始终没人消费,等到过期之后,就会进入死信交换机,之后进入死信队列,若是有消费者监听死信队列,则可以获得此过时消息,即实现延时推送,延时时间为过期时间。

死信为何产生:
  • 一个消息,过期之后,即为死信。
  • 当队列里的消息存储空间或者存储个数满了,排在队列中队头的消息(即此队列中第一个进入的消息)将成为死信
  • 消费者拒绝此消息,而且没有设置重回队列
生产者代码示例:
		// 设置属性,消息10秒钟过期
        AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
                .deliveryMode(2) // 持久化消息
                .contentEncoding("UTF-8")
                .expiration("10000") // TTL
                .build();

        // 发送消息
        channel.basicPublish("", "USE_QUEUE", properties, msg.getBytes());

此处交换机命名为空,因为RabbitMQ含有一个默认为空名字的交换机,它会默认找到一个和后面的routingKey内容一样名字的队列进行连接。

消费者代码示例:
		// 声明死信交换机
        channel.exchangeDeclare("DEAD_LETTER_EXCHANGE","topic", false, false, false, null);
        // 声明死信队列
        channel.queueDeclare("DEAD_LETTER_QUEUE", false, false, false, null);
        // 绑定
        channel.queueBind("DEAD_LETTER_QUEUE","DEAD_LETTER_EXCHANGE","#");

        // 创建消费者
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String msg = new String(body, "UTF-8");
            }
        };

        // 开始获取消息
        // String queue, boolean autoAck, Consumer callback
        channel.basicConsume("DEAD_LETTER_QUEUE", true, consumer);

绑定键为#,意味着是无条件绑定,任意都可以。
这样就实现完成了。
但是这样的延时发送有一个问题,消息本身可以设置一个过期时间,队列也可以设置一个统一的过期时间,这两个时间同时存在时,是以少的那个时间为准,因此每一个不同的延时发送时间都需要单独创建一个队列,这非常麻烦。

2)方案二:Delayed Message 插件

使用Delayed Message 插件后(需要单独下载插件)即可声明一个名为x-delayed-messgaed的特殊类型交换机,该交换机可以直接实现延时发送功能。

生产者代码示例
		// 延迟的间隔时间,目标时刻减去当前时刻
        Map<String, Object> headers = new HashMap<String, Object>();
        headers.put("x-delay", delayTime.getTime() - now.getTime());

        AMQP.BasicProperties.Builder props = new AMQP.BasicProperties.Builder()
                .headers(headers);
        channel.basicPublish("DELAY_EXCHANGE", "DELAY_KEY", props.build(),
                msg.getBytes());
消费者代码示例
		// 声明x-delayed-message类型的exchange
        Map<String, Object> argss = new HashMap<String, Object>();
        argss.put("x-delayed-type", "direct");
        channel.exchangeDeclare("DELAY_EXCHANGE", "x-delayed-message", false,
                false, argss);

        // 声明队列
        channel.queueDeclare("DELAY_QUEUE", false,false,false,null);

        // 绑定交换机与队列
        channel.queueBind("DELAY_QUEUE", "DELAY_EXCHANGE", "DELAY_KEY");

        // 创建消费者
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String msg = new String(body, "UTF-8");
                SimpleDateFormat sf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
                System.out.println("收到消息:[" + msg + "]\n接收时间:" +sf.format(new Date()));
            }
        };

        // 开始获取消息
        // String queue, boolean autoAck, Consumer callback
        channel.basicConsume("DELAY_QUEUE", true, consumer);

4.消息控制

假如消息存满了,应该如何呢?最简单的方式当然是重启,但显然这不是一个可以经常使用的方法。

服务端控制

一般情况下,我们在服务端可以通过三种设置来对消息存储的数量进行控制,这个控制与死信产生的机制也有关系

  1. 队列长度:x-max-length或x-max-length-bytes
  2. 内存大小:Conn vm_memory_high_watermark
  3. 磁盘空间:disk_free_limit.relative或disk_free_limit.absolute
消费端控制

prefetch count预取数量,即限制消费端同时处理消息的数量

		// 声明队列(默认交换机AMQP default,Direct)
        // String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 创建消费者,并接收消息
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String msg = new String(body, "UTF-8");
                channel.basicAck(envelope.getDeliveryTag(), true);
            }
        };

        //非自动确认消息的前提下,如果一定数目的消息(通过基于consume或者channel设置Qos的值)未被确认前,不进行消费新的消息。
        channel.basicQos(2);
        channel.basicConsume(QUEUE_NAME, false, consumer);

basicQos方法控制了同时消费消息的最大数量,basicAck方法消费完消息后向服务端发送ACK,假如消费端它没有发送ACK数量等于最大消费消息数量,服务端就不会继续发送消息给它,这样就实现了对消费端的控制,直到它返回一条ACK为止。
除此之外,当有多个消费端消费消息时,一定是遵循“能者多劳”,处理速度快的将会获得更多的消息以提升整体性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值