RabbitMQ解決分佈式事務問題

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);
		}
	}

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