學而時習之,不亦說乎。今天總結一下常用的分佈式事務的處理方法,一般有XA、TCC和消息中間件最終一致性三種解決方案。XA是採用二段提交的方法實現強一致性,現在基本沒人用,簡單點說就是一個應用操作多個數據源,常用方案就是springboot+JTA,有些數據連接池框架像druid之類的也支持,效率超低,還不如改微服務;TCC是指try、confirm、cancel,即執行、確認、回滾,比如A銀行向B銀行轉賬,發起轉賬請求後,先調A的接口扣錢,再調B的接口加錢,如果B出異常,A通過代碼回滾,這個解決方案的缺點是要封裝的回滾機制代碼太多,不好維護,現在一般也不採用這種方法;消息中間件最終一致性方案是目前最常用的解決方案,原理與TCC類似,即業務模塊先向A發送準備消息,如果A能收到並返回確認消息,A就執行扣款操作,待A執行完業務操作再向B,處理過程和A一樣,全部正常返回,則返回完成信息,如發現異常,再通知A回滾業務,但這種方式的開發量要小得多,這種方式充分利用MQ的解耦作用和消息確認機制,降低了開發難度,開發者只需要處理生產者的準備消息和消費者的消費確認消息是否正常,就能實現最終一致性。
在實現層面上比較常用的是使用RabbitMQ消息中間件,當然其他的RocketMQ、Kafka等也能實現,只是RabbitMQ提供ConfirmCallback,開發起來更簡單一些,所以本次主要總結一下RabbitMQ的消息確認。
RabbitMQ的消息確認。消息確認是保證消息傳遞可靠性的重要步驟,持久化只能保證消息不丟失,但是如果消息如果投遞失敗我們怎麼進行補償操作呢?解決辦法就是實現回調函數進行操作,在消息的發送和消息的消費都可以進行補償操作,下面我們就要講解消息確認。
消息確認種類
消息的確認做有很多法,其中包括事務機制、批量確認、異步確認等。
事務機制:我們在channel對象中可以看到 txSelect(),txCommit(),txrollback() 這些方法,分別對應着開啓事務,提交事務,回滾。由於使用事務會造成生產者與Broker交互次數增加,造成性能資源的浪費,而且事務機制是阻塞的,在發送一條消息後需要等待RabbitMq迴應,之後才能發送下一條,因此事務機制不提倡,大家在網上也很少看到RabbitMq使用事務進行消息確認的。
批量確認:批量其實是一個節約資源的操作,但是在RabbitMq中我們使用批量操作會造成消息重複消費,原因是批量操作是使客戶端程序定期或者消息達到一定量,來調用方法等待Broker返回,這樣其實是一個提高效率的做法,但是如果出現消息重發的情況,當前這批次的消息都需要重發,這就造成了重複消費,因此批量確認的操作性能沒有提高反而下降。
異步確認:異步確認雖然編程邏輯比上兩個要複雜,但是性價比最高,無論是可靠性還是效率都沒得說,他是利用回調函數來達到消息可靠性傳遞的,筆者接觸過RocketMq,這個中間件也是通過函數回調來保證是否投遞成功,下面就讓我們來詳細講解異步確認是怎麼實現的。
每一個顏色塊之間都存在着消息的確認機制,我們大概分爲兩大類,發送方確認和接收方確認,其中發送方確認又分爲生產者到交換器到確認和交換器到隊列的確認。
消息發送確認
ConfirmCallback
ConfirmCallback是一個回調接口,消息發送到 Broker 後觸發回調,確認消息是否到達 Broker 服務器,也就是隻確認是否正確到達 Exchange 中。
我們需要在生產者的配置中添加下面配置,表示開啓發布者確認
spring.rabbitmq.publisher-confirms=true
然後在生產者的Java配置類實現該接口
@Component
public class RabbitTemplateConfig implements RabbitTemplate.ConfirmCallback{
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void initRabbitTemplate() {
// 設置生產者消息確認
rabbitTemplate.setConfirmCallback(this);
}
/**
* 消息發送到 Broker 後觸發回調,確認消息是否到達 Broker 服務器,也就是隻確認是否正確到達 Exchange 中
*
* @param correlationData
* @param b
* @param s
*/
@Override
public void confirm(@Nullable CorrelationData correlationData, boolean b, @Nullable String s) {
System.out.println("ack:[{}]" + b);
if (b) {
System.out.println("消息到達rabbitmq服務器");
} else {
System.out.println("消息可能未到達rabbitmq服務器");
}
}
ReturnCallback
通過實現 ReturnCallback 接口,啓動消息失敗返回,此接口是在交換器路由不到隊列時觸發回調,該方法可以不使用,因爲交換器和隊列是在代碼裏綁定的,如果消息成功投遞到Broker後幾乎不存在綁定隊列失敗,除非你代碼寫錯了。
使用此接口需要在生產者配置中加入一下配置,表示發佈者返回
spring.rabbitmq.publisher-returns=true
然後基於剛纔的生產者Java配置裏實現接口ReturnCallback
@Component
public class RabbitTemplateConfig implements RabbitTemplate.ReturnCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void initRabbitTemplate() {
rabbitTemplate.setReturnCallback(this);
}
/**
* 啓動消息失敗返回,比如路由不到隊列時觸發回調
*
* @param message
* @param i
* @param s
* @param s1
* @param s2
*/
@Override
public void returnedMessage(Message message, int i, String s, String s1, String s2) {
System.out.println("消息主體 message : " + message);
System.out.println("消息主體 replyCode : " + i);
System.out.println("描述 replyText:" + s);
System.out.println("消息使用的交換器 exchange : " + s1);
System.out.println("消息使用的路由鍵 routing : " + s2);
}
}
以上兩段Java配置可以寫在一個類裏。
到此,我們完成了生產者的異步確認,我們可以在回調函數中對當前失敗的消息進行補償,這樣保證了我們沒有發送成功的數據也被觀察到了,比如某某條數據需要發送到消費者消費,但是沒有發送成功,這就需要你在此做一些其他操作嘍,根據你具體業務來。
消息消費確認
消費者確認發生在監聽隊列的消費者處理業務失敗,如,發生了異常,不符合要求的數據……,這些場景我們就需要手動處理,比如重新發送或者丟棄。
我們知道ACK是默認是自動的,自動確認會在消息發送給消費者後立即確認,但存在丟失消息的可能,如果消費端消費邏輯拋出異常,加入你用回滾了也只是保證了數據的一致性,但是消息還是丟了,也就是消費端沒有處理成功這條消息,那麼就相當於丟失了消息。
消息確認模式有:
AcknowledgeMode.NONE:自動確認
AcknowledgeMode.AUTO:根據情況確認
AcknowledgeMode.MANUAL:手動確認
需要在消費者的配置里加手動 ack(確認)則需要修改確認模式爲 manual,手動確認的方式有很多,可以在RabbitListenerContainerFactory類進行設置。
spring.rabbitmq.listener.direct.acknowledge-mode=MANUAL
消費者類
@Service
public class AsyncConfirmConsumer {
@RabbitListener(queues = "confirm_queue")
@RabbitHandler
public void asyncConfirm(Order order, Message message, Channel channel) throws IOException {
try {
System.out.println("消費消息:" + order.getName());
// int a = 1 / 0;
channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
System.out.println("消費消息確認" + message.getMessageProperties().getConsumerQueue() + ",接收到了回調方法");
} catch (Exception e) {
//重新回到隊列
// channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
// System.out.println("嘗試重發:" + message.getMessageProperties().getConsumerQueue());
//requeue =true 重回隊列,false 丟棄
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
// TODO 該消息已經導致異常,重發無意義,自己實現補償機制
}
}
}
需要注意的 basicAck 方法需要傳遞兩個參數
deliveryTag(唯一標識 ID):當一個消費者向 RabbitMQ 註冊後,會建立起一個 Channel ,RabbitMQ 會用 basic.deliver 方法向消費者推送消息,這個方法攜帶了一個 delivery tag, 它代表了 RabbitMQ 向該 Channel 投遞的這條消息的唯一標識 ID,是一個單調遞增的正整數,delivery tag 的範圍僅限於 Channel
multiple:爲了減少網絡流量,手動確認可以被批處理,當該參數爲 true 時,則可以一次性確認 delivery_tag 小於等於傳入值的所有消息
basicNack方法需要傳遞三個參數
deliveryTag(唯一標識 ID):上面已經解釋了。
multiple:上面已經解釋了。
requeue: true :重回隊列,false :丟棄,我們在nack方法中必須設置 false,否則重發沒有意義。
basicReject方法需要傳遞兩個參數
deliveryTag(唯一標識 ID):上面已經解釋了。
requeue:上面已經解釋了,在reject方法裏必須設置true。
還要說明一下,建議大家不要重發,重發後基本還是失敗,因爲出現問題一般都是異常導致的,出現異常的話,我的觀點是丟棄這個消息,然後在catch裏做補償操作。
到此,我們都已經準備好了,可以進行測試,我把剩餘相關代碼都寫在一起了。
@RestController
public class AsyncConfirmController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/async/{id}")
public String AETest(@PathVariable Integer id) {
Order order = new Order(id, "胖虎");
rabbitTemplate.convertAndSend("confirm_exchange", "", order);
return "成功";
}
---------------------------------
@Configuration
public class AsyncConfirmListener {
@Bean
public Queue confirmQueue() {
return new Queue("confirm_queue");
}
@Bean
public FanoutExchange confirmExchange() {
return new FanoutExchange("confirm_exchange");
}
//交換器綁定隊列
@Bean
Binding bindingExchangeConfirm(Queue confirmQueue, FanoutExchange confirmExchange) {
return BindingBuilder.bind(confirmQueue).to(confirmExchange);
}
}