RabbitMQ入门教程

177 阅读11分钟

1.为什么会有这个

异步

为了提升系统的性能,同步变异步。

以前调用

是  调用接口 (5s)--》1方法(5s)-》2方法(5s)-》3方法(5s)。 ==20s

异步是

   调用接口(5s)   --》1方法(5s)
                   --》2方法(5s)
                   --》3方法(5s)  ==10s
   

消息中间件异步

    调用接口(5s)-->消息中间件(1s)--》1方法(5s)
                                   --》2方法(5s)
                                   --》3方法(5s)  --》6s
   

一对比就可以看出消息中间件的异步更加快捷

解耦

以下订单减库存为例,下完订单就要调用减库存的方法,如果要进行系统升级,下订单的参数改变,那么减库存的方法也得改,麻烦。 下完订单,将消息发给消息中间件,不用关心别的接口设计成什么样。 减库存自己去取消息,自己减库存就ok。

削峰

以秒杀举例,当我们大量请求进来以后,我们可以给前端返回秒杀成功,然后,将消息发给消息中间件,剩下的那些业务去订阅消息中间件的秒杀请求,挨个处理下订单,减库存。

rabbitmq兼容jms(java message service)协议,类似于jdbc,并且实现amqp

里面定义了一些api。

消费模式

1、点对点消费 生产者将消息发给消息代理,消息代理将其发送到队列当中,消息接收者从队列中获取消息内容,消息从队列中移除。

消息只能有唯一的生产者和消费者,但是可以有多个消费者去接收。

2、发布订阅 是基于topic的,一个发布了,所有订阅的主机都会接收到相应的信息。

rabbitmq里的组件

生产者 ————>产出消息的人

消息————>由message头和体组成,头里面有消息的各种设置,体就是具体的消息内容,里面还有一个routeKey,表示发送给那个路由

消息代理————>里面有路由器exchange,exchange负责接收消息,并将消息转发给对应的queue。

无论是生产者还是消费者都需要创建一个长连接来连接中间件,长连接里面有很多信道,可以负责消息的传送,

docker安装rabbitmq
docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672rabbitmq:management


4369,25672 (Erlang发现&集群端口)5672,5671(AMQP端口)

15672(web管理后台端口)

61613,61614(STOMP协议端口)1883,8883(MQTT协议端口)

https:/ /www.rabbitmq.com/networking.html  rabbitmq官网

rabbitmq运行机制

生产者产生消息,发送给broker,broker里面的exchange接受消息,并发送给绑定的queue,然后消费者从queue中取消息。

image.png

exchange的类型

根据交换机的类型的不同发送给的queue也不一样。 接下来来看一下常见的exchange类型,direct,点对点。 topic,发布订阅,fanout扇出。

direct类型

image.png

fanout类型

image.png

topic类型

image.png topic是根据模式匹配的,转发到对应的queue。

springboot整合rabbitmq

引入maven

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
        <version>2.7.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.12</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
</dependencies>

然后再测试类里面测试,创建exchange、queue、binding。

通过amqpAdmin来创建。


@Autowired
AmqpAdmin amqpAdmin;
@Autowired
RabbitTemplate rabbitTemplate;
@Test
void createExchange() throws Exception{
    DirectExchange exchange = new DirectExchange("com.bai.exchange",true,true);
    amqpAdmin.declareExchange(exchange);
}
@Test
void createQueue() throws Exception{
    //String name, boolean durable, boolean exclusive, boolean autoDelete, @Nullable Map<String, Object> arguments
    Queue queue = new Queue("hello-java",true,false,false);
  amqpAdmin.declareQueue(queue);
}

@Test
void createBinding() throws Exception{
    //String destination, DestinationType destinationType, String exchange, String routingKey, @Nullable Map<String, Object> arguments) {
    Binding binding = new Binding("hello-java", Binding.DestinationType.QUEUE,"com.bai.exchange","hello-java",null);
    amqpAdmin.declareBinding(binding);
    
}
@Test
void sendMessage() throws Exception{
    //String exchange, String routingKey, Object object, @Nullable CorrelationData correlationData)
    User user = new User();
    user.setName("user");
    user.setPassword("123456");
    user.setGender(1);
    rabbitTemplate.convertAndSend("com.bai.exchange","hello-java",user);
}

通过测试

