由淺至深的RabbitMQ學習之路

源於螞蟻課堂的學習,點擊這裏查看(老餘很給力)    

MQ背景

對於例如發送郵件或短信等行爲,傳統做法往往是自上而下執行,這樣一來,增加用戶等待響應時間,嚴重影響用戶體驗。
之後,開始將這些延時操作放入異步線程去處理,但是這樣會增加CPU的開銷。
故消息中間件橫空出世,很好地解決了這一痛點,實現異步、解耦、流量削峯等功能

 

 市面主流的MQ

ActiveMQ 

歷史悠久的開源項目,是Apache下的一個子項目。
已經在很多產品中得到應用,實現了JMS1.1規範,可以和spring-jms輕鬆融合,實現了多種協議,不夠輕巧(源代碼比RocketMQ多),
支持持久化到數據庫,對隊列數較多的情況支持不好。

RabbitMQ

結合erlang語言本身的併發優勢,支持很多的協議:AMQP,XMPP, SMTP, STOMP,也正是如此,使的它變的非常重量級,
更適合於企業級的開發。

RocketMQ 

阿里系下開源的一款分佈式、隊列模型的消息中間件,原名Metaq,3.0版本名稱改爲RocketMQ,是阿里參照kafka設計思想使用java
實現的一套mq。同時將阿里系內部多款mq產品(Notify、metaq)進行整合,只維護核心功能,去除了所有其他運行時依賴,保證核心功能
最簡化,在此基礎上配合阿里上述其他開源產品實現不同場景下mq的架構,目前主要多用於訂單交易系統。

Kafka

Apache下的一個子項目,使用scala實現的一個高性能分佈式Publish/Subscribe消息隊列系統,具有以下特性:
高吞吐:在一臺普通的服務器上既可以達到10W/s的吞吐速率;
高堆積:支持topic下消費者較長時間離線,消息堆積量大;

RabitMQ環境的基本安裝 (windows)

1.下載並安裝erlang,下載地址:http://www.erlang.org/download
2.配置erlang環境變量信息
  新增環境變量ERLANG_HOME=erlang的安裝地址
  將%ERLANG_HOME%\bin加入到path中
3.下載並安裝RabbitMQ,下載地址:http://www.rabbitmq.com/download.html
注意: RabbitMQ 它依賴於Erlang,需要先安裝Erlang。

 RabitMQ管理平臺中心

RabbitMQ 管理平臺地址 http://127.0.0.1:15672
默認賬號:guest/guest  用戶可以自己創建新的賬號

Virtual Hosts:
每個VirtualHost相當一個相對獨立的RabbitMQ服務器,VirtualHost之間是相互隔離的。exchange、queue、message不能互通。

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

 快速入門RabbitMQ簡單隊列

 Maven依賴

<dependencies>
    <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>3.6.5 </version>
    </dependency>
</dependencies>

 獲取連接

public class RabitMQConnection {
    /**
     * 獲取連接
     *
     * @return
     */
    public static Connection getConnection() throws IOException, TimeoutException {
        // 1.創建連接
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 2.設置連接地址
        connectionFactory.setHost("127.0.0.1");
        // 3.設置端口號:
        connectionFactory.setPort(5672);
        // 4.設置賬號和密碼
        connectionFactory.setUsername("yanxiaohui");
        connectionFactory.setPassword("yanxiaohui");
        // 5.設置VirtualHost
        connectionFactory.setVirtualHost("/yxh");
        return connectionFactory.newConnection();
    }
}

 生產者

public class Producer {
    private static final String QUEUE_NAME = "rabbit_mq_demo";

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        // 1.創建我們的連接
        Connection connection = RabitMQConnection.getConnection();
        // 2.創建我們通道
        Channel channel = connection.createChannel();
        // 開啓了確認消息機制
        channel.confirmSelect();
        String msg = "這是一個mq的入門案例";
        channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
        if (channel.waitForConfirms()) {
            System.out.println("發送消息成功");
        } else {
            System.out.println("發送消息失敗");
        }
        channel.close();
        connection.close();
    }
}

 消費者

