知识拓展之RabbitMQ

123 阅读11分钟

知识拓展之RabbitMQ

1. 什么是MQ?

MQ全称Message Queue(消息队列),是在消息的传输过程中保存消息的容器。多用于系统之间的异步通信

同步通信和异步通信的区别

  • 同步通信:此刻A 和 B 直接连接,信息传输,类似于打电话,中断其他操作,禁止三心二意。
  • 异步通信:A 将消息发送给第三方 消息存储,然后通过消息传输队列发送给 B,A发送完消息以后就可以进行其他操作,不会造成阻塞。比如 以前的通信方式,A 写信 寄到邮局,然后寄出去以后,A不考虑这个信是否丢失延迟,他回去做自己的事,然后B收到邮局寄来的信,B回信也是通过这种方式回信。

2. 理解消息队列

消息就是两台计算机之间传送的数据单位,本质上就是一段数据,它能被一个或者多个应用程序所理解,是应用程序的之间传递的信息载体,消息可以非常简单,例如只包含文本字符串,也可以更复杂的嵌入对象。

队列是数据结构中概念。而队列中,数据先进先出,后进后出,犹如排队做核酸。

消息队列 MQ是把消息和队列结合起来,称为消息队列(Message Queue)。把要传输的数据(消息) 与队列进行绑定,用队列先进先出机制来实现消息传递,消息队列由消费者和生产者两部分构成;生产者主要负责生产消息并把消息放入队列中,再由消费者去处理。消费者可以到指定队列中获取消息,或者订阅相应的队列,最后由MQ服务端进行消息推送。

什么是订阅? 订阅就是为消费者服务的,消费者提前订阅,当消息队列中有消息产出时,自动去获取消息进行消费。生活中有很多这种例子,比如购买腾讯、优酷等视频会员就会有订阅模式,当你的会员到期时,会自动帮你完成续费。

3. MQ的优势和劣势

优势

  1. 应用解耦 例如在我们非常熟悉的电商系统平台中,用户下订单需要调用订单系统,订单系统需要调用订单系统,订单系统需要调用库存系统、支付系统,、物流系统等进行数据交互来完成整个下单业务,这种把整个业务耦合在一起。很容易出现我们遇见的问题:

    ①、当中间某个环节库存系统或支付系统出现异常或故障,会造成整个订单系统崩溃无法完成订单业务;

    ②、当我们需求开发时,可能会新增一个其他系统的业务与当前的订单系统关联并进行数据交互,此时就必须修改订单系统的代码。如果系统壮大,业务越来越多,越来越复杂时,随之带来的维护成本也会成指数增加。

    由此我们不难看出 系统耦合性越高,容错性和可维护性就会越低。如果在系统中引入MQ,即订单系统将消息先发送到MQ中,MQ再将数据消息转发到其他系统,问题就可以得到解决,MQ天生就是来干这个事的。那么当我们加入MQ以后整个系统会发生什么变化?

    ①、由于订单系统只发消息给MQ,不直接对接其他系统,如果其他任何一个系统出现异常或故障时,不影响整个订单业务的执行。当异常或故障解决以后,程序自动从MQ获取数据信息完成后续相关业务逻辑即可。

    ②、如果需求修改,新增了一个业务系统,此时无需修改其他系统的代码,只需修改MQ将消息发送给新增的系统即可,实现数据信息的可靠有效传递。使用MQ让系统应用间进行解耦,提升整个系统的容错性和可维护性

  2. 异步提速 开发一款App系统,其登录注册是最为常见的需求,例如注册的业务有手机注册入库、绑定邮箱验证、手机短信验证。

    用户的注册操作需要等待350ms才能得到响应,如果邮件验证系统/手机短信通知系统耗时比较长,那么用户的响应随之就会变长,非常影响客户的体验度,而当使用了MQ后,客户的体验度飙升。

  3. 削峰填谷 假设系统承载的QPS是1000,如果流量顶峰时达到50000,则会造成系统压垮崩溃。使用了MQ之后,限制消费消息的速度为1000,把数据产生的压力放在MQ中,系统的高峰就会被削掉,这个过程叫 削峰,因为消息积压的数据比较多,在高峰期过后的一段时间内,消费消息的速度还会继续维持在这个速度,直到把积压的消息消费完,这个过程叫 填谷。这样可以大大提高系统的稳定性和用户体验。

劣势

  • 系统可用性降低: 系统引入的外部依赖越多,系统稳定性越差。一旦MQ宕机,就会对业务造成影响。
  • 系统复杂度提高: MQ的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过MQ进行异步调用。
  • 一致性问题 : A系统处理完业务,通过MQ给B、C、D三个系统发消息数据,如果B系统、C系统处理成功,D系统处理失败,则会造成数据处理的不一致。

