可靠mq(四)——mq必定發送

問題

一般發送mq的場景爲:本地修改業務數據,數據修改成功,調用RabbitTemplate.send方法發送mq消息。

1、爲了避免長事務,一般是把RabbitTemplate.send方法放在保存業務數據的事務之外。這樣就可能出現,業務數據修改成功了,但是調用RabbitTemplate方法失敗,導致mq訂閱方不知道數據發生變更。

2、可能出現,業務數據保存成功之後,在發送mq之前,代碼拋出異常或者服務掛了,導致mq消息沒有發送出去。

3、即使調用RabbitTemplate.send方法成功了,但是可能出現mq服務在把消息落盤之前就掛了,導致重啓後消息丟失。

可靠mq最重要的就是解決mq必定被髮送成功的問題。

解決問題的大方向

要讓mq消息一定被髮送出去,可以在本地業務庫創建一張mq發送表,利用mysql的事務來實現,方案大致如下:

1、開啓事務

2、修改本地業務數據

3、事務提交之前,保存下要發送的mq消息,狀態爲“發送中”

4、事務提交之後,調用RabbitTemplate.send方法發送mq消息

5、利用rabbitMq的確認機制,添加消息發送回調監聽:成功到達exchange,刪除步驟1中保存的mq消息;到達exchange失敗,發送失敗次數+1,修改消息狀態(到達最大重試次數,狀態改爲“失敗”)

6、利用定時任務,定時查詢狀態爲“發送中”的mq消息。重新調用RabbitTemplate.send方法發送mq消息

消息發送失敗後,就會一直重複步驟5、6,直到mq消息被成功發送到exchange或者到達最大重試次數。

相關的流程圖如下:

mq-platform實現方式

上述方案實現的難點在於:

1、在事務提交之前保存要發送的mq消息,把mq消息和業務數據作爲一個事務的數據,一起落庫

2、在事務提交之後用RabbitTemplate發送mq消息

看到“提交之前”和“提交之後”這些字眼,很容易就想到做一個攔截器,攔截spring的事務,這樣就可以做我們需要的增強。

spring事務管理其實已經提供了這種機制,不需要我們去造輪子,偉大的org.springframework.transaction.suppor.TransactionSynchronization接口!!!

該接口代碼如下:

public interface TransactionSynchronization extends Flushable {
    int STATUS_COMMITTED = 0;
    int STATUS_ROLLED_BACK = 1;
    int STATUS_UNKNOWN = 2;
 
    //掛起
    default void suspend() {}
    //恢復
    default void resume() {}
    //刷新
    default void flush() {}
    //提交之前,只有不發生異常纔會進入該方法
    default void beforeCommit(boolean readOnly) {}
    //沒搞懂有啥作用,感覺都被只是出發afterCompletion
    default void beforeCompletion() {}
    //事務成功提交之後
    default void afterCommit() {}
    //事務成功提交或回滾之後
    default void afterCompletion(int status) {}
}

 

只要實現這些方法,就可以達到我們想要的效果。例如實現beforeCommit方法,提交前保存mq消息;實現afterCommit,提交事務後發送mq消息。

在mq-platform中,具體的代碼在mq-platform-producer模塊的DatabaseRabbitProducerTransactionListener。

關於步驟5提到的回調,是指實現org.springframework.amqp.rabbit.core.RabbitTemplate$ConfirmCallback接口,實現類在mq-platform-producer模塊的com.fangdd.mqplatform.producer.callback.RabbitConfirmCallback

RabbitConfirmCallback:

@Slf4j
public class RabbitConfirmCallback implements RabbitTemplate.ConfirmCallback {
    private ProducerManager<RabbitProducer> producerManager;
    private Alarm alarm;
 
    public RabbitConfirmCallback(ProducerManager<RabbitProducer> producerManager, Alarm alarm) {
        this.producerManager = producerManager;
        this.alarm = alarm;
    }
 
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        try {
            if (Objects.nonNull(correlationData) && !StringUtils.isEmpty(correlationData.getId())) {
                String correlationDataJsonString = JSON.toJSONString(correlationData);
                log.info("receive publish confirm, correlationData: {}", correlationDataJsonString);
                RabbitCorrelationId id = RabbitCorrelationId.parseRabbitCorrelationId(correlationData.getId());
                if (Objects.isNull(id)) {
                    log.info("fail to parse correlationId:{}", correlationData.getId());
                    return;
                }
                if (ack) {
                    log.info("success to send message, correlationId:{}", correlationData.getId());
                    producerManager.deleteMq(id.getApplication(), id.getMessageId());
                } else {
                    throw new MqPlatformException("fail to send message, correlationId:" + correlationData.getId());
                }
            } else {
                if (ack) {
                    log.warn("success to send message, but lack of message id");
                } else {
                    throw new MqPlatformException("fail to send message and lack of message id");
                }
            }
        } catch (MqPlatformException e) {
            log.warn("send mq confirm fail", e);
            if (Objects.nonNull(alarm)) {
                alarm.failWhenProduce(e);
            }
        }
    }
}