public class Consumer {
    private static final String QUEUE_NAME = "rabbit_mq_demo";
    private static int serviceTimeOut = 1000;

    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.創建我們的連接
        Connection connection = RabitMQConnection.getConnection();
        // 2.創建我們通道
        final Channel channel = connection.createChannel();
        DefaultConsumer defaultConsumer = 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:" + msg);
                // 手動ack應答模式
                channel.basicAck(envelope.getDeliveryTag(), false);
                try {
                    Thread.sleep(serviceTimeOut);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };
        // 3.創建我們的監聽的消息
        channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
    }
}

 RabbitMQ如何保證消息不丟失

生產者:投遞消息時採用消息確認機制confirm,確保消息投遞至消息中間件中,否則進行重新投遞
消費者:採用手動ack的方式通知消息中間件刪除消息,即消息成功消費後再將其刪除
中間件:採用持久化的方式將消息持久化至硬盤中

 RabitMQ五種消息模式

點對點

消費者自動確認機制,即生成一個消息,推送一個消息至消費者,不管其是否消費成功,默認自動刪除消息

 工作模式

消費者手動ack通知消息的服務器刪除消息,然後推送下一個。
在有多個消費者時,默認輪詢推送,但基於前一個消息刪除後纔會對其推送下個消息,故手動ack可以實現能者多勞的工作模式

 發佈訂閱(fanout)

引入交換機的概念,多個隊列綁定至同一交換機上,生產者只需要將消息投放至交換機,交換機就會通過發佈訂閱的方式將
消息廣播至其上隊列。
交換機類型
    Fanout exchange(扇型交換機)默認
    Direct exchange(直連交換機)
    Topic exchange(主題交換機)
    Headers exchange(頭交換機)

生產者 

public class ProducerFanout {

    private static final String EXCHANGE_NAME = "fanout_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        //  創建Connection
        Connection connection = RabitMQConnection.getConnection();
        // 創建Channel
        Channel channel = connection.createChannel();
        // 通道關聯交換機
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout", true);
        String msg = "發佈訂閱模式";
        channel.basicPublish(EXCHANGE_NAME, "", null, msg.getBytes());
        channel.close();
        connection.close();
    }
}

 消費者1

public class Consumer1 {

    private static final String QUEUE_NAME = "consumerFanout_1";

    private static final String EXCHANGE_NAME = "fanout_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        System.out.println("消費者1...");
        // 創建我們的連接
        Connection connection = RabitMQConnection.getConnection();
        // 創建我們通道
        final Channel channel = connection.createChannel();
        // 關聯隊列消費者關聯隊列
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
        DefaultConsumer defaultConsumer = 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("消費者1獲取消息:" + msg);
            }
        };
        // 開始監聽消息 自動簽收
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);

    }
}

 消費者2

public class Consumer2 {

    private static final String QUEUE_NAME = "consumerFanout_2";

    private static final String EXCHANGE_NAME = "fanout_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        System.out.println("消費者2...");
        // 創建我們的連接
        Connection connection = RabitMQConnection.getConnection();
        // 創建我們通道
        final Channel channel = connection.createChannel();
        // 關聯隊列消費者關聯隊列
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
        DefaultConsumer defaultConsumer = 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("消費者2獲取消息:" + msg);
            }
        };
        // 開始監聽消息 自動簽收
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);

    }
}

路由(direct)

在發佈訂閱的基礎上,引入路由key,每個隊列都可以綁定多個路由key,生產者投遞消息時,可以指定路由key,
交換機根據路由key
查找對應的隊列進行消息投放。
即路由key類似於隊列的一種屬性,方便生成者投放消息時做查詢過濾,又叫direct

 生產者

/**
 * 定義交換機的名稱
 */
