RabbitMQ(2)

订阅模型 -Direct

有选择性的接收消息

在订阅模式中,生产者发布消息,所有消费者都可以获取所有消息。

在路由模式中,我们将添加一个功能 - 我们将只能订阅一部分消息。 例如,我们只能将重要的错误消息引导到日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有日志消息。

但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。

在Direct模型下,队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)

消息的发送方在向Exchange发送消息时,也必须指定消息的routing key。

image.png

P:生产者,向Exchange发送消息,发送消息时,会指定一个 routing key。

X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列

C1:消费者,其所在队列指定了需要routing key 为 error 的消息

C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息

代码demo:

/**
 * 生产者,模拟为商品服务
 */
public class Send {
    private final static String EXCHANGE_NAME = "direct_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明exchange,指定类型为direct
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");
        // 消息内容
        String message = "商品删除了, id = 1001";
        // 发送消息,并且指定routing key 为:delete,代表删除
        channel.basicPublish(EXCHANGE_NAME, "delete", null, message.getBytes());
        System.out.println(" [商品服务:] Sent '" + message + "'");

        channel.close();
        connection.close();
    }
}
/**
 * 消费者1
 */
public class Recv {
    private final static String QUEUE_NAME = "direct_exchange_queue_1";
    private final static String EXCHANGE_NAME = "direct_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        
        // 绑定队列到交换机,同时指定需要订阅的routing key。假设此处需要update和delete消息
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");

        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                    byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者1] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}
/**
 * 消费者2
 */
public class Recv2 {
    private final static String QUEUE_NAME = "direct_exchange_queue_2";
    private final static String EXCHANGE_NAME = "direct_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        
        // 绑定队列到交换机,同时指定需要订阅的routing key。订阅 insert、update、delete
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "insert");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");

        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                    byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者2] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

最后启动,两个消费者均会收到delete的消息,如果在发送insert的消息,就只有带‘insert’的key接收的消息。起到了Route的作用

订阅模型 -Topic

Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符!起到了一个匹配的作用。

