可靠mq(二)——mq冪等消費

問題

假設報備服務和積分服務之間是用mq消息進行交互,經紀人報備後,報備服務發出積分加5的mq消息,積分服務接到mq之後就會給經紀人帳號加5積分。

如果積分服務多次接收到同一個消息,就會執行多次積分加5的動作,導致經紀人的積分多加了

解決問題的大方向

1、接收到mq消息後,根據消息id查詢是否已經消費過該消息。如果消費過該消息,轉步驟5;如果沒有消費過,轉步驟2

2、保存mq消費記錄,保存成功,轉步驟3,保存失敗,轉步驟5(多消費者的情況下,保證mq消息也只被消費一次!!!)

3、執行正常的mq消費邏輯,消費成功,轉步驟5,消費失敗,轉步驟4

4、消費失敗,刪除mq消費記錄,nack消息並requeue,結束(確保mq消息下次到達時,可以被消費)

5、消費成功,ack確認mq消息,結束

流程圖如下:

mq-platform-consumer要解決的問題

如何拿到消息id

彎路:

一開始想到的是,用AOP對加了@RabbitListener的註解的方法添加一個arround類型的增強。但現實給了我一巴掌,使用@RabbitListener註解的方法的入參,不強制要求是org.springframework.amqp.core,Message,看了源碼,在MessagingMessageListenerAdapter中,會將Message的消息體做轉換,匹配@RabbitListener方法的入參,所以不強制要求參數類型。

想着,對@RabbitListener方法加AOP行不通,因爲沒有Message參數,那如果,對@RabbitListener方法的調用者用AOP,是不是就可以呢?!實際上也行不通,看了源碼,那些類是手動new出來的,不歸spring管理。。。。。。

還有另外一個更嚴重的問題,用自己定義的MessageListenerContainer的bean來消費消息,根本就不會涉及到@RabbitListener註解。

所以,用aop針對@RabbitListener做增強來實現冪等的方案根本無法實現

正確的道路:

看spring集成rabbitMq源碼的時候,發現幾點:

1、監聽mq消息,是用SimpleRabbitListenerContainerFactory類來產生SimpleRabbitListenerContainer對象,由SimpleRabbitListenerContainer對象來new消費者進行消息監聽

2、SimpleRabbitListenerContainerFactory有一個屬性adviceChain,類似一個責任鏈,在創建SimpleRabbitListenerContainer對象的時候,會把adviceChain的值設置給SimpleRabbitListenerContainer對象。SimpleRabbitListenerContainer監聽到消息的時候,mq消息先經過adviceChain的所有攔截,纔會到達@RabbitListener註解的方法

3、RabbitAutoConfirguration是通過用BeanPostProcessor來爲@RabbitListener方法產生SimpleRabbitListenerContainer對象

利用這三個信息,我們就可以解決AOP方案遇到的問題:

1、定義一個IdempotentOperationsInterceptor類,拿到Message參數中的messageId屬性,做消費檢查

@Slf4j
public class IdempotentOperationsInterceptor extends AbstractConsumerOperationsInterceptor {
    private ConsumerManager consumerManager;
 
    public IdempotentOperationsInterceptor(ConsumerManager consumerManager) {
        this.consumerManager = consumerManager;
    }
 
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Message message = getMessage(invocation);
        if (Objects.isNull(message)) {
            return invocation.proceed();
        }
        String messageId = getMessageId(message);
        if (StringUtils.isEmpty(messageId)) {
            return invocation.proceed();
        }
        if (!consumerManager.insertConsumeRecord(messageId)) {
            return null;
        }
 
        boolean success = false;
        Object result;
        try {
            result = invocation.proceed();
            success = true;
        } catch (Throwable e) {
            Cat.logMetricForCount("consume_message_failed");
            log.warn("fail to consume message: {}", message.toString());
            throw e;
        } finally {
            if (!success) {
                try {
                    // TODO 1、觸發告警;2、考慮死信機制,例如與刪除冪等記錄的同一個事務中保存mq消息
                    consumerManager.deleteConsumeRecord(messageId);
                } catch (Throwable e) {
                    log.warn("fail to delete consume message: {}", message.toString());
                }
            }
        }
        return result;
    }
 
    private String getMessageId(Message message) {
        return Objects.nonNull(message.getMessageProperties()) ? message.getMessageProperties().getMessageId() : null;
    }
}

2、同樣使用BeanPostProcessor,把IdempotentOperationsInterceptor類的實例,設置到SimpleRabbitListenerContainerFactory對象adviceChain屬性的數組尾部(該BeanPostProcessor的優先級略高於RabbitAutoConfiguration使用的BeanPostProcessor,詳細代碼見SimpleRabbitListenerContainerFactoryBeanPostProcessor類)

 

3、將IdempotentOperationsInterceptor設置到SimpleRabbitListenerContainerFactory對象,可以解決加了@RabbitListener註解的方法的冪等,但是對於那些自己手動定義的MessageListenerContainer的bean,卻不起作用。這時候我們可以沿用套路,再增加一個RabbitListenerContainerBeanPostProcessor,該後置處理,將對所有類型是AbstractMessageListenerContainer的bean的adviceChain屬性做處理,將IdempotentOperationsInterceptor強制設置到adviceChain屬性上

用什麼保存消息消費記錄

在公司技術體系下,可以考慮用mysql和redis存儲消費記錄

使用mysql,需要在業務庫中引入一張表:

列名

意義

id 自增id
message_id 消息id
application 消息來自哪個應用,防止不同應用有相同的消息id
create_time 創建時間
update_time 更新時間,該字段沒存在的必要,但是沒有更新時間,不給建表。。。

message_id+application組成一個唯一索引,利用唯一性,可以做到避免多消費者併發消費的問題

使用redis,很方便,單線程,天然能解決併發問題,而且可以設置過期時間,清理歷史數據

mq-platform-consumer中,目前是選擇用mysql。因爲關係型數據庫,支持事務!!在插入消費記錄之前開啓事務,在成功消費mq消息之後,提交事務,消費失敗則回滾事務。這樣可以避免出現mq消費失敗之後,清理消費記錄失敗,導致消息無法被消費的問題。TransactionIdempotentOperationsInterceptor類就是支持事務的冪等檢查。

目前這個方案有些缺陷,就是消費mq的業務邏輯中,如果涉及到大量的遠程調用,會造成長事務。

如果是這種情況,就要考慮使用redis了,但使用redis,就需要用上redis實現分佈式鎖,解決併發消費的問題。

 

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