private static final String EXCHANGE_NAME = "direct_exchange";

public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
    // 1.創建我們的連接
    Connection connection = RabitMQConnection.getConnection();
    // 2.創建我們通道
    Channel channel = connection.createChannel();
    // 不需要直接關心隊列,只關心交換機
    channel.exchangeDeclare(EXCHANGE_NAME, "direct", true);
    String msg = "路由直連的消息"
    channel.basicPublish(EXCHANGE_NAME, "key1", null, msg.getBytes());
    channel.close();
    connection.close();
    // 如果交換機沒有綁定隊列,消息可能會丟失
}

 消費者1

public class Consumer1 {

    private static final String QUEUE_NAME = "consumer_direct_1";
    
    private static final String EXCHANGE_NAME = "direct_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 創建我們的連接
        Connection connection = RabitMQConnection.getConnection();
        // 創建我們通道
        final Channel channel = connection.createChannel();
        // 關聯隊列消費者關聯隊列
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "key1");
        DefaultConsumer defaultConsumer = 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("消費者1獲取消息:" + msg);
            }
        };
        // 開始監聽消息 自動簽收
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);

    }
}

 消費者2

public class Consumer2 {

    private static final String QUEUE_NAME = "consumer_direct_2";
    
    private static final String EXCHANGE_NAME = "direct_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 創建我們的連接
        Connection connection = RabitMQConnection.getConnection();
        // 創建我們通道
        final Channel channel = connection.createChannel();
        // 關聯隊列消費者關聯隊列
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "key2");
        DefaultConsumer defaultConsumer = 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("消費者2獲取消息:" + msg);
            }
        };
        // 開始監聽消息 自動簽收
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);

    }
}

 主題(topic) 

在路由的基礎上,實現通配符查詢,即模糊查詢,*代表一個單詞,#代表多個單詞
這樣一來,隊列不用去綁定一個又一個的路由key,直接綁定通配符的路由key即可

 SpringBoot整合RabbitMQ

maven依賴 

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.RELEASE</version>
</parent>
<dependencies>

    <!-- springboot-web組件 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 添加springboot對amqp的支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    <!--fastjson -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.49</version>
    </dependency>
</dependencies>

application.yml

spring:
  rabbitmq:
    ####連接地址
    host: 127.0.0.1
    ####端口號
    port: 5672
    ####賬號
    username: yanxiaohui
    ####密碼
    password: yanxiaohui
    ### 地址
    virtual-host: /yxh

配置類

@Component
public class RabbitMQConfig {

    /**
     * 定義交換機
     */
    private String EXCHANGE_SPRINGBOOT_NAME = "springboot_exchange";


    /**
     * 短信隊列
     */
    private String FANOUT_SMS_QUEUE = "fanout_sms_queue";
    /**
     * 郵件隊列
     */
    private String FANOUT_SMS_EMAIL = "fanout_email_queue";

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

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

    /**
     * 創建交換機
     *
     * @return
     */
    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange(EXCHANGE_SPRINGBOOT_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);
    }
}

 生產者

@RestController
public class FanoutProducer {
    @Autowired
    private AmqpTemplate amqpTemplate;

    @RequestMapping("/sendMsg")
    public String sendMsg(String msg) {
        // 參數1 交換機名稱 、參數2路由key  參數3 消息
        amqpTemplate.convertAndSend("springboot_exchange", "", msg);
        return "success";
    }
}

 消費者

@Component
@RabbitListener(queues = "fanout_email_queue")
public class FanoutEmailConsumer {

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

死信隊列

產生的背景  

俗稱備胎隊列,用於存放消息多次消費失敗、消費超時、超過隊列最大長度被拒絕接收的消息。
消息中間件因爲某種原因拒收該消息後,可以轉移到死信隊列中存放,死信隊列也可以有交換機和路由key等。