image.png

image.png

已经创建成功。 但是!

如果传的是对象的话,会发生这种现象:

image.png

上面传的是字符串,下面是对象,对象被序列化了。 这是为什么呢?

因为rabbitmq里面自动配置的就是将对象转换成流,如果你没有配置相关的规则的话。

image.png 就是因为这个

image.png 他会把对象转换成字节流。

所以我们可以换一种方式,

image.png

这个messageconverter是接口类型的,ctrl+h,显示出他的实现类。

接下来,我们添加一个新的配置类,用json的转换器。

@Configuration
public class RabbitMQConfig {
    public MessageConverter getMessageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

image.png 竟然还是这个?仔细一看,竟然是没有加@bean

加上之后再来

image.png 终于,变成json格式的了。

然后,就是

image.png

接收消息,可以把要接受的消息类型卸载后面,参数会自动接收到消息。

RabbitHandler的使用 可以使用rabbithandnler来接收不同的实体类。

就比如,你发请求的时候,发送的实体类不一样,但是都发的是那个队列。 那接收的时候呢?不好接收,这个时候就可以用rabbithandler。

用它来重载一个方法。

image.png 这里没有手敲,直接截的图。

@rabbitListener是用在类或方法上,监听那个类的消息。 @RabbitHandler用在方法上,可以重写接收的参数。

消息确认机制

如果rabbitmq发送消息失败了怎么办呢?这里引入了rabbitmq的消息确认机制。

官网上说,可以使用事务的操作,但是会是性能下降250倍。

image.png

image.png

当p发送到broker的时候,会有一个确认回调。

交换机也就是e发送到queue之后,会再有一个回调也就是returnCallback

可靠抵达

image.png

编写配置类

@Configuration
public class RabbitMQConfig2 {
    @Resource
    RabbitTemplate rabbitTemplate;
    /*
     * 定制RabbitTemplate
     * */
    @PostConstruct   //myrabbitConfig创建完成后。执行这个方法
    public void initRabbitTemplate(){
        //确认回调
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             *
                只要消息抵达代理broker  就返回true

             * @param correlationData 当前消息的唯一关联数据(这个是消息的唯一id)
             * @param b 消息是否收到
             * @param s 失败的原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean b, String s) {
                System.out.println("correlationData: [" + correlationData+"]  ack  ["+b+"]  case["+s+"]");
            }
        });
    }
}

这个不能和上一个配置写到一起会有循环注入的问题,所以单独另写了一个配置类。

至于为什么会有这个循环注入的问题,先留个疑问。

然后在application.properties里面配置

#开启发送确认
spring.rabbitmq.publisher-confirm-type=correlated

这个就是开启了确认回调。

然后,再次启动。 我们查看。

image.png

image.png

发送数据

image.png

然后观察到。全部成功。

然后就是设置消息失败的回调

spring.rabbitmq.publisher-returns=true
spring.rabbitmq.template.mandatory=true
/*
* 设置消息抵达队列的确认回调
* */
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
    /**
     * 只要消息没有投递给指定队列,就出发失败回调
     * @param returnedMessage
     */
    @Override
    public void returnedMessage(ReturnedMessage returnedMessage) {
        /*
        *   private final Message message;  投递失败的消息详细信息
            private final int replyCode;    回复的状态码
            private final String replyText; 回复的文本内容
            private final String exchange;  当时这个消息发送给那个交换机
            private final String routingKey;    当时这个消息用那个路由键
        * */
        System.out.println("FailMessage:["+returnedMessage.getMessage()+"]  replyCode["+returnedMessage.getReplyCode()
                +"] replyText["+returnedMessage.getReplyText()+"] exchange:["+returnedMessage.getExchange()+"] routingKey"+returnedMessage.getRoutingKey()+"]");
    }
});

做一些修改,让i%2==0的时候,发送错误的路由键,看看会不会调用回调方法

image.png 这个是结果,错误的就会回调错误的方法、

