消息中間件之RabbitMQ基礎

消息中間件之RabbitMQ基礎

一、RabbirMQ介紹

RabbitMQ使用Erlang語言開發,支持的併發量不大,適用於中小企業使用,併發量不是很大。

RabbitMQ是一個在AMQP基礎上實現的,可複用的企業消息系統。它可以用於大型軟件系統各個模塊之間的高效通信,支持高併發,支持可擴展。

支持多種開發語言支持,JavaPythonRubyPHPC/C++等。

RabbitMQ支持的工作隊列

二、RabbitMQ安裝

想吐槽,這個RabbitMQErlang安裝實在是不好用,可能還是自己太菜,反正挺麻煩的。但是我們還是用Docker這個好用的,哈哈!

官方也是用的:https://www.rabbitmq.com/download.html

docker pull rabbitmq:3-management     # 拉取rabbitmq
docker run -d -p 5672:5672 -p 15672:15672 --name rabbitmq rabbitmq:3-management  # 啓動容器

如果開啓的防火牆,需要先開放端口567215672,然後在更新規則。

[root@localhost ~]# firewall-cmd --zone=public --add-port=15672/tcp --permanent
[root@localhost ~]# firewall-cmd --zone=public --add-port=5672/tcp --permanent
[root@localhost ~]# firewall-cmd --reload   

訪問:http://IP:15672/#/

用戶名和密碼:guestguest,輸入登錄之後就可以進入RabbitMQ管理頁面。

RabbitMQ支持這幾種端口號:

  • 5672:消息中間內部通訊的端口
  • 15672:管理平臺端口號
  • 25672:集羣的端口號

三、管理界面的使用

3.1. 管理界面介紹

3.2. 創建用戶

首先看一下用戶角色

角色 代碼 描述
超級管理員 administrator 可登陸管理控制檯,可查看所有的信息,並且可以對用戶,策略(policy)進行操作
監控者 monitoring 可登陸管理控制檯,同時可以查看rabbitmq節點的相關信息(進程數,內存使用情況,磁盤使用情況等)
策略制定者 policymaker 可登陸管理控制檯, 同時可以對policy進行管理。但無法查看節點的相關信息(上圖紅框標識的部分)
普通管理者 management 僅可登陸管理控制檯,無法看到節點信息,也無法對策略進行管理
其他 無法登陸管理控制檯,通常就是普通的生產者和消費者

創建用戶

3.3. 添加Virtual Hosts

3.4. 爲用戶添加Virtual Hosts

四、五種隊列

常用的只有:點對點的簡單隊列、工作隊列、發佈訂閱、路由、通配符這五種,下面我們詳細介紹下

4.1. 點對點模式(簡單隊列)

4.1.1. 介紹

一個生產者P發送消息到隊列Q,一個消費者C接收。有多個消費者會使用輪詢方法進行消費隊列中信息。

4.1.2. 代碼演示

引入依賴

<!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client -->
<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.8.0</version>
</dependency>

工具類:

import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author 墨龍吟
 * @version 1.0.0
 * @ClassName RabitConnection.java
 * @Description tabbitmq 連接工具
 * @createTime 2020年01月29日 - 18:53
 */
public class RabbitConnection {

    public static Connection connection(){
        Connection connection = null;
        try {
            ConnectionFactory connectionFactory = new ConnectionFactory();
            // step 設置IP
            connectionFactory.setHost("192.168.252.132");
            // step 設置端口
            connectionFactory.setPort(5672);
            // step 設置用戶名和密碼
            connectionFactory.setUsername("admin");
            connectionFactory.setPassword("admin");
            // step 設置 Virtual Host
            connectionFactory.setVirtualHost("/long");
            connection = connectionFactory.newConnection();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
        return connection;
    }

}

生產者

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author 墨龍吟
 * @version 1.0.0
 * @ClassName MQProducer.java
 * @Description 生產者
 * @createTime 2020年01月29日 - 18:58
 */
public class MQProducer {

    public static void main(String[] args) {
        try {
            Connection connection = RabbitConnection.connection();
            // step 創建通道
            Channel channel = connection.createChannel();
            String msg = "中國加油!";
            channel.basicPublish("", "hello", null, msg.getBytes());
            System.out.println("生產者發送消息:" + msg);
            channel.close();
            connection.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }

}

消費者

import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @author 墨龍吟
 * @version 1.0.0
 * @ClassName MQConsumer.java
 * @Email [email protected]
 * @Description 消費者
 * @createTime 2020年01月29日 - 19:02
 */
public class MQConsumer {