 SpringBoot整合死信隊列

maven依賴 

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.RELEASE</version>
</parent>
<dependencies>

    <!-- springboot-web組件 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 添加springboot對amqp的支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    <!--fastjson -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.49</version>
    </dependency>
</dependencies>

application.yml

spring:
  rabbitmq:
    ####連接地址
    host: 127.0.0.1
    ####端口號
    port: 5672
    ####賬號
    username: yanxiaohui
    ####密碼
    password: yanxiaohui
    ### 地址
    virtual-host: /yxh
server:
  port: 8080

###模擬演示死信隊列
yxh:
  dlx:
    exchange: yxh_dlx_exchange
    queue: yxh_order_dlx_queue
    routingKey: dlx
  ###備胎交換機
  order:
    exchange: yxh_order_exchange
    queue: yxh_order_queue
    routingKey: yxh.order

 信隊列配置

@Component
public class DeadLetterMQConfig {
    /**
     * 訂單交換機
     */
    @Value("${yxh.order.exchange}")
    private String orderExchange;

    /**
     * 訂單隊列
     */
    @Value("${yxh.order.queue}")
    private String orderQueue;

    /**
     * 訂單路由key
     */
    @Value("${yxh.order.routingKey}")
    private String orderRoutingKey;
    /**
     * 死信交換機
     */
    @Value("${yxh.dlx.exchange}")
    private String dlxExchange;

    /**
     * 死信隊列
     */
    @Value("${yxh.dlx.queue}")
    private String dlxQueue;
    /**
     * 死信路由
     */
    @Value("${yxh.dlx.routingKey}")
    private String dlxRoutingKey;

    /**
     * 聲明死信交換機
     *
     * @return DirectExchange
     */
    @Bean
    public DirectExchange dlxExchange() {
        return new DirectExchange(dlxExchange);
    }

    /**
     * 聲明死信隊列
     *
     * @return Queue
     */
    @Bean
    public Queue dlxQueue() {
        return new Queue(dlxQueue);
    }

    /**
     * 聲明訂單業務交換機
     *
     * @return DirectExchange
     */
    @Bean
    public DirectExchange orderExchange() {
        return new DirectExchange(orderExchange);
    }

    /**
     * 綁定死信隊列到死信交換機
     *
     * @return Binding
     */
    @Bean
    public Binding binding() {
        return BindingBuilder.bind(dlxQueue())
                .to(dlxExchange())
                .with(dlxRoutingKey);
    }

    /**
     * 聲明訂單隊列
     *
     * @return Queue
     */
    @Bean
    public Queue orderQueue() {
        // 訂單隊列綁定我們的死信交換機
        Map<String, Object> arguments = new HashMap<>(2);

        arguments.put("x-dead-letter-exchange", dlxExchange);
        arguments.put("x-dead-letter-routing-key", dlxRoutingKey);
        return new Queue(orderQueue, true, false, false, arguments);
    }

    /**
     * 綁定訂單隊列到訂單交換機
     *
     * @return Binding
     */
    @Bean
    public Binding orderBinding() {
        return BindingBuilder.bind(orderQueue())
                .to(orderExchange())
                .with(orderRoutingKey);
    }

 消費者

@Component
public class OrderDlxConsumer {

    /**
     * 死信隊列監聽隊列回調的方法
     *
     * @param msg
     */
    @RabbitListener(queues = "yxh_order_dlx_queue")
    public void orderConsumer(String msg) {
        System.out.println("死信隊列消費訂單消息" + msg);
    }
}

 訂單消費者

@Component
public class OrderConsumer {

    /**
     * 監聽隊列回調的方法
     *
     * @param msg
     */
    @RabbitListener(queues = "yxh_order_queue")
    public void orderConsumer(String msg) {
        System.out.println("正常訂單消費者消息msg:" + msg);
    }
}