4. AMQP

  • 什么是AMQP: 即Advanced Message Queuing Protocol(高级消息队列协议),是一个网络协议,专门为消息中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受不同中间件产品,不同开发语言等条件的限制。2006年AMQP规范发布等,类比HTTP。
  • AMQP工作过程: 生产者(Publisher)将消息发布到交换机(Exchange),交换机根据规则将消费分发给交换机绑定的队列(Queue),队列再将消息投递给订阅了此队列的消费者。

5. 工作原理

  • Producer【消息的生产者】:一个向交换机发布消息的客户端应用程序。
  • Connection【连接】:生产者/消费者和RabbitMQ服务器之间建立的TCP连接。
  • Channel【信道】:是TCP里面的虚拟连接。例如:Connection相当于电缆,Channel相当于独立光纤束,一条TCP连接中可以创建多条信道,增加连接效率。无论是发布消息、接收消息、订阅队列都是通过信道完成的。
  • Broker:消息队列服务器实体。相当于RabbitMQ服务器。
  • Virtual Host【虚拟机】:出于多租户和安全因素设计的,把AMQP的基本组件划分到一个虚拟的分组中。每个vhost本质上就是一个mini版的RabbitMQ服务器,拥有自己的队列、交换机、绑定和权限机制。当多个不同的用户使用同一个RabbitMQ服务器时,可以划分多个虚拟主机。RabbitMQ默认的主机是/。
  • Queue【消息队列】 用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。消息一直在队列里面,等待消费者链接到这个 队列将其取走。
  • Binding【绑定】 消息队列和交换机之间的虚拟连接,绑定中包含路由规则,绑定信息保存到交换机的路由表中,作为消息的分发依据。 Consumer【消息的消费者】 表示一个从消息队列中取得消息的客户端应用程序。

6. 五个模型示例

📕 Hellow World简单模型

一对一消费,只有一个消费者能接收到。

创建一个队列,生产者发送消息,存储到队列,通过队列传递给消费者。

//消费者@Component
public class HelloWordListener {
    // @RabbitListener(queues = ("simple.queue")) // queues需手动先创建队列
    @RabbitListener(queuesToDeclare = @Queue("simple.queue"))  // queuesToDeclare 自动声明队列
    public void holloWordListener(String message){
        System.out.println("message = " + message);
    }
}
​
//生产者
@Component
public class HelloWorldProduce{
    @Autowired
    private RabbitTemplate rabbitTemplate;
​
    @Test
    public void testSimpleQueue() {
        String queueName = "simple.queue"; // 队列名称
        String message = "heel,simple.queue"; // 要发送的消息
        rabbitTemplate.convertAndSend(queueName,message);
    }
}

📕 Work queues工作队列

多个消费者,你一个我一个分配消息,有预取机制,默认公平消费,可配置能者多劳,谁完成的快,谁多做一点

工作队列,生产者向队列发送消息,多个消费者进行消费分配,默认的是公平消费,一人消费一条消息,也可以进行机制分配,完成的越快从队列获取的越快,消费的消息条数也就越多。

//消费者
@Component
public class WoekWordListener {
​
    @RabbitListener(queuesToDeclare = @Queue("workQueue")) // queuesToDeclare 自动声明队列
    public void holloWordListener(String message) throws InterruptedException {
        Thread.sleep(200);
        System.out.println("message1 = " + message);
    }
    
    @RabbitListener(queuesToDeclare = @Queue("workQueue")) // queuesToDeclare 自动声明队列
    public void holloWordListener1(String message) throws InterruptedException {
        Thread.sleep(400);
        System.out.println("message2 = " + message);
    }
}
​
//生产者
@Component
public class WorkWordProduce{
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @Test
    public void testWorkQueue(){
        String queueName = "workQueue";
        String message = "hello,work.queue__";
        for (int i = 0; i < 10; i++) {
            rabbitTemplate.convertAndSend(queueName,message+i);
            System.out.println("i = " + i);
        }
    }
}

📕 Publish/Subscribe发布订阅模型

发布订阅模式与之前的案例的区别就是允许将同一消息发送给多个消费者

实现方式就是加入了exchange(交换机),注意:交换机是不可缓存消息的。

使用fanout交换机,会将接收到的消息路由到每一个跟其绑定的queue(队列)