原理就是發送mq的時候,設置關聯數據,關聯數據中包含了消息是屬於哪個應用和消息id。當接收到回調消息時,再根據關聯數據,操作對應的mq消息(刪除或修改狀態信息)

步驟6中,對於發送到mq服務失敗的消息,則是利用xxl-job來實現輪詢發送失敗的mq消息。相關代在mq-platform-producer模塊的RabbitSendMqJob類

RabbitSendMqJob:

@Slf4j
@JobHandler(value = "rabbitSendMqJobHandler")
public class RabbitSendMqJob extends IJobHandler {
    private static final int MAX_BATCH_SIZE = 100;
    private ProducerManager<RabbitProducer> producerManager;
    private RabbitService rabbitService;
    private String application;
    private int batchSize;
    private int loopCount = 1;
 
    public RabbitSendMqJob(
        ProducerManager<RabbitProducer> producerManager,
        RabbitService rabbitService,
        String application,
        int batchSize) {
        this.producerManager = producerManager;
        this.rabbitService = rabbitService;
        this.application = application;
        if (batchSize > MAX_BATCH_SIZE) {
            this.loopCount = batchSize / MAX_BATCH_SIZE + (batchSize % MAX_BATCH_SIZE > 0 ? 1 : 0);
            this.batchSize = MAX_BATCH_SIZE;
        } else {
            this.batchSize = batchSize;
        }
    }
 
    @Override
    public ReturnT<String> execute(String s) throws Exception {
        int i = 0;
        while (i < loopCount) {
            int size = clearRecord(batchSize);
            if (size < 1) {
                break;
            }
            i++;
        }
        return SUCCESS;
    }
 
    private int clearRecord(int batchSize) {
        List<RabbitProducer> list;
        if (StringUtils.isEmpty(application)) {
            list = producerManager.listAllApplicationMq(MessageSendStatusEnum.SENDING, 0, batchSize);
        } else {
            list = producerManager.listSendingMq(application, 0, batchSize);
        }
        if (CollectionUtils.isEmpty(list)) {
            return 0;
        }
        for (RabbitProducer producer : list) {
            try {
                producerManager.failSendMq(application, producer.getMessageId());
                rabbitService.send(producer);
            } catch (Throwable ignore) {}
        }
        return list.size();
    }
}

用於保存待發送mq消息的表結構爲:

CREATE TABLE `rabbit_producer` (
  `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  `type` TINYINT(3) NOT NULL DEFAULT 0 COMMENT '消息類型,0=即時消息,1=順序消息',
  `application` VARCHAR(50) NOT NULL COMMENT '發送消息的應用名稱',
  `virtual_host` VARCHAR(255) NOT NULL COMMENT '虛擬主機',
  `exchange` VARCHAR(255) NULL COMMENT '交換器',
  `routing_key` VARCHAR(255) NULL COMMENT '路由key',
  `message_id` VARCHAR(50) NOT NULL COMMENT '消息id',
  `body` TEXT NOT NULL COMMENT '消息內容',
  `group_name` VARCHAR(50) DEFAULT NULL COMMENT '消息分組,同分組內,消息序號按發送順序遞增',
  `send_status` TINYINT(3) NOT NULL DEFAULT 0 COMMENT '發送狀態, 0=預提交,1=發送中,2=發送失敗',
  `retry_times` SMALLINT(6) NOT NULL DEFAULT 0 COMMENT '重試次數',
  `max_retry_times` SMALLINT(6) NOT NULL COMMENT '最大重試次數',
  `next_retry_time` DATETIME DEFAULT NULL COMMENT '下一次重試時間',
  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
  `update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP  COMMENT '修改時間',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_message_app` (`message_id`, `application`)
) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8mb4 COMMENT = 'rabbitMq發送表';

application是爲了以後拓展爲由mq.platform.ip.fdd重發mq消息所預留

group_name是爲了實現順序消費增加的字段

send_status沒有“發送成功”的值,是因爲發送成功就會刪除記錄

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