 生產者投遞消息

@RestController
public class DeadLetterProducer {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    /**
     * 訂單交換機
     */
    @Value("${yxh.order.exchange}")
    private String orderExchange;
    /**
     * 訂單路由key
     */
    @Value("${yxh.order.routingKey}")
    private String orderRoutingKey;

    @RequestMapping("/sendOrder")
    public String sendOrder() {
        String msg = "死信隊列的demo";
        rabbitTemplate.convertAndSend(orderExchange, orderRoutingKey, msg, new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                message.getMessageProperties().setExpiration("10000");
                return message;
            }
        });
        return "succcess";
    }
}

 消息中間件如何獲取消費結果

由於中間件的操作是異步的,所以想要獲取其操作結果,只有通過主動查詢的方式,才能知道消息是否已消費。
即通過業務生成全局的消息ID,使用消息ID去數據庫查找對應的業務是否發生了預期的變化,進而得出消息是否成功消費。
此方法也是其解決消息冪等性的思路

RabbitMQ消息冪等問題 

消息自動重試機制

在消費者消費消息的代碼中出現異常後,默認是重複執行的,(默認無數次)。
如果代碼本身就有問題,而非外部因素(網絡抖動等)影響,那麼這種重試本質無意義。
所以應該對重試機制設置重試次數和時間間隔,超過重試機制還是拋出異常的,我們可以將消息放入死信或者數據庫將其記錄,方便補償。

SpringBoot開啓重試策略

spring:
  rabbitmq:
    ####連接地址
    host: 127.0.0.1
    ####端口號
    port: 5672
    ####賬號
    username: yanxiaohui
    ####密碼
    password: yanxiaohui
    ### 地址
    virtual-host: /yxh
    listener:
      simple:
        retry:
          ####開啓消費者(程序出現異常的情況下會)進行重試
          enabled: true
          ####最大重試次數
          max-attempts: 5
          ####重試間隔次數
          initial-interval: 3000

消費者開啓重試策略

System.out.println("消費者消息msg:" + msg);
JSONObject msgJson = JSONObject.parseObject(msg);
String email = msgJson.getString("email");
String emailUrl = "http://127.0.0.1:8081/sendEmail?email=" + email;
JSONObject jsonObject = null;
try {
    jsonObject = HttpClientUtils.httpGet(emailUrl);
} catch (Exception e) {
    String errorMsg = email + ",調用第三方郵件接口失敗:" + ",錯誤原因:" + e.getMessage();
    throw new Exception(errorMsg);
}
System.out.println("郵件消費者調用第三方接口結果:" + jsonObject);

rabbitMQ如何解決消息冪等問題 

採用消息全局id根據業務來定

生產者

@RequestMapping("/sendOrderMsg")
    public String sendOrderMsg() {
        // 1.生產訂單id
        String orderId = System.currentTimeMillis() + "";
        String orderName = "生成消息冪等的訂單";
        OrderEntity orderEntity = new OrderEntity(orderName, orderId);
        String msg = JSONObject.toJSONString(orderEntity);
        sendMsg(msg, orderId);
        return orderId;
        // 後期客戶端主動使用orderId調用服務器接口 查詢該訂單id是否在數據庫中存在數據 消費成功 消費失敗
    }

    @Async
    public void sendMsg(String msg, String orderId) {
        rabbitTemplate.convertAndSend(orderExchange, orderRoutingKey, msg,
                new MessagePostProcessor() {
                    @Override
                    public Message postProcessMessage(Message message) throws AmqpException {
//                        message.getMessageProperties().setExpiration("10000");
                        message.getMessageProperties().setMessageId(orderId);
                        return message;
                    }
                });
        // 消息投遞失敗
    }

消費者 

