化身天使的博客

11.spring消息队列

1. RabbitMq

docker安装

  • docker pull rabbitmq:management # 带管理界面, 管理界面在15672端口
  • docker pull rabbitmq #5672 端口

docker-compose

  rabbitMq:
    image: rabbitmq:management
    container_name: rabbitMq
    ports:
      - "5672:5672"
      - "15672:15672"
    volumes:
      - /home/young/app/rabbitmq/data:/var/lib/rabbitmq

docker启动后

  • 访问 http://127.0.0.1:15672
  • 帐号/密码: guest
  • 在admin-> users 处添加用户或修改密码,修改后重新登录, 不要在弹出的框里输入
  • 在admin菜单添加虚拟主机Virtual Host

1.1. 配置

依赖

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

配置文件

spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=abc123
spring.rabbitmq.virtualHost=rabbitMqHost1   #虚拟主机名字

配置类

在发送对象时,对象会被序列化成字节数组,若要反序列化对象,需要自定义 MessageConverter
在config目录创建RabbitMQConfig
下面提供了两种方式

  1. 基于 默认的SimpleMessageConverter
  2. 基于 Jackson
  • 生产者需要配置 rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());

@Configuration
public class RabbitMQConfig {
    
    // 1. 基于 默认的SimpleMessageConverter 
    @Bean
    public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new MessageConverter() {
            @Override
            public Message toMessage(Object object, MessageProperties messageProperties) throws MessageConversionException {
                return null;
            }
 
            @Override
            public Object fromMessage(Message message) throws MessageConversionException {
                try(ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(message.getBody()))){
                    return (User)ois.readObject();
                }catch (Exception e){
                    e.printStackTrace();
                    return null;
                }
            }
        });
 
        return factory;

    }
    // 2.使用 Jackson 序列化器
    @Bean
    public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        return factory;
    }
}

1.2. 结构

1.2.1. 生产者

  • 注入
    • @Autowired
      private RabbitTemplate rabbitTemplate;
  • 发送
    • 无交换机
      • rabbitTemplate.convertAndSend("routingKey","消息内容")
      • rabbitTemplate.convertAndSend("消费者name","消息内容")
    • 有交换机
      • rabbitTemplate.convertAndSend("exchange","routingKey","消息内容")
      • rabbitTemplate.convertAndSend("交换机name","消费者name","消息内容")
  • 序列化
    • 设置序列化器
      • rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());

1.2.2. 消费者