创建用户1
FailMessage:[(Body:'[B@3f685507(byte[80])' MessageProperties [headers={__TypeId__=com.example.springrabbitmqdemo1.entity.User}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])]  replyCode[312] replyText[NO_ROUTE] exchange:[com.bai.exchange] routingKeyhello-java22]
correlationData: [null]  ack  [true]  case[null]
创建用户2
创建用户3
FailMessage:[(Body:'[B@35903e77(byte[80])' MessageProperties [headers={__TypeId__=com.example.springrabbitmqdemo1.entity.User}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])]  replyCode[312] replyText[NO_ROUTE] exchange:[com.bai.exchange] routingKeyhello-java22]
correlationData: [null]  ack  [true]  case[null]
创建用户4
创建用户5
correlationData: [null]  ack  [true]  case[null]
correlationData: [null]  ack  [true]  case[null]
FailMessage:[(Body:'[B@7b904429(byte[80])' MessageProperties [headers={__TypeId__=com.example.springrabbitmqdemo1.entity.User}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])]  replyCode[312] replyText[NO_ROUTE] exchange:[com.bai.exchange] routingKeyhello-java22]
创建用户6
创建用户7
correlationData: [null]  ack  [true]  case[null]
FailMessage:[(Body:'[B@7557b52b(byte[80])' MessageProperties [headers={__TypeId__=com.example.springrabbitmqdemo1.entity.User}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])]  replyCode[312] replyText[NO_ROUTE] exchange:[com.bai.exchange] routingKeyhello-java22]
correlationData: [null]  ack  [true]  case[null]
创建用户8
correlationData: [null]  ack  [true]  case[null]
创建用户9
correlationData: [null]  ack  [true]  case[null]
FailMessage:[(Body:'[B@71682f59(byte[80])' MessageProperties [headers={__TypeId__=com.example.springrabbitmqdemo1.entity.User}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])]  replyCode[312] replyText[NO_ROUTE] exchange:[com.bai.exchange] routingKeyhello-java22]
correlationData: [null]  ack  [true]  case[null]
correlationData: [null]  ack  [true]  case[null]

消费端确认

消费端确认默认是自动确认,只要消息接收到,客户端会自动确认,服务端就会移除这个消息。

那么这么做有什么问题呢?

当我们生产完消息之后,去消费,消费完一个,宕机,那么后续的所有没有消费的消息都就会消失。

也就是说:我们收到很多消息,自动回复ack,只处理成功一个,宕机了,发生消息丢失。

比如

image.png 我们启动debug模式。 等待消费的有六十条。

image.png

放行两条之后

image.png

接下来停止服务

image.png 里面的消息没有了。 产生消息丢失现象。

解决办法:

我们可以设置为手动接收

#手动ack
spring.rabbitmq.listener.simple.acknowledge-mode=manual

然后设置接收

@RabbitListener(queues = {"hello-java"})
public void receive(Message msg, User user, Channel channel) throws Exception {
    System.out.println("接收到的消息是" + msg + "信息是" + user);
    long deliveryTag = msg.getMessageProperties().getDeliveryTag();
    System.out.println("deliveryTag ===>" + deliveryTag);
    //签收货物,非批量模式
    try {
        if (deliveryTag % 2 == 0) {
            channel.basicAck(deliveryTag, false);
            System.out.println("签收了...." + deliveryTag);
        }
    } catch (Exception e) {
        //网络异常
        e.printStackTrace();
    }
}

也就是说,delivertag%2==0的时候,就不会签收。

看看具体效果。

image.png 签收了2,4

image.png 终止服务后 image.png

发现他没有消失。

if (deliveryTag % 2 == 0) {
    channel.basicAck(deliveryTag, false);
    System.out.println("签收了...." + deliveryTag);
}else
//            long deliveryTag  标签, boolean multiple是否批量去收默认为false,如果设置为true这个货物以前的所有货物全部被拒
//            , boolean requeue 是否重新入队,如果为false,则丢弃消息.如果为true,则重新发回服务器,服务器重新入队
    channel.basicNack(deliveryTag,false,true);
}

这里也可以做一些其他的操作。 就是如果,没有成功的或可以Nack

总结

image.png

延时队列--实现定时任务

延时队列的使用举例:下订单。

生成一个订单之后,30分钟,未支付,关闭订单。40分钟后检查,订单不存在或取消,解锁库存。

为啥是40分钟呢,是怕这个订单的延时,所以晚10m去扫描。

死信队列

Dead Letter Exchanges (DLX) ·一个消息在满足如下条件下,会进死信路由,记住这里是路由而不是队列, 一个路由可以对应很多队列。(什么是死信)