String msg = new String(message.getBody());
System.out.println("訂單隊列獲取消息:" + msg);
OrderEntity orderEntity = JSONObject.parseObject(msg, OrderEntity.class);
if (orderEntity == null) {
    return;
}
// messageId根據具體業務來定,如果已經在數據表中插入過數據,則不會插入
String orderId = message.getMessageProperties().getMessageId();
if (StringUtils.isEmpty(orderId)) {
    // 開啓消息確認機制
    channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    return;
}
OrderEntity dbOrderEntity = orderMapper.getOrder(orderId);
if (dbOrderEntity != null) {
    // 說明已經處理過請求
    channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    return;
}

int result = orderMapper.addOrder(orderEntity);
if (result >= 0) {
    // 開啓消息確認機制
    channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}

RabbitMQ如何解決分佈式事務 

什麼是分佈式事務

官方說法:在分佈式系統中,因爲跨服務調用接口,存在多個不同的事務,每個事務都互不影響。就存在分佈式事務的問題。
說白了就是:在一個事務中,出現rpc遠程調用,其中對數據的變更脫離當前事務的管理,導致當前事務回滾時,無法將遠程事務一併回滾。

解決分佈式事務核心思想

最終一致性。分佈式領域不存在強一致性,對於短暫期間的不一致,可以允許通過補償或延時使其最終數據保持一致。

RabbitMQ解決分佈式事務的思路

1.通過消息確認機制confirm確保消息一定投遞至消息中間中
2.消費者手動ack確定消息的消費成功
3.對於消費成功但事務回滾的操作,需要進行補充,即將整個業務操作(除去消息投遞)都記錄至補償隊列,然後補償業務的數據缺失。

 Maven依賴

<dependencies>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.1.1</version>
    </dependency>
    <!-- mysql 依賴 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!-- 阿里巴巴數據源 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.0.14</version>
    </dependency>
    <!-- SpringBoot整合Web組件 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 添加springboot對amqp的支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    <!--fastjson -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.49</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

application.yml 

spring:
  rabbitmq:
    ####連接地址
    host: 127.0.0.1
    ####端口號
    port: 5672
    ####賬號
    username: yanxiaohui
    ####密碼
    password: yanxiaohui
    ### 地址
    virtual-host: /yxh
    ###開啓消息確認機制 confirms
    publisher-confirms: true
    publisher-returns: true
    listener:
      simple:
        retry:
          ####開啓消費者(程序出現異常的情況下會)進行重試
          enabled: true
          ####最大重試次數
          max-attempts: 5
          ####重試間隔次數
          initial-interval: 3000
          ###開啓ack模式
        acknowledge-mode: manual
  datasource:
    url: jdbc:mysql://localhost:3306/order?useUnicode=true&characterEncoding=UTF-8
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
server:
  port: 8080

mq配置

@Component
public class OrderRabbitMQConfig {

    /**
     * 派單隊列
     */
    public static final String ORDER_DIC_QUEUE = "order_dic_queue";
    /**
     * 補單對接
     */
    public static final String ORDER_CREATE_QUEUE = "order_create_queue";
    /**
     * 派單交換機
     */
    private static final String ORDER_EXCHANGE_NAME = "order_exchange_name";

    /**
     * 定義派單隊列
     *
     * @return
     */
    @Bean
    public Queue directOrderDicQueue() {
        return new Queue(ORDER_DIC_QUEUE);
    }

    /**
     * 定義補派單隊列
     *
     * @return
     */
    @Bean
    public Queue directCreateOrderQueue() {
        return new Queue(ORDER_CREATE_QUEUE);
    }


    /**
     * 定義訂單交換機
     *
     * @return
     */
    @Bean
    DirectExchange directOrderExchange() {
        return new DirectExchange(ORDER_EXCHANGE_NAME);
    }


    /**
     * 派單隊列與交換機綁定
     *
     * @return
     */
    @Bean
    Binding bindingExchangeOrderDicQueue() {
        return BindingBuilder.bind(directOrderDicQueue()).to(directOrderExchange()).with("orderRoutingKey");
    }