注解

  • RabbitHandler
    • 用在方法上,加了这个注解就成为了消费者
  • RabbitListener 配置消费者参数,可用在类或方法上
    • queues 监听的队列名称
    • queuesToDeclare 定义消费者名称
      • queuesToDeclare= @Queue(name = "消费者name") // routingKey name= 可省略
    • exchange 交换机
      • = @Exchange(value = "topic.exchange",durable = "true",type = "topic")
        参数注解
  • Payload
    • 请求体
    • public void processMessage1(@Payload String body) {
  • Headers
    • 请求头
    • public void processMessage1(@Headers Map<String,Object> headers) {
    • 单个 @Header String token

1.2.3. broker

创建队列

  • 队列名字 写在Durable或nonDurable里
    private final static String QUEUE1="queue1";
    @Bean(QUEUE1)
    public Queue queue1() {
        return QueueBuilder
                .nonDurable(QUEUE1)  //不持久化,重启时不保存该队列,注意队列名称在这里或下面定义
                //.Durable("或者队列名称填这里")   //持久化, 队列和消息会写入磁盘
                .expires(60*1000) //队列存活时间,单位毫秒, 超过该时间删除队列
                .ttl(30*1000) //队列中消息的存活时间,超过该时间从队列删除
                .autoDelete() //没有消费者连接时,自动删除该队列
                .exclusive() // 将队列与第一个连接它的消费者绑定,只传递消息给该消费者
                
                .maxPriority(10) //最高优先级
                .maxLength(10)  // 最大消息数
                .maxLengthBytes(1024) //每条消息最大字节数
                
                .deadLetterExchange("deadExchange")  // 关联死信交换机
                .deadLetterRoutingKey("deadQueue") //关联死信队列
                .build();
    }

创建交换机

  • 交换机类型
    • topicExchange , directExchange , fanoutExchange , headersExchange
    @Bean("exchange1")
    public Exchange exchange1(){
        return ExchangeBuilder
                .topicExchange("topicExchange1") // topic交换机,名字topicExchange1
                .delayed()  // 创建延迟交换机
//                .autoDelete() // 自动删除
//                .durable(false) // 缺省持久化,指定false非持久化
                .build();
    }

绑定队列

  • 形参 queue2, exchange1必须对应创建队列和交换机时Bean里指定的名字,
    • 上面定义了 常量 QUEUE1="queue1", 这里要用queue1,而不能用QUEUE1
  • bind 队列
  • to 交换机
  • with 设置路由规则
    @Bean
    public Binding binding2(Queue queue1,Exchange exchange1 ) {
        return    BindingBuilder
                .bind(queue1)
                .to(exchange1)
                .with("#.mail").noargs();
    }

1.3. 消息收发

生产者 -> 交换机 -> 队列 -> 消费者

  • 交换机和他的队列组成一个虚拟主机, 虚拟主机在mq的服务器上
  • 简单模式和工作队列模式不涉及交换机

1.3.1. 简单模式

  • 只有一个生产者,一个消费者,不用定义交换机和队列
  • 消费者定义自己的路由键,生产者发送消息时指定消费者的路由键

创建消费者类 TestConsumer.java

  • 被 @RabbitHandler 注解的方法,接收的参数即是消息内容
  • @RabbitListener, 定义消费者名字,交换机/队列等
@Slf4j
@Component
public class TestConsumer{

    @RabbitHandler
    @RabbitListener(queuesToDeclare= @Queue("消费者name))
    public void process(String msg){  //接收消息
        log.debug("消费者接收到消息内容:{}",msg)
    }

}

创建生产者类 TestPubliser.java

  • 主要就这一句,发送消息给指定的消费者rabbitTemplate.convertAndSend("消费者","消息")
@Component
public class TestPubliser{

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void publiser(){  
        rabbitTemplate.convertAndSend("消费者name","消息内容")
    }

}

测试类
建个控制类,在控制层测吧,下面这样直接测,报null了

  • 调用方法要抛出异常
@SpringBootTest
public class RabbitTest
    @Autowired
    private TestPubliser tPubliser;

    @Test
    public void test() throws InterruptedException {
        tPubliser.publiser();
    }

传参为实体类

新建一个实体类如 User.java

  • 需要实现Serializable接口
public class User implements Serializable{}

消费者接收参数的类型和生产者发送的消息类型都改为User

1.3.2. 工作队列模式

  • 一个生产者,多个消费者,不涉及交换机
  • 多个消费者轮流从队列读取消息
  • 跟一对一模式相比,仅仅是创建多个消费者,并起同样的名字

流程

  • 新建配置类
    • 创建交换机
    • 创建队列
    • 绑定交换机和队列
  • 创建消费者,分别监听各自的队列
  • 创建生产者

1.3.3. Direct路由模式

路由键完全匹配

1.3.4. Topic 发布订阅模式

路由键通配符匹配

  • 生产者端定义交换机和队列, 并进行绑定, 绑定时设置路由键
  • 发送消息时生产者指定交换机和路由键,根据路由键到达指定队列
  • 消费者监听队列
    通配符
* 匹配一个词(有且只有一个)
    *.abc
    ab.abc  匹配
    a.b.abc 不匹配
# 0或多个词

定义交换机,队列,在消费者config目录创建 RabbitMqConfig1


import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class RabbitMqConfig1 {
    //1.1 定义交换机
    @Bean
    public Exchange exchange1(){
        return ExchangeBuilder
                .topicExchange("topicExchange1") // topic交换机,名字topicExchange1
                .durable(true) // durable=true,持久化
                .build();
    }
    //2.1 定义队列1
    @Bean("queue1") //用于绑定时的传参
    public Queue queue1(){
        return QueueBuilder
                .durable(true) //持久化
//                .ttl(60*1000)  // 该队列内消息的存活时间,单位毫秒, 超过该时间,消息将从队列删除
                .build();
    }

    //2.2 定义队列2
    @Bean("queue2")
    public Queue queue2(){
        Queue queue = new Queue("queue");
//        queue.
        return queue;

    }

    /**3.1 定义绑定关系
     *
     * @param queue1 入参要和创建时的Bean 里的名字一致
     * @param e1
     * @return
     */
    @Bean
    public Binding binding1(Queue queue1, Exchange e1 ) {
        return    BindingBuilder
                .bind(queue1).to(e1).with("msg.*") // 绑定关系,queue1,交换机e1,路由key:msg.*
                .noargs();
    }
    @Bean
    public Binding binding2(Queue queue2,Exchange e1 ) {
        return    BindingBuilder
                .bind(queue2).to(e1).with("#.mail").noargs();
    }
}

创建消费者

@Component
@RabbitListener(queues = "queue1")  //要监听的队列
public class TopicConsumer {
    @RabbitHandler  //处理方法,根据传参确定处理方法
    public void receive(String msg) {
        System.out.println("TopicConsumer 接收到消息:" + msg);
    }
    @RabbitHandler
    public void receive(Map msg) {
        System.out.println("TopicConsumer 接收到消息:" + msg);
    }
}

定义生产者

    @GetMapping("/pub/{word}")
    public ResultBody pub1(@PathVariable String word){
        rabbitTemplate.convertAndSend("exchange1",word+".mail","hello msg "+word);
        // 交换机名称, 路由键, 消息内容
        return ResultBody.success();
    }
    @GetMapping("/pub2")
    public ResultBody pub2(@RequestParam String word){
        rabbitTemplate.convertAndSend("exchange1","msg."+word,"hello msg "+word);
        return ResultBody.success();
    }

1.3.5. Fanout 广播模式

广播,发给交换机绑定的所有队列

1.3.6. headers

1.3.7. 死信队列

当配置了死信队列时,被删除的消息都会进入死信队列,如:

  • 普通队列已满,新来的消息进入死信队列
  • 消息超过存活时间
  • 拒签并且不重回队列

流程

  • 创建死信交换机
  • 创建死信队列
  • 绑定1,2
  • 创建交换机
  • 创建队列,并关联死信交换机、队列
  • 绑定4,5
    //0.1 死信交换机
    @Bean("deadExchange")
    public Exchange deathExchange() {
        return ExchangeBuilder
                .topicExchange("deadExchange")
                .durable(true)
                .build();

    }
    //0.2 死信队列
    @Bean("deadQueue")
    public Queue deadQueue() {
        return QueueBuilder
                .durable()
                .build();
    }
    // 0.3 绑定死信组, 
    @Bean
    public Binding bindDead(Exchange deadExchange,Queue deadQueue){
        return BindingBuilder
                .bind(deadQueue)
                .to(deadExchange)
                .with("dead.#") // 接收所有dead开头的消息
                .noargs();

    }

        //1.1 定义交换机
    @Bean
    public Exchange exchange1(){
        return ExchangeBuilder
                .topicExchange("topicExchange1") // topic交换机,名字topicExchange1
                .durable(true) // durable=true,持久化
                .build();
    }

        // 1.2 定义队列三,关联死信队列
    @Bean("queue3")
    public Queue queue3() {
        return QueueBuilder
                .durable()
                .deadLetterExchange("deadExchange")  // 关联死信交换机
                .deadLetterRoutingKey("deadQueue") //关联死信队列
                .build();
    }
    // 1.3 绑定
    @Bean
    public  Binding binding3(Exchange e1,Queue queue3){
        return BindingBuilder
                .bind(queue3).to(e1).with("#.#").noargs();
    }

1.3.8. 延迟队列

使用死信队列实现

实现机制:

  • 创建一个死信队列
  • 设置一个普通队列的消息过期时间为1分钟,关联死信队列
  • 生产者发送一条消息,设置超时时间一分钟
  • 一分钟后该条消息转移到死信队列
  • 创建一个消费者绑定该死信队列
  • 由于队列创建时,过期时间就固定了,因此如果有多个不同延时的需求
    • 在convertAndSend里,第四个参数传入回调函数,同时队列的设置里,去掉ttl
    (msg)->{
              //发送消息的延时时长
              msg.getMessageProperties().setExpiration(ttlTime);
              return msg;
          }
    

使用插件实现

安装插件,docker安装

创建,实际上是创建了延迟交换机,只在创建交换机时有差别,创建延迟交换机,队列和绑定都和普通队列相同

  • 方式一
    @Bean("exchange1")
    public Exchange exchange1(){
        return ExchangeBuilder
                .topicExchange("topicExchange1") // topic交换机,名字topicExchange1
                .delayed()  // 创建延迟交换机
                .build();
    }
  • 方式二
    @Bean("delayExchange")
    public Exchange expireExchange() {
        HashMap<String, Object> exchange = new HashMap<>();
        exchange.put("x-delayed-type","topic"); // topic 类型的延迟交换机

        return new CustomExchange("delayExchange","x-delayed-message",true,false,exchange);
                /**
         * 参数1:交换机名称
         * 参数2:类型必须是 x-delayed-message
         * 参数3:是否持久化
         * 参数4:是否自动删除(当最后一个消费者断开连接之后队列是否自动被删除)
         * 参数5:自定义交换机的 HashMap
         */
    }

1.4. 消息确认

1.4.1. confirm, Return

  • confirm 交换机确认收到消息
    • 模式:生产者发送消息后, 服务器要给客户端发一条确认消息
    • 解决的问题:应对场景,生产者发出的消息,服务器没收到
  • Return 队列确认收到消息
    • 模式:配置一个消费者专门处理目的地错误的消息
    • 解决的问题:生产者的消息发送到了不存在的路由,

配置文件, 在生产者配置

  • publisher-confirm-type confirm配置
    • simple 消息发送成功失败都会调用
    • correlated 消息发送成功调用
  • publisher-return return配置
spring.rabbitmq.publisher-confirm-type=simple #  生产者 交换机确认收到消息 
spring.rabbitmq.publisher-return=true   #生产者 队列确认收到消息

在config目录 编写


@Component
public class MqMsgConfirmReturn implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback  {

    @Resource
    private RabbitTemplate rabbitTemplate;

    @PostConstructor
    public void init() {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
    }
    /**
     * 监听交换机消息
     * @param correlationData 消息ID
     * @param ack 是否成功
     * @param cause 原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        System.out.println("confirm: " + correlationData + " ack: " + ack + " cause: " + cause);
    }

    /**
     * 分发到队列失败时执行
     * @param message 消息内容
     * @param replyCode   返回码
     * @param replyText 失败原因
     * @param exchange 目标交换机
     * @param routingKey 目标路由
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        System.out.println("returnedMessage: " + message + " replyCode: " + replyCode + " replyText: " + replyText + " exchange: " + exchange + " routingKey: " + routingKey);
    }
}

1.4.2. ACK机制 消费者端

  • 默认情况下,消费者接收消息后,无论是否处理成功,消息都会从队列中移出,导致失败的消息无法重新处理
  • 开启ack模式:
    • 消费者处理消息成功则进行签收,消息从队列移出
    • 消费者处理消息失败则拒收,消息退回队列

配置文件,在消费者配置,开启手工模式,并指定每次拉取消息数量

spring.rabbitmq.listener.simple.acknowledge-mode=manual 
spring.rabbitmq.listener.simple.prefetch=1  #每次拉取消息数量

创建消息者类, 投递方式

  • basicAck():签收
  • basicNack(): 拒收
  • basicReject():拒收,相比二少了第二个参数批量签收

@Component
public class TopicAckConsumer {
    @RabbitListerner(queues="queue2")
    public void consume(Message message, Channel channel) {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();//获取投递次数,每投递一次,deliveryTag加1
        byte[] body = message.getBody();  // 获取消息内容
        try{
            //对象反序列化为map
//            ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(body));
//            Map<String,String> msgMap = (Map<String,String>) objectInputStream.readObject();
//            objectInputStream.close();

            channel.basicAck(deliveryTag,true) ; //确认签收, 投递序号,是否一次签收多条
        }catch(Exception e){

            channel.basicNack(deliveryTag,true,true); //拒绝签收, 投递序号,是否一次签收多条,是否退回队列
            log.warn(":{}",e);
        }
        System.out.println("TopicAckConsumer: " + message);
    }
}

1.5. 消息属性

1.5.1. 消息内容

1.5.2. 过期时间

在队列中定义

  • 该队列内所有消息的过期时间
  • ttl 属性
    • 类型 long
    • 单位 毫秒
    @Bean("queue1") //用于绑定时的传参
    public Queue queue1(){
        return QueueBuilder
                .durable(true) //持久化
                .ttl(60*1000)  // 该队列内消息的存活时间,单位毫秒, 超过该时间,消息将从队列删除
                .build();
    }

在生产者发送消息时定义

  • 每条消息到达队首时,判断是否过期,过期则删除
  • 如果同时指定了队列过期时间,这两个过期时间哪个短用哪个
  • 属性
    • 类型 字符串
    • 单位 毫秒
        // 定义消息属性
        MessageProperties messageProperties = new MessageProperties();
        // 设置消息存活时间
        messageProperties.setExpiration("1000");
        // 创建消息对象
        Message message = new Message("消息内容".getBytes(StandardCharsets.UTF_8), messageProperties);

        rabbitTemplate.convertAndSend("exchange", "route", message);

1.5.3. 优先级

在队列中设置该队列的最高优先级,这会是发送消息时的最高优先级

    @Bean("queue1") 
    public Queue queue1(){
        return QueueBuilder
                .maxPriority(10) // 最高优先级
                .build();
    }

生产者发消息时指定优先级

        MessageProperties messagePropertie = new MessageProperties();
        messagePropertie.setPriority(10);  // 设置为该队列中的最高优先级10
        Message message = new Message(msg.getBytes(), messagePropertie); // byte[] ,MessageProperties
        rabbitTemplate.convertAndSend("exchange1", "msgword", message);

多次投递失败的,加入死信队列

设置默认队列,

posted @ 2024-01-25 19:52  化身天使  阅读(54)  评论(0)    收藏  举报