    public static void main(String[] args) {
        try {
            Connection connection = RabbitConnection.connection();
            Channel channel = connection.createChannel();
            channel.basicConsume("hello", true, 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");
                    System.out.println("消費者接受的消息:" + msg);
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4.2. 工作隊列

4.2.1. 介紹

默認的傳統隊列是爲均攤消費,存在不公平性;如果每個消費者速度不一樣的情況下,均攤消費是不公平的,應該是能者多勞。

4.2.2. 圖例

採用工作隊列,在通道中只需要設置basicQos爲1即可,表示MQ服務器每次只會給消費者推送1條消息必須手動ack確認之後纔會繼續發送。channel.basicQos(1)

4.2.3. 實例

生產者:

public class MQProducer {

    public static void main(String[] args) {
        try {
            Connection connection = RabbitConnection.connection();
            // step 創建通道
            Channel channel = connection.createChannel();
            for (int i = 0; i < 10; i++) {
                String msg = "第" + i + "條,中國加油!";
                channel.basicPublish("", "hello", null, msg.getBytes());
                System.out.println("生產者發送消息:" + msg);
            }
            channel.close();
            connection.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }

}

消費者:

public class MQConsumer {

    public static void main(String[] args) {
        final int time = 2000;
        System.out.println("消費者:" + time);
        try {
            Connection connection = RabbitConnection.connection();
            final Channel channel = connection.createChannel();
            // MQ每次只能給消費者發送一條消息,必須返回ack之後纔可以繼續發送消息給消費者
            channel.basicQos(1);
            // auto Ack 默認自動簽收(true), false(必須手動Ack)
            channel.basicConsume("hello", false, 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");
                    try {
                        Thread.sleep(time);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("消費者接受的消息:" + msg);
                    // 手動告訴MQ從隊列中刪除這條消息
                    channel.basicAck(envelope.getDeliveryTag(), false);
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

結果:

4.2.4. 隊列存在的問題:消失丟失

消息丟失可能存在三方面:

  • 生產者丟失數據:生產者將數據發送到 RabbitMQ 的時候,可能數據就在半路丟失;
  • RabbitMQ丟失數據:MQ還沒有持久化就自己丟失了 (MQ掛了、MQ拒絕接受消息 (隊列滿了));
  • 消費端丟失數據:剛消費到,還沒處理,結果進程掛了(重啓消費端)
  • 其他情況下: 硬盤壞了、持久化的過程斷電了 ; 最好通過表記錄每次生產者投遞消息,如果長期沒有被消費,手動的補償消費。
4.2.5. 消息不丟失解決方法:
  • 生產者方面:

    • 方案一: 開啓RabbitMQ事務(同步方法,不推薦)

      // 開啓事務
      channel.txSelect
      try {
          // 這裏發送消息
      } catch (Exception e) {
          channel.txRollback
      
          // 這裏再次重發這條消息
      }
      
      // 提交事務
      channel.txCommit
      
    • 方案二:開啓confirm模式(異步,推薦)

      channel.confirmSelect();
      String msg = "第" + i + "條,中國加油!";
      channel.basicPublish("", "hello", null, msg.getBytes());
      if (channel.waitForConfirms()) {
          System.out.println("生產者發送消息:" + msg + "成功");
      } else {
          System.out.println("生產者發送消息:" + msg + "失敗");
      }
      
  • MQ方面:

    • 開啓RabbitMQ的持久化(默認的情況下MQ服務器端創建隊列和交換機都是持久化的)

    • 通過代碼設置持久化。

  • 消費者方面:

    • 關閉RabbitMQ的自動ACK

4.3. 發佈訂閱模式

RabbitMQ支持的後面幾種模式都是依賴於交換機。交換機支持一下幾種模式:

  • Direct exchange(直連交換機)
  • Fanout exchange(扇型交換機)
  • Topic exchange(主題交換機)
  • Headers exchange(頭交換機)
4.3.1. 原理介紹

簡單解釋就是,可以將消息發送給不同類型的消費者。做到發佈一次,消費多個。(使用扇形交換機)

4.3.2. 創建交換機

4.3.3. 實例

先創建對應virtual host的交換機。

生產者:

public class PsProducer {

    private final static String EXCHANGE_NAME = "test_long";

    public static void main(String[] args) throws IOException, TimeoutException {
        System.out.println("發佈訂閱模式中生產者啓動...");
        // 創建新的連接
        Connection connection = RabbitConnection.connection();
        // 創建通道
        Channel channel = connection.createChannel();
        // 綁定的交換機 參數1交互機名稱 參數2 exchange類型
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout", true);
        String msg = "這是生產者發送的一個消息...";
        // 發送消息
        channel.basicPublish(EXCHANGE_NAME, "", null, msg.getBytes());
        channel.close();
        connection.close();
    }

}

短信消費者:

public class SmsConsumer {

    private final static String EXCHANGE_NAME = "test_long";
    private final static String QUEUE_NAME = "sms_queue";

    public static void main(String[] args) throws IOException {
        System.out.println("短信消費者啓動...");
        // 創建新的連接
        Connection connection = RabbitConnection.connection();
        // 創建通道
        Channel channel = connection.createChannel();
        // 消費者關聯隊列
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
        channel.basicConsume(QUEUE_NAME, true, 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");
                System.out.println("短信消費者接受的消息:" + msg);
            }
        });
    }
}

郵件消費者:

public class EmailConsumer {

    private final static String EXCHANGE_NAME = "test_long";
    private final static String QUEUE_NAME = "email_queue";

    public static void main(String[] args) throws IOException {
        System.out.println("郵件消費者啓動...");
        Connection connection = RabbitConnection.connection();
        Channel channel = connection.createChannel();
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
        channel.basicConsume(QUEUE_NAME, true, 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");
                System.out.println("郵件消費者接受的消息:" + msg);
            }
        });
    }
}

結果:

4.4. 路由模式

4.4.1. 簡介

當交換機類型爲direct類型時,根據隊列綁定的路由建轉發到具體的隊列中存放消息。

4.4.2. 實例代碼

生產者

public class Producer {

    private final static String EXCHANGE_NAME = "long_direct_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        System.out.println("發佈訂閱模式中生產者啓動...");
        Connection connection = RabbitConnection.connection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, "direct", true);
        for (int i = 0; i < 10; i++) {
            if (i % 2 == 0) {
                String msg = "這是生產者" + i + "發送郵件的一個消息...";
                System.out.println("郵件消息: " + msg);
                channel.basicPublish(EXCHANGE_NAME, "email", null, msg.getBytes());
            } else {
                String msg = "這是生產者" + i + "發送短信的一個消息...";
                System.out.println("短信消息: " + msg);
                channel.basicPublish(EXCHANGE_NAME, "sms", null, msg.getBytes());
            }
        }
        channel.close();
        connection.close();
    }

}

短信消費者

public class SmsConsumer {

    private final static String EXCHANGE_NAME = "long_direct_exchange";
    private final static String QUEUE_NAME = "sms_queue";

    public static void main(String[] args) throws IOException {
        System.out.println("短信消費者啓動...");
        Connection connection = RabbitConnection.connection();
        Channel channel = connection.createChannel();
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "sms");
        channel.basicConsume(QUEUE_NAME, true, 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");
                System.out.println("短信消費者接受的消息:" + msg);
            }
        });

    }

}

郵件消費者

public class EmailConsumer {