    /**
     * 補單隊列與交換機綁定
     *
     * @return
     */
    @Bean
    Binding bindingExchangeCreateOrder() {
        return BindingBuilder.bind(directCreateOrderQueue()).to(directOrderExchange()).with("orderRoutingKey");
    }
}

生產者 

@Component
@Slf4j
public class OrderProducer implements RabbitTemplate.ConfirmCallback {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private RabbitTemplate rabbitTemplate;


    @Transactional
    public String send() {
        // 1.創建訂單
        String orderId = System.currentTimeMillis() + "";
        OrderEntity orderEntity = createOrder(orderId);
        //2.將訂單添加到數據庫中(步驟一 先往數據庫中添加一條數據)
        int result = orderMapper.addOrder(orderEntity);
        if (result <= 0) {
            return orderId;
        }
        //3.使用消息中間件異步 ,分配訂單
        String sendMsgJson = JSONObject.toJSONString(orderEntity);
        send(sendMsgJson);
        int i = 1 / 0;
        return orderId;
    }

    public OrderEntity createOrder(String orderId) {
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setName("分佈式事務");
        orderEntity.setOrderCreatetime(new Date());
        // 價格是300元
        orderEntity.setOrderMoney(300d);
        // 狀態爲 未支付
        orderEntity.setOrderState(0);
        Long commodityId = 30L;
        // 商品id
        orderEntity.setCommodityId(commodityId);
        orderEntity.setOrderId(orderId);
        return orderEntity;
    }

    private void send(String sendMsg) {

        log.info(">>>生產者發送訂單數據:" + sendMsg);
        // 設置生產者消息確認機制
        this.rabbitTemplate.setMandatory(true);
        this.rabbitTemplate.setConfirmCallback(this);
        // 構建回調返回參數
        CorrelationData correlationData = new CorrelationData(sendMsg);
        String orderExchange = "order_exchange_name";
        String orderRoutingKey = "orderRoutingKey";
        rabbitTemplate.convertAndSend(orderExchange, orderRoutingKey, sendMsg, correlationData);
    }

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String s) {
        String sendMsg = correlationData.getId();
        System.out.println("生產者開始消息確認orderId:" + sendMsg);
        if (!ack) {
            // 遞歸調用發送
            send(sendMsg);
            return;
        }
        System.out.println("生產者消息確認orderId:" + sendMsg);
    }
}

消費者

@Component
public class DistriLeafleConsumer {

    @Autowired
    private DispatchMapper dispatchMapper;

    @RabbitListener(queues = "order_dic_queue")
    public void distriLeafleConsumer(Message message, Channel channel) throws IOException {
        String msg = new String(message.getBody());
        System.out.println("派代服務平臺msg:" + msg);
        JSONObject jsonObject = JSONObject.parseObject(msg);
        // 訂單id
        String orderId = jsonObject.getString("orderId");
        // 假設派單userID 1234
        Long userId = 1234L;
        DispatchEntity dispatchEntity = new DispatchEntity(orderId, userId);
        int result = dispatchMapper.insertDistribute(dispatchEntity);
        if (result >= 0) {
            // 手動ack 刪除該消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        }
    }
}

補單消費者(補償隊列的消費)

@Component
public class CreateOrderConsumer {

    @Autowired
    private OrderMapper orderMapper;

    @RabbitListener(queues = "order_create_queue")
    public void createOrderConsumer(Message message, Channel channel) throws IOException {
        String msg = new String(message.getBody());
        OrderEntity orderEntity = JSONObject.parseObject(msg, OrderEntity.class);
        String orderId = orderEntity.getOrderId();

        // 根據訂單號碼查詢該筆訂單是否創建
        OrderEntity dbOrderEntity = orderMapper.findOrderId(orderId);
        if (dbOrderEntity != null) {
            // 手動ack 刪除該消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            return;
        }
        int result = orderMapper.addOrder(orderEntity);
        if (result >= 0) {
            // 手動ack 刪除該消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        }
    }
}

 

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