Routingkey`一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert

 通配符规则:

         #:匹配一个或多个词

         *:匹配不多不少恰好1个词

举例:

         audit.#:能够匹配audit.irs.corporate 或者 audit.irs

         audit.*:只能匹配audit.irs

demo实现:

public class Send {
    private final static String EXCHANGE_NAME = "topic_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明exchange,指定类型为topic
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");
        // 消息内容
        String message = "新增商品 : id = 1001";
        // 发送消息,并且指定routing key 为:insert ,代表新增商品
        channel.basicPublish(EXCHANGE_NAME, "item.insert", null, message.getBytes());
        System.out.println(" [商品服务:] Sent '" + message + "'");

        channel.close();
        connection.close();
    }
}
public class Recv {
    private final static String QUEUE_NAME = "topic_exchange_queue_1";
    private final static String EXCHANGE_NAME = "topic_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        
        // 绑定队列到交换机,同时指定需要订阅的routing key。需要 update、delete
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.update");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.delete");

        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                    byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者1] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}
public class Recv2 {
    private final static String QUEUE_NAME = "topic_exchange_queue_2";
    private final static String EXCHANGE_NAME = "topic_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        
        // 绑定队列到交换机,同时指定需要订阅的routing key。订阅 insert、update、delete
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.*");

        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                    byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者2] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

运行可看出,生产者发送了item.insert的消息,消费者1只绑定了‘item.update’和‘item.delete’,所以接受不到生产者发送的消息。消费者2绑定了item.*所以可匹配一个或多个字段,因此可以接收到生产者所发出的消息。

问题一:如果RabbitMQ服务挂掉了怎么办?

可以采用持久化,包括容器持久化及消息持久化,

 //如在声明交换机时,增加一个参数设置为true。
channel.exchangeDeclare(EXCHANGE_NAME, "topic","ture");

    队列:将第二个参数durable改为true

channel.queueDeclare(QUEUE_NAME, true, false, false, null);
//发布消息,将第三个参数的null属性改为MessageProperties.PRESISTENT_TEXT_PLAIN
channel.basicPublish(EXCHANGE_NAME, "item.insert", MessageProperties.PRESISTENT_TEXT_PLAIN, message.getBytes());

生产者确认机制:RabbitMQ向生产者发送一条消息

解决消息丢失?

1)ACK消费者确认

2)持久化

3)发送消息前,将消息持久化到数据库,并记录消息状态(可靠消息服务)

4)生产者确认。

幂等性(同一接口被重复执行,其结果一致)

Spring AMQP

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
spring:
  rabbitmq:
    host: 39.108.254.46
    username: kf
    password: kf
    virtual-host: /kf
    ##端口默认15672
@Component
public class Listener {

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "spring.test.queue", durable = "true"),
            exchange = @Exchange(
                    value = "spring.test.exchange",
                    ignoreDeclarationExceptions = "true",
                    type = ExchangeTypes.TOPIC
            ),
            key = {"#.#"}))
    public void listen(String msg){
        System.out.println("接收到消息:" + msg);
    }
}

- `@Componet`:类上的注解,注册到Spring容器

- `@RabbitListener`:方法上的注解,声明这个方法是一个消费者方法,需要指定下面的属性:

  - `bindings`:指定绑定关系,可以有多个。值是`@QueueBinding`的数组。`@QueueBinding`包含下面属性:

    - `value`:这个消费者关联的队列。值是`@Queue`,代表一个队列

    - `exchange`:队列所绑定的交换机,值是`@Exchange`类型

    - `key`:队列和交换机绑定的`RoutingKey`

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class MqDemo {

    @Autowired
    private AmqpTemplate amqpTemplate;
    //测试消息发送
    @Test
    public void testSend() throws InterruptedException {
        String msg = "hello, Spring boot amqp";
        this.amqpTemplate.convertAndSend("spring.test.exchange","a.b", msg);
        // 等待10秒后再结束
        Thread.sleep(10000);
    }
}

SpringBoot 配置使用

1.配置文件

spring:
  rabbitmq:
    host: 192.168.0.1
    port: 5672
    username: admin
    password: public
    #    virtual-host: GHost
    virtual-host: /

    # 确认消息已发送到交换机
    publisher-confirms: true
    # 消息发送失败后返回
    publisher-returns: true

    listener:
      # 默认配置是simple
      type: simple
      simple:
        # 手动ack Acknowledge mode of container. auto none
        acknowledge-mode: manual
        # Maximum number of unacknowledged messages that can be outstanding at each consumer.
        prefetch: 3

2.RabbitTemplate配置

@Configuration
public class RabbitTemplateConfig {

    @Bean
    public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory){
        RabbitTemplate rabbitTemplate=new RabbitTemplate();
        rabbitTemplate.setConnectionFactory(connectionFactory);

        rabbitTemplate.setMandatory(true);

        //推送到server回调
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) ->
                log.info("ConfirmCallback correlationData:{},ack:{},cause:{}",correlationData,ack,cause));

        //消息返回给生产者, 路由不到队列时返回给发送者  先returnCallback,再 confirmCallback
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            log.info("ReturnCallback message:{},replyCode:{},replyText:{},exchange:{},routingKey:{}",
                    message,replyCode,replyText,exchange,routingKey);
        });
        return rabbitTemplate;
    }
}

3.消息发送

@RestController
@RequestMapping("mqTest1")
public class MqTest1Controller {

    @Autowired
    RabbitTemplate rabbitTemplate;
    
    @GetMapping("streamPush")
    public String streamPush(){
        rabbitTemplate.convertAndSend(HeadersRabbitConfig.HEADERS_EXCHANGE_NAME,"","I am a msg".getBytes(),message -> {
            Map<String,Object> headers=message.getMessageProperties().getHeaders();
            headers.put("format","pdf");
            //headers.put("type","report");
            headers.put("han","shaohua");
            return message;
        });
        return "hehe";
    }
}

4.消息监听

@Component
@RabbitListener(queues = {TopicRabbitConfig.TOPIC_QUEUE_NAME})
public class TopicHandler2 {

    @RabbitHandler
    public void processByteMsg(@Headers MessageHeaders headers, byte[] msgPayload) {
        String msg = new String(msgPayload);
        log.info("TopicHandler2收到消息:{},exchange:{},routingKey:{},queue:{}",
                msg, headers.get("amqp_receivedExchange"),
                headers.get("amqp_receivedRoutingKey"),
                headers.get("amqp_consumerQueue"));
    }

    @RabbitHandler
    public void processStringMsg(@Headers MessageHeaders headers, Channel channel, String msg,
                                 @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag,
                                 @Header(AmqpHeaders.CONSUMER_TAG) String consumerTag) throws Exception {
        log.info("TopicHandler2收到消息:{},exchange:{},routingKey:{},queue:{}",
                msg, headers.get("amqp_receivedExchange"),
                headers.get("amqp_receivedRoutingKey"),
                headers.get("amqp_consumerQueue"));
        channel.basicAck(deliveryTag, false);
        //拒绝消息, requeue为true会重入队列, 消息就会一直接受
        //channel.basicNack(deliveryTag,false,false);
        //channel.basicCancel(consumerTag);
    }

}

5.交换机绑定队列关系

5.1  Fanout

Fanout类型的exchange会忽略routingKey,所以配置中无法设置routingKey

@Configuration
public class FanoutRabbitConfig {

    public static final String FANOUT_QUEUE_NAME="htFanoutQueue";
    public static final String FANOUT_EXCHANGE_NAME="htFanoutExchange";
    
    //发送消息时只需要创建exchange即可
    @Bean
    FanoutExchange fanoutExchange(){
        return new FanoutExchange(FANOUT_EXCHANGE_NAME);
    }

    @Bean
    public Queue fanoutQueue(){
        //durable:true mq服务器重启后仍然存在 false重启后队列自动删除
        return new Queue(FANOUT_QUEUE_NAME,false);
    }

    /**
     * 队列绑定fanout类型的exchange是无法设置routingKey
     * @return
     */
    @Bean
    Binding bindingFanout(){
        return BindingBuilder.bind(fanoutQueue()).to(fanoutExchange());
    }
}

5.2 direct

 

/**
 * direct类型exchange routingKey匹配
 */
@Configuration
public class DirectRabbitConfig {

    static final String DIRECT_QUEUE_NAME="htDirectQueue";
    public static final String DIRECT_EXCHANGE_NAME="htDirectExchange";
    public static final String ROUTING_KEY="htDirectRouting";

    //发送消息是只需要创建exchange即可
    @Bean
    DirectExchange directExchange(){
        return new DirectExchange(DIRECT_EXCHANGE_NAME);
    }

    //接收消息是需要声明队列,然后通过routingKey将队列与exchange绑定
    @Bean
    public Queue directQueue(){
        return new Queue(DIRECT_QUEUE_NAME,false);
    }

    @Bean
    Binding bindingDirect(){
        return BindingBuilder.bind(directQueue()).to(directExchange()).with(ROUTING_KEY);
    }
}

3.Topic

@Configuration
public class TopicRabbitConfig {

    static final String TOPIC_QUEUE_NAME="htTopicQueue";
    static final String TOPIC_QUEUE1_NAME="htTopicQueue1";

    public static final String TOPIC_EXCHANGE_NAME="htTopicExchange";
    /**
     * #匹配0个或多个单词,*匹配一个单词,即使一个的多个routingKey都匹配上了,但该队列只收到一次消息
     */
    public static final String TOPIC="htTopicRouting.*";
    public static final String TOPIC1="htTopicRouting.topic1";


    //发送消息是只需要创建exchange即可
    @Bean
    TopicExchange topicExchange(){
        return new TopicExchange(TOPIC_EXCHANGE_NAME);
    }

    //接收消息是需要声明队列,然后通过routingKey将队列与exchange绑定

    //带通配符的topic
    @Bean
    public Queue topicQueue(){
        //durable:true mq服务器重启后让然存在 false重启后队列自动删除
        return new Queue(TOPIC_QUEUE_NAME,false);
    }
    @Bean
    Binding bindingTopic(){
        return BindingBuilder.bind(topicQueue()).to(topicExchange()).with(TOPIC);
    }

    //topic1
    @Bean
    public Queue topicQueue1(){
        return new Queue(TOPIC_QUEUE1_NAME,false);
    }
    @Bean
    Binding bindingTopic1(){
        return BindingBuilder.bind(topicQueue1()).to(topicExchange()).with(TOPIC1);
    }
}

4.header

@Configuration
public class HeadersRabbitConfig {

    static final String HEADERS_QUEUE_NAME="htHeadersQueue";
    public static final String HEADERS_EXCHANGE_NAME="htHeadersExchange";

    //发送消息是只需要创建exchange即可
    @Bean
    HeadersExchange headersExchange(){
        return new HeadersExchange(HEADERS_EXCHANGE_NAME);
    }

    @Bean
    public Queue headersQueue(){
        return new Queue(HEADERS_QUEUE_NAME,false);
    }

    @Bean
    Binding bindingDirect(){
        Map<String, Object> arguments = new HashMap<>();
        arguments.put("format",  "pdf");
        arguments.put("type",  "report");
        //x-match为any
        return BindingBuilder.bind(headersQueue()).to(headersExchange()).whereAny(arguments).match();
    }

    @Bean
    Binding bindingAllDirect(){
        Map<String, Object> arguments = new HashMap<>();
        arguments.put("format",  "pdf");
        arguments.put("type",  "report");
        //x-match为all
        return BindingBuilder.bind(headersQueue()).to(headersExchange()).whereAll(arguments).match();
    }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值