//消费者
// 消费者直接绑定交换机,指定类型为fanout
@Component
public class FanoutExchangeListener {
    // 不指定队列,消息过了就没了
    //  @RabbitListener(bindings = {@QueueBinding(value = @Queue,exchange = @Exchange(value = "fanoutTest",type = ExchangeTypes.FANOUT))})// 指定队列,可以接收缓存到队列里的消息
    @RabbitListener(bindings = {@QueueBinding(value = @Queue(value ="test",durable = "true" ),exchange = @Exchange(value = "fanoutTest",type = ExchangeTypes.FANOUT))})
    public void reveivel(String message){
        System.out.println("message = " + message);
    }
​
    @RabbitListener(bindings = {@QueueBinding(value = @Queue,exchange = @Exchange(value = "fanoutTest",type = ExchangeTypes.FANOUT))})
    public void reveivel2(String message){
        System.out.println("message1 = " + message);
    }
}
​
//生产者
@Component
public class FanoutExchangeProduce{
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @Test
    public void tesyPubSubQueue(){
        // 参数1:交换机名称 , 参数2routingKey,(fanout类型可不写) , 参数3,消息内容
        rabbitTemplate.convertAndSend("fanoutTest","","消息内容");
    }
}

📕 Routing路由模型

routing模型也是将消息发送到交换机

使用的是Direct类型的交换机,会将接收到的消息根据规则将路由到指定的Queue队列(队列) ,因此称为路由模式。

//消费者// 消费者直接绑定交换机,指定类型为direct,并指定key表示能消费的key
@Component
public class RoutingExchangeListener {
​
    // 不指定队列,消息过了就没了
    //  @RabbitListener(bindings = {@QueueBinding(value = @Queue,exchange = @Exchange(value = "direstTest",type = ExchangeTypes.DIRECT),key = {"info","error"})})
    
    // 指定队列,可以接收缓存到队列里的消息
    // key = {"info","error"} 表示我能接收到routingKey为 info和error的消息
    @RabbitListener(bindings = {@QueueBinding(value = @Queue(value ="test1",durable = "true" ),exchange = @Exchange(value = "direstTest",type = ExchangeTypes.DIRECT),key = {"info","error"})})
    public void receivel(String message){
        System.out.println("message = " + message);
    }
    // key = {"error"} 表示我只能接收到routingKey为 error的消息
    @RabbitListener(bindings = {@QueueBinding(value = @Queue,exchange = @Exchange(value = "direstTest",type = ExchangeTypes.DIRECT),key = {"error"})})
    public void receivel1(String message){
        System.out.println("message1 = " + message);
    }
}
​
@Component
public class RoutingExchangeProduce{
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    // 路由模型
    @Test
    public void direstExchangeTest(){
        rabbitTemplate.convertAndSend("direstTest","info","发送info的key的路由消息");
    }
    // 路由模型
    @Test
    public void direstExchangeTest1(){
        rabbitTemplate.convertAndSend("direstTest","error","发送error的key的路由消息");
    }
}

📕 Topics主题模型

topicExchange与dirctExchange类型,区别在于routingKey必须是多个单词的列表,并且,分隔。

*(代表通配符,任意一个字段),#(代表一个或多个字段)

//消费者@Component
public class TopicsExchangeListener {
​
    // 不指定队列,消息过了就没了
    //  @RabbitListener(bindings = {@QueueBinding(value = @Queue,exchange = @Exchange(name = "topicList",type = ExchangeTypes.TOPIC),key = {"user.save","user.*"})})
    
    // 指定队列,可以接收缓存到队列里的消息
    // key = {"user.save","user.*"} 表示能消费 routingkey为  user.save 和 user.任意一个字符  的消息
    @RabbitListener(bindings = {@QueueBinding(value = @Queue(value ="test2",durable = "true" ),exchange = @Exchange(name = "topicList",type = ExchangeTypes.TOPIC),key = {"user.save","user.*"})})
    public void recevicel(String message){
        System.out.println("message = " + message);
    }
    // key = {"order.#","user.*"} 表示能消费 routingkey为  order.一个或多个字符   和  user.任意一个字符  的消息
    @RabbitListener(bindings = {@QueueBinding(value = @Queue,exchange = @Exchange(name = "topicList",type = ExchangeTypes.TOPIC),key = {"order.#","user.*"})})
    public void recevicel1(String message){
        System.out.println("message1 = " + message);
    }
}
​
// 生产者
@Component
public class TopicExchangeProduce{
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @Test
    public void topicTest(){
        rabbitTemplate.convertAndSend("topicTest","user.save","topic路由消息,use.save");
    }
    
    @Test
    public void topicTest1(){
        rabbitTemplate.convertAndSend("topicTest","order.select.getone","topic路由消息,order.select.getone");
    }
​
}