    private final static String EXCHANGE_NAME = "long_direct_exchange";
    private final static String QUEUE_NAME = "email_queue";

    public static void main(String[] args) throws IOException {
        System.out.println("郵件消費者啓動...");
        Connection connection = RabbitConnection.connection();
        Channel channel = connection.createChannel();
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "email");
        channel.basicConsume(QUEUE_NAME, true, 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");
                System.out.println("郵件消費者接受的消息:" + msg);
            }
        });

    }

}

消費結果:

4.5. 通配符模式

4.5.1. 簡介

當交換機類型爲topic類型時,根據隊列綁定的路由鍵模糊轉發到具體的隊列中存放。

#號表示支持匹配多個詞;*號表示只能匹配一個詞。

4.5.2. 實例代碼

生產者:

public class Producer {

    private final static String EXCHANGE_NAME = "long_topic_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        System.out.println("發佈訂閱模式中生產者啓動...");
        Connection connection = RabbitConnection.connection();
        Channel channel = connection.createChannel();
        // 修改爲topic類型
        channel.exchangeDeclare(EXCHANGE_NAME, "topic", true);
        for (int i = 0; i < 10; i++) {
            if (i % 2 == 0) {
                String msg = "這是生產者" + i + "發送郵件的一個消息...";
                System.out.println("郵件消息: " + msg);
             	// 	路由鍵 爲 topic.email.long
                channel.basicPublish(EXCHANGE_NAME, "topic.email.long", null, msg.getBytes());
            } else {
                String msg = "這是生產者" + i + "發送短信的一個消息...";
                System.out.println("短信消息: " + msg);
                // 路由鍵 爲 topic.sms
                channel.basicPublish(EXCHANGE_NAME, "topic.sms", null, msg.getBytes());
            }
        }
        channel.close();
        connection.close();
    }

}

短信消費者:使用#可以匹配所有topic開頭的消息。

public class SmsConsumer {

    private final static String EXCHANGE_NAME = "long_topic_exchange";
    private final static String QUEUE_NAME = "sms_queue";

