SpringBoot消息重試機制
消息重試機制冪等性
如何合適選擇重試機制
情況1: 消費者獲取到消息後,調用第三方接口,但接口暫時無法訪問,是否需要重試? 需要重試
情況2: 消費者獲取到消息後,拋出數據轉換異常,是否需要重試? 不需要重試
總結:對於情況2,如果消費者代碼拋出異常是需要發佈新版本才能解決的問題,那麼不需要重試,重試也無濟於事。應該採用日誌記錄+定時任務job健康檢查+人工進行補償
消費者如果保證消息冪等性,不被重複消費
產生原因:網絡延遲傳輸中,會造成進行MQ重試中,在重試過程中,可能會造成重複消費。
解決辦法:
使用全局MessageID判斷消費方使用同一個,解決冪等性。
基於全局消息id區分消息,解決冪等性
生產者:
請求頭設置消息id(messageId)
@Component
public class FanoutProducer {
@Autowired
private AmqpTemplate amqpTemplate;
public void send(String queueName) {
String msg = "my_fanout_msg:" + System.currentTimeMillis();
Message message = MessageBuilder.withBody(msg.getBytes()).setContentType(MessageProperties.CONTENT_TYPE_JSON)
.setContentEncoding("utf-8").setMessageId(UUID.randomUUID() + "").build();
System.out.println(msg + ":" + msg);
amqpTemplate.convertAndSend(queueName, message);
}
}
消費者:
核心代碼
@Component
public class FanoutEamilConsumer {
@RabbitListener(queues = "fanout_email_queue")
public void process(Message message) throws Exception {
System.out
.println(Thread.currentThread().getName() + ",郵件消費者獲取生產者消息msg:" + new String(message.getBody(), "UTF-8")
+ ",messageId:" + message.getMessageProperties().getMessageId());
// int i = 1 / 0;
}
}
application配置
spring:
rabbitmq:
####連接地址
host: 127.0.0.1
####端口號
port: 5672
####賬號
username: guest
####密碼
password: guest
### 地址
virtual-host: /admin_host
listener:
simple:
retry:
####開啓消費者重試
enabled: true
####最大重試次數
max-attempts: 5
####重試間隔次數
initial-interval: 3000
server:
port: 8081
RabbitMQ消費者重試調用接口
//郵件隊列
@Component
public class FanoutEamilConsumer {
@RabbitListener(queues = "fanout_email_queue")
public void process(String msg) throws Exception {
System.out.println("郵件消費者獲取生產者消息msg:" + msg);
JSONObject jsonObject = JSONObject.parseObject(msg);
// 獲取email參數
String email = jsonObject.getString("email");
// 請求地址
String emailUrl = "http://127.0.0.1:8083/sendEmail?email=" + email;
JSONObject result = HttpClientUtils.httpGet(emailUrl);
if (result == null) {
// 因爲網絡原因,造成無法訪問,繼續重試
throw new Exception("調用接口失敗!");
}
System.out.println("執行結束....");
}
}
@RabbitListener(queues = "fanout_email_queue")
public void process(Message message) throws Exception {
// 獲取消息Id
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(), "UTF-8");
System.out.println("郵件消費者獲取生產者消息" + "messageId:" + messageId + ",消息內容:" + msg);
JSONObject jsonObject = JSONObject.parseObject(msg);
// 獲取email參數
String email = jsonObject.getString("email");
// 請求地址
String emailUrl = "http://127.0.0.1:8083/sendEmail?email=" + email;
JSONObject result = HttpClientUtils.httpGet(emailUrl);
if (result == null) {
// 因爲網絡原因,造成無法訪問,繼續重試
throw new Exception("調用接口失敗!");
}
System.out.println("執行結束....");
}
RabbitMQ簽收模式
//郵件隊列
@Component
public class FanoutEamilConsumer {
@RabbitListener(queues = "fanout_email_queue")
public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
System.out
.println(Thread.currentThread().getName() + ",郵件消費者獲取生產者消息msg:" + new String(message.getBody(), "UTF-8")
+ ",messageId:" + message.getMessageProperties().getMessageId());
// 手動ack
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
// 手動簽收
channel.basicAck(deliveryTag, false);
}
}
開啓手動應答
spring:
rabbitmq:
####連接地址
host: 127.0.0.1
####端口號
port: 5672
####賬號
username: guest
####密碼
password: guest
### 地址
virtual-host: /admin_host
listener:
simple:
retry:
####開啓消費者異常重試
enabled: true
####最大重試次數
max-attempts: 5
####重試間隔次數
initial-interval: 2000
####開啓手動ack
acknowledge-mode: manual
RabbitMQ死信隊列
死信隊列 聽上去像 消息“死”了 其實也有點這個意思,死信隊列 是 當消息在一個隊列 因爲下列原因:
消息被拒絕(basic.reject/ basic.nack)並且不再重新投遞 requeue=false
消息超期 (rabbitmq Time-To-Live -> messageProperties.setExpiration())
隊列超載
變成了 “死信” 後 被重新投遞(publish)到另一個Exchange 該Exchange 就是DLX 然後該Exchange 根據綁定規則 轉發到對應的 隊列上 監聽該隊列 就可以重新消費 說白了 就是 沒有被消費的消息 換個地方重新被消費
生產者 --> 消息 --> 交換機 --> 隊列 --> 變成死信 --> DLX交換機 -->隊列 --> 消費者
什麼是死信呢?什麼樣的消息會變成死信呢?
消息被拒絕(basic.reject或basic.nack)並且requeue=false.
消息TTL過期
隊列達到最大長度(隊列滿了,無法再添加數據到mq中)
應用場景分析
在定義業務隊列的時候,可以考慮指定一個死信交換機,並綁定一個死信隊列,當消息變成死信時,該消息就會被髮送到該死信隊列上,這樣就方便我們查看消息失敗的原因了
如何使用死信交換機呢?
定義業務(普通)隊列的時候指定參數
x-dead-letter-exchange: 用來設置死信後發送的交換機
x-dead-letter-routing-key:用來設置死信的routingKey
死信隊列環境搭建
死信隊列配置
生產者配置
@Component
public class FanoutConfig {
/**
* 定義死信隊列相關信息
*/
public final static String deadQueueName = "dead_queue";
public final static String deadRoutingKey = "dead_routing_key";
public final static String deadExchangeName = "dead_exchange";
/**
* 死信隊列 交換機標識符
*/
public static final String DEAD_LETTER_QUEUE_KEY = "x-dead-letter-exchange";
/**
* 死信隊列交換機綁定鍵標識符
*/
public static final String DEAD_LETTER_ROUTING_KEY = "x-dead-letter-routing-key";
// 郵件隊列
private String FANOUT_EMAIL_QUEUE = "fanout_email_queue";
// 短信隊列
private String FANOUT_SMS_QUEUE = "fanout_sms_queue";
// fanout 交換機
private String EXCHANGE_NAME = "fanoutExchange";
// 1.定義郵件隊列
@Bean
public Queue fanOutEamilQueue() {
// 將普通隊列綁定到死信隊列交換機上
Map<String, Object> args = new HashMap<>(2);
args.put(DEAD_LETTER_QUEUE_KEY, deadExchangeName);
args.put(DEAD_LETTER_ROUTING_KEY, deadRoutingKey);
Queue queue = new Queue(FANOUT_EMAIL_QUEUE, true, false, false, args);
return queue;
}
// 2.定義短信隊列
@Bean
public Queue fanOutSmsQueue() {
return new Queue(FANOUT_SMS_QUEUE);
}
// 2.定義交換機
@Bean
FanoutExchange fanoutExchange() {
return new FanoutExchange(EXCHANGE_NAME);
}
// 3.隊列與交換機綁定郵件隊列
@Bean
Binding bindingExchangeEamil(Queue fanOutEamilQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanOutEamilQueue).to(fanoutExchange);
}
// 4.隊列與交換機綁定短信隊列
@Bean
Binding bindingExchangeSms(Queue fanOutSmsQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanOutSmsQueue).to(fanoutExchange);
}
/**
* 配置死信隊列
*
* @return
*/
@Bean
public Queue deadQueue() {
Queue queue = new Queue(deadQueueName, true);
return queue;
}
@Bean
public DirectExchange deadExchange() {
return new DirectExchange(deadExchangeName);
}
@Bean
public Binding bindingDeadExchange(Queue deadQueue, DirectExchange deadExchange) {
return BindingBuilder.bind(deadQueue).to(deadExchange).with(deadRoutingKey);
}
}
消費者配置
@RabbitListener(queues = "fanout_email_queue")
public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(), "UTF-8");
System.out.println("郵件消費者獲取生產者消息msg:" + msg + ",消息id:" + messageId);
JSONObject jsonObject = JSONObject.parseObject(msg);
Integer timestamp = jsonObject.getInteger("timestamp");
try {
int result = 1 / timestamp;
System.out.println("result:" + result);
// 通知mq服務器刪除該消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
e.printStackTrace();
// // 丟棄該消息
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
@Component
public class DeadConsumer {
@RabbitListener(queues = "dead_queue")
public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(), "UTF-8");
System.out.println("死信郵件消費者獲取生產者消息msg:" + msg + ",消息id:" + messageId);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
MQ解決分佈式事務三個重要概念
1、 確保生產者消息一定要投遞到MQ服務器中 Confirm機制
2、 確保消費者能夠正確的消費消息,採用手動ACK(注意冪等)
3、 如何保證第一個事務一定要創建成功(在創建一個補單的隊列,綁定同一個交換機,檢查訂單數據是否已經創建在數據庫中 實現補償機制)
生產者 一定確保消息投遞到MQ服務器(使用)
RabbitMQ解決分佈式事務問題
RabbitMQ解決分佈式事務原理: 採用最終一致性原理。 需要保證以下三要素 1、確認生產者一定要將數據投遞到MQ服務器中(採用MQ消息確認機制) 2、MQ消費者消息能夠正確消費消息,採用手動ACK模式(注意重試冪等性問題) 3、如何保證第一個事務先執行,採用補償機制,在創建一個補單消費者進行監聽,如果訂單沒有創建成功,進行補單。
訂單項目
生產者
@Service
public class OrderService extends BaseApiService implements RabbitTemplate.ConfirmCallback {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RabbitTemplate rabbitTemplate;
public ResponseBase addOrderAndDispatch() {
OrderEntity orderEntity = new OrderEntity();
orderEntity.setName("螞蟻課堂永久會員充值");
orderEntity.setOrderCreatetime(new Date());
// 價格是300元
orderEntity.setOrderMoney(300d);
// 狀態爲 未支付
orderEntity.setOrderState(0);
Long commodityId = 30l;
// 商品id
orderEntity.setCommodityId(commodityId);
String orderId = UUID.randomUUID().toString();
orderEntity.setOrderId(orderId);
// ##################################################
// 1.先下單,創建訂單 (往訂單數據庫中插入一條數據)
int orderResult = orderMapper.addOrder(orderEntity);
System.out.println("orderResult:" + orderResult);
if (orderResult <= 0) {
return setResultError("下單失敗!");
}
// 2.使用消息中間件將參數存在派單隊列中
send(orderId);
return setResultSuccess();
}
private void send(String orderId) {
JSONObject jsonObect = new JSONObject();
jsonObect.put("orderId", orderId);
String msg = jsonObect.toJSONString();
System.out.println("msg:" + msg);
// 封裝消息
Message message = MessageBuilder.withBody(msg.getBytes()).setContentType(MessageProperties.CONTENT_TYPE_JSON)
.setContentEncoding("utf-8").setMessageId(orderId).build();
// 構建回調返回的數據
CorrelationData correlationData = new CorrelationData(orderId);
// 發送消息
this.rabbitTemplate.setMandatory(true);
this.rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.convertAndSend("order_exchange_name", "orderRoutingKey", message, correlationData);
}
// 生產消息確認機制
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String orderId = correlationData.getId();
System.out.println("消息id:" + correlationData.getId());
if (ack) {
System.out.println("消息發送確認成功");
} else {
send(orderId);
System.out.println("消息發送確認失敗:" + cause);
}
}
}
補單消費者
@Component
public class CreateOrderConsumer {
@Autowired
private OrderMapper orderMapper;
@RabbitListener(queues = "order_create_queue")
public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(), "UTF-8");
System.out.println("補單消費者" + msg + ",消息id:" + messageId);
JSONObject jsonObject = JSONObject.parseObject(msg);
String orderId = jsonObject.getString("orderId");
// 判斷訂單是否存在,如果不存在 實現自動補單機制
OrderEntity orderEntityResult = orderMapper.findOrderId(orderId);
if (orderEntityResult != null) {
System.out.println("訂單已經存在 無需補單 orderId:" + orderId);
return;
}
// 訂單不存在 ,則需要進行補單
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);
// ##################################################
// 1.先下單,創建訂單 (往訂單數據庫中插入一條數據)
try {
int orderResult = orderMapper.addOrder(orderEntity);
System.out.println("orderResult:" + orderResult);
if (orderResult >= 0) {
// 手動簽收消息,通知mq服務器端刪除該消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
} catch (Exception e) {
// 丟棄該消息
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
}
RabbitmqConfig
@Component
public class RabbitmqConfig {
// 下單並且派單存隊列
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";
// 1.定義訂單隊列
@Bean
public Queue directOrderDicQueue() {
return new Queue(ORDER_DIC_QUEUE);
}
// 2.定義補訂單隊列
@Bean
public Queue directCreateOrderQueue() {
return new Queue(ORDER_CREATE_QUEUE);
}
// 2.定義交換機
@Bean
DirectExchange directOrderExchange() {
return new DirectExchange(ORDER_EXCHANGE_NAME);
}
// 3.訂單隊列與交換機綁定
@Bean
Binding bindingExchangeOrderDicQueue() {
return BindingBuilder.bind(directOrderDicQueue()).to(directOrderExchange()).with("orderRoutingKey");
}
// 3.補單隊列與交換機綁定
@Bean
Binding bindingExchangeCreateOrder() {
return BindingBuilder.bind(directCreateOrderQueue()).to(directOrderExchange()).with("orderRoutingKey");
}
}
派單服務
消費者
@Component
public class DispatchConsumer {
@Autowired
private DispatchMapper dispatchMapper;
@RabbitListener(queues = "order_dic_queue")
public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(), "UTF-8");
System.out.println("派單服務平臺" + msg + ",消息id:" + messageId);
JSONObject jsonObject = JSONObject.parseObject(msg);
String orderId = jsonObject.getString("orderId");
if (StringUtils.isEmpty(orderId)) {
// 日誌記錄
return;
}
DispatchEntity dispatchEntity = new DispatchEntity();
// 訂單id
dispatchEntity.setOrderId(orderId);
// 外賣員id
dispatchEntity.setTakeoutUserId(12l);
// 外賣路線
dispatchEntity.setDispatchRoute("40,40");
try {
int insertDistribute = dispatchMapper.insertDistribute(dispatchEntity);
if (insertDistribute > 0) {
// 手動簽收消息,通知mq服務器端刪除該消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
} catch (Exception e) {
e.printStackTrace();
// // 丟棄該消息
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
}