一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不会被再次放在队列里,被其他消费者使用。

(basic.reject/ basic.nack) requeue=false-上面的消息的TTL到了,消息过期了。

一队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上Dead Letter Exchange其实就是一种普通的exchange,和创建其他exchange没有两样。

只是在某一个设置Dead Letter Exchange的队列中有消息过期了,会自动触发消息的转发,发送到Dead Letter Exchange中去。

我们既可以控制消息在一段时间后变成死信,又可以控制变成死信的消息被路由到某一个指定的交换机,结合二者,其实就可以实现一个延时队列。

第一种实现延时队列就是给队列设置过期时间。

image.png

第二种实现方式就是给消息设置过期时间。

image.png

我们推荐给队列设置过期时间,因为我们的rabbitmq是惰性过期时间。

他会检查第一个过期时间发现是5m,后面的就不检查,5m之后,第一个过期之后,才检查之后的,那后面只有1m,或者2m过期的,就晚了,应该早过期的,结果产生了延时。

所以一般都用第一种,给队列设置过期时间。

具体使用场景

image.png

队列自动创建(RabbitMQ 自动创建队列/交换器/绑定_林老师带你学编程的博客-CSDN博客_rabbittemplate创建队列)

/* 容器中的Queue、Exchange、Binding 会自动创建(在RabbitMQ)不存在的情况下 */

/**
 * 死信队列
 *
 * @return
 */@Bean
public Queue orderDelayQueue() {
    /*
        Queue(String name,  队列名字
        boolean durable,  是否持久化
        boolean exclusive,  是否排他
        boolean autoDelete, 是否自动删除
        Map<String, Object> arguments) 属性
     */
    HashMap<String, Object> arguments = new HashMap<>();
    arguments.put("x-dead-letter-exchange", "order-event-exchange");
    arguments.put("x-dead-letter-routing-key", "order.release.order");
    arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟
    Queue queue = new Queue("order.delay.queue", true, false, false, arguments);

    return queue;
}

/**
 * 普通队列
 *
 * @return
 */
@Bean
public Queue orderReleaseQueue() {

    Queue queue = new Queue("order.release.order.queue", true, false, false);

    return queue;
}

/**
 * TopicExchange
 *
 * @return
 */
@Bean
public Exchange orderEventExchange() {
    /*
     *   String name,
     *   boolean durable,
     *   boolean autoDelete,
     *   Map<String, Object> arguments
     * */
    return new TopicExchange("order-event-exchange", true, false);

}


@Bean
public Binding orderCreateBinding() {
    /*
     * String destination, 目的地(队列名或者交换机名字)
     * DestinationType destinationType, 目的地类型(Queue、Exhcange)
     * String exchange,
     * String routingKey,
     * Map<String, Object> arguments
     * */
    return new Binding("order.delay.queue",
            Binding.DestinationType.QUEUE,
            "order-event-exchange",
            "order.create.order",
            null);
}

@Bean
public Binding orderReleaseBinding() {

    return new Binding("order.release.order.queue",
            Binding.DestinationType.QUEUE,
            "order-event-exchange",
            "order.release.order",
            null);
}

/**
 * 订单释放直接和库存释放进行绑定
 * @return
 */
@Bean
public Binding orderReleaseOtherBinding() {

    return new Binding("stock.release.stock.queue",
            Binding.DestinationType.QUEUE,
            "order-event-exchange",
            "order.release.other.#",
            null);
}


/**
 * 商品秒杀队列
 * @return
 */
@Bean
public Queue orderSecKillOrrderQueue() {
    Queue queue = new Queue("order.seckill.order.queue", true, false, false);
    return queue;
}

@Bean
public Binding orderSecKillOrrderQueueBinding() {
    //String destination, DestinationType destinationType, String exchange, String routingKey,
    //           Map<String, Object> arguments
    Binding binding = new Binding(
            "order.seckill.order.queue",
            Binding.DestinationType.QUEUE,
            "order-event-exchange",
            "order.seckill.order",
            null);

    return binding;
}

当我们写完之后,重启一下项目,发现

image.png 该生成的已经生成了。

但是!但是!但是!

用@bean生成的消息队列,当我们改变了原有的属性的时候,他不会去覆盖已有的队列,也就是说,我们只能手动的去删除 已有的交换机和队列。