    public static void main(String[] args) throws IOException {
        System.out.println("短信消費者啓動...");
        Connection connection = RabbitConnection.connection();
        Channel channel = connection.createChannel();
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "topic.#");
        channel.basicConsume(QUEUE_NAME, true, 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");
                System.out.println("短信消費者接受的消息:" + msg);
            }
        });

    }

}

郵件消費者:使用*可以匹配所有topic.*.*(*爲替代字符)的消息。

public class EmailConsumer {

    private final static String EXCHANGE_NAME = "long_topic_exchange";
    private final static String QUEUE_NAME = "email_queue";

    public static void main(String[] args) throws IOException {
        System.out.println("郵件消費者啓動...");
        Connection connection = RabbitConnection.connection();
        Channel channel = connection.createChannel();
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "topic.email.*");
        channel.basicConsume(QUEUE_NAME, true, 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");
                System.out.println("郵件消費者接受的消息:" + msg);
            }
        });

    }

}

運行結果

五、Sptingboot整合RabbitMQ

5.1. 添加依賴

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</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-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.amqp</groupId>
        <artifactId>spring-rabbit-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

5.2. 配置文件

spring:
  rabbitmq:
    # 連接地址
    host: 192.168.252.132
    # 端口號
    port: 5672
    # 賬號
    username: admin
    # 密碼
    password: admin
    # 地址
    virtual-host: /long

5.3. 配置類:註冊隊列和交換機

主要三步:

  1. 創建隊列
  2. 創建交換機
  3. 將隊列綁定到交換機
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

/**
 * @author 墨龍吟
 * @version 1.0.0
 * @ClassName RabbitConfig.java
 * @Description RabbitMQ 配置類
 * @createTime 2020年02月17日 - 22:10
 */
@Component
public class RabbitConfig {

    /** 交換機名稱 */
    private final static String EXCHANGE_NAME = "spring_boot_exchange";

    /** 短信隊列名稱 */
    private final static String FANOUT_SMS_QUEUE = "fanout_sms_queue";

    /** 郵件隊列名稱 */
    private final static String FANOUT_EMAIL_QUEUE = "fanout_email_queue";


    /** 創建短信隊列 */
    @Bean
    public Queue smsQueue() {
        return new Queue(FANOUT_SMS_QUEUE);
    }

    /** 創建郵件隊列 */
    @Bean
    public Queue emailQueue() {
        return new Queue(FANOUT_EMAIL_QUEUE);
    }

    /** 創建交換機 */
    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange(EXCHANGE_NAME);
    }

    /** 將短信隊列綁定到交換機 */
    @Bean
    public Binding smsBindingExchange(Queue smsQueue, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(smsQueue).to(fanoutExchange);
    }

    /** 將郵件隊列綁定到交換機 */
    @Bean
    public Binding emailBindingExchange(Queue emailQueue, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(emailQueue).to(fanoutExchange);
    }

}

5.4. 創建生產者和消費者

生產者:

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 墨龍吟
 * @version 1.0.0
 * @ClassName HomeController.java
 * @Description 消費者
 * @createTime 2020年02月17日 - 22:47
 */
@RestController
public class HomeController {

    private final static String EXCHANGE_NAME = "spring_boot_exchange";

    @Autowired
    private AmqpTemplate amqpTemplate;

    /**
     * 投遞消息,客戶端不會馬上知道消費者是否被消費,但是能夠確認知道我們是否投遞消息到中間件
     * @return
     */
    @GetMapping("/send_msg")
    public String sendMsg() {
        // 參數1 交換機名稱 、參數2路由key  參數3 消息
        amqpTemplate.convertAndSend(EXCHANGE_NAME, "", "這個是一條消息");
        return "success";
    }
}

消費者:

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * @author 墨龍吟
 * @version 1.0.0
 * @ClassName FanoutEmailConsumer.java
 * @Description 郵件消費者
 * @createTime 2020年02月17日 - 22:52
 */
@Component
@RabbitListener(queues = "fanout_email_queue")
public class FanoutEmailConsumer {

    @RabbitHandler
    public void process(String msg) {
        System.out.println("郵件消費者收到消息:" + msg);
    }
}

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * @author 墨龍吟
 * @version 1.0.0
 * @ClassName FanoutEmailConsumer.java
 * @Description 郵件消費者
 * @createTime 2020年02月17日 - 22:52
 */
@Component
@RabbitListener(queues = "fanout_sms_queue")
public class FanoutSmsConsumer {

    @RabbitHandler
    public void process(String msg) {
        System.out.println("短信消費者收到消息:" + msg);
    }
}

springboot會自動創建交換機和隊列,不需要我們手動創建。

5.5. 結果

六、歡迎關注個人微信公衆號

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章