목차
3. 소비자가 반복적으로 메시지를 소비하는 것을 방지하는 방법
1. 메시지의 신뢰성을 확보하는 방법
1.1 안정적인 메시지 전달
프로덕션 환경에서 몇 가지 알 수 없는 이유로 rabbitmq가 다시 시작되고 RabbitMQ를 다시 시작하는 동안 생산자 메시지 전달이 실패하여 메시지 손실이 발생하므로 수동 처리 및 복구가 필요합니다. 그래서 RabbitMQ 메시지를 안정적으로 전달하는 방법에 대해 생각하기 시작했습니다. 특히 이러한 극단적인 상황에서 RabbitMQ 클러스터를 사용할 수 없을 때 배달할 수 없는 메시지를 처리하는 방법은 무엇입니까?
메시지 발신자로 RabbitMQ를 사용할 때 메시지 손실 또는 배달 실패 시나리오를 방지하려고 합니다. RabbitMQ는 메시지 전달의 신뢰성 모드를 제어하는 두 가지 방법을 제공합니다 .
- 확인 확인 모드
- 반환 반환 모드
- 생산자에서 교환으로 보내는 메시지는 ConfirmCallback을 반환합니다.
- 메시지가 교환-->대기열에서 배달되지 않으면 returnCallback이 반환됩니다.
기본적으로 rabbitmq는 위의 두 가지 모드를 활성화하지 않습니다.
이 두 콜백을 사용하여 안정적인 메시지 전달을 제어합니다.
확인 및 반품 이행
확인 모드를 사용하려면 ConnectionFactory의 Publisher-confirm-type: correlation을 설정하십시오.
콜백 함수를 설정하려면 rabbitTemplate.setConfirmCallback을 사용하십시오. 메시지가 교환기에 전송되면 확인 메서드를 다시 호출합니다. 메서드에서 ack를 판단하여 true이면 전송 성공, false이면 전송 실패이므로 처리해야 합니다.
리턴 모드를 사용하려면 ConnectionFactory의 Publisher-returns="true"를 설정하십시오.
rabbitTemplate.setReturnCallback을 사용하여 반환 기능을 설정하고 메시지가 교환기에서 대기열로 라우팅되지 않을 때 반환된 메시지 콜백 함수를 실행합니다.
확인 메커니즘
데모: 4. springboot는 RabbitMQ를 통합합니다.
(1) 구성 파일에서 확인을 켭니다.
#开启confirm确认机制 spring.rabbitmq.publisher-confirm-type=correlated
(2) rabbitTemplate의 ConfirmCallback 콜백 함수 설정
/** * 使用confirm机制: * (1)需要开启confirm机制。-----配置文件中加入:spring.rabbitmq.publisher-confirm-type=correlated * (2)为rabbitTemplate指定setConfirmCallback回调函数 */ @Test void test001() { //只能保证消息从生产者到交换机的可靠性 rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() { @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { //触发该方法 if (ack == false) { System.out.println("未来根据项目的需要,完成相应的操作"); } } }); rabbitTemplate.convertAndSend("Topics-exchange", "lazy.aaa", "Hello RabbitMQ..."); }
반환 메커니즘
(1) 구성 파일에서 반환을 켭니다.
#开启return机制 spring.rabbitmq.publisher-returns=true
(2) rabbitTemplate의 리턴 콜백 함수 설정
/** * return机制: * (1)开启return机制---配置文件加入:spring.rabbitmq.publisher-returns=true * (2)为rabbitTemplate设置return的回调函数 */ @Test void test002() { //只有当消息无法从交换机到队列时才会触发 rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() { @Override public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) { //为交换机到队列分发消息失败时会触发 System.out.println("replyCode====" + replyCode); System.out.println("replyText====" + replyText); } }); rabbitTemplate.convertAndSend("Topics-exchange", "lazy.aaa", "Hello RabbitMQ..."); }
1.2 대기열에서 메시지가 손실되지 않도록 하는 방법
(1) 대기열을 영구로 설정
(2) 메시지 지속성 설정
1.3 메시지를 안정적으로 사용할 수 있는지 확인
ACK 확인 메커니즘
여러 소비자가 동시에 메시지를 수신합니다.메시지를 수신하는 도중에 소비자가 갑자기 전화를 끊습니다.이 메시지가 손실되지 않도록 확인 메커니즘이 필요합니다.즉,소비자는 소비 후 서버에 통지해야 하며 서버는 데이터를 삭제합니다.
이렇게 하면 소비자에게 문제가 있고 서버에 대한 동기 메시지가 없더라도 소비할 다른 소비자가 있는 경우를 해결하여 메시지가 손실되지 않도록 합니다.
ACK 구현
ack는 승인, 확인을 의미합니다. 메시지 수신 후 소비자의 확인 방법을 나타냅니다.
세 가지 확인 방법이 있습니다.
- 자동 확인:acknowledge="none"
- 수동 확인:acknowledge="manual"
- 이상 상황에 따라 확인:acknowledge="auto"(이 방법은 사용하기 번거롭고 일반적으로 사용되지 않음)
자동 확인은 소비자가 메시지를 수신하면 자동으로 수신을 확인하고 RabbitMQ 메시지 대기열에서 해당 메시지를 제거함을 의미합니다. 그러나 실제 비즈니스 처리에서는 메시지가 수신되고 비즈니스 처리가 비정상적으로 진행되어 메시지가 손실될 가능성이 매우 높습니다.
수동 확인 방식이 설정되어 있으면 업무 처리가 성공한 후 channel.basicAck()를 호출하여 수동으로 영수증에 서명해야 하며, 예외가 있는 경우 channel.basicNack() 메서드를 호출하여 자동으로 재전송되도록 합니다. 메시지.
소비자 측:
(1).소비자 측 수정 - 메시지 수동 확인
#修改消费端----手动确认消息 spring.rabbitmq.listener.simple.acknowledge-mode=manual
(2).코드 수정
package com.wqg.listener; import com.rabbitmq.client.Channel; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; import org.springframework.amqp.core.Message; import java.io.IOException; /** * @ fileName:MyListener * @ description: * @ author:wqg * @ createTime:2023/7/12 18:57 */ @Component public class MyListener { //basicAck:确认消息----rabbit服务端删除 //basicNack:服务继续发送消息 @RabbitListener(queues = {"Topics-queue002"})//queues:表示你监听的队列名 public void h(Message message , Channel channel) throws IOException { long deliveryTag = message.getMessageProperties().getDeliveryTag(); //把监听到的消息封装到Message类对象中 byte[] body = message.getBody(); String s = new String(body); System.out.println("消息内容==="+s); try { //int a = 10/0; //模拟宕机 System.out.println("核心业务的处理~~~"); /** *long deliveryTag: 消息的标注 * boolean multiple:是否把该消息之前未确认的消息一起确认掉 */ channel.basicAck(deliveryTag,true);//确认消息 } catch (Exception e) { /** * long deliveryTag; boolean multiple; * boolean requeue:是否要求rabbitmq服务重新发送该消息 */ channel.basicNack(deliveryTag,true,true); } } }
요약: 메시지의 신뢰성을 보장하는 방법은 무엇입니까?
- 신뢰할 수 있는 메시지 전달 보장: 확인 메커니즘 및 반환 메커니즘
- 대기열에서:---지속성
- ack 메커니즘을 사용하여 소비자의 안정적인 소비를 보장합니다.
2. 지연 대기열
2.1.TTL
TTL의 전체 이름은 Time To Live(time to live/expiration time)입니다.
메시지가 생존 시간에 도달하면 소비되지 않은 경우 자동으로 지워집니다.
RabbitMQ는 메시지에 대한 만료 시간을 설정하거나 전체 큐(Queue)에 대한 만료 시간을 설정할 수 있습니다.
데모:
그래픽 인터페이스를 사용하여 생성
프로듀서 테스트
결과
대기열은 만료 시간으로 설정되고 메시지도 만료 시간으로 설정됨 ----- 최단 시간에 따라 실행
요약:
- 대기열 만료 시간을 설정하려면 매개변수 x-message-ttl, 단위: ms(밀리초)를 사용하여 전체 대기열 메시지를 균일하게 만료합니다.
- 만료 매개변수를 사용하여 메시지 만료 시간을 설정합니다. 단위: ms(밀리초), 메시지가 큐의 선두에 있을 때(소비될 때) 메시지가 만료되었는지 여부를 판단합니다.
- 둘 다 설정하면 더 짧은 시간이 우선합니다.
2.2 배달 못한 편지 대기열
데드 레터 큐, 영어 약어: DLX. Dead Letter Exchange(데드 레터 교환), 그 메시지가 Dead message가 되면 다른 교환기로 재전송이 가능한데, 이 교환이 바로 DLX입니다.
어떤 종류의 메시지가 데드 레터 메시지가 됩니까?
- 대기열 메시지 길이가 한도에 도달했습니다.
- 소비자는 소비 메시지 basicNack/basicReject를 거부하고 메시지를 원래 대상 대기열 requeue=false에 다시 넣지 않습니다.
- 원래 대기열에 메시지 만료 설정이 있으며 메시지 도착 제한 시간이 소비되지 않았습니다.
대기열 바인딩 데드 레터 교환:
데모:
GUI를 사용하여 만들기:
프로듀서 테스트
결과
2.3 지연 대기열
지연 큐, 즉 큐에 들어간 직후 메시지가 소비되지 않고 지정된 시간에 도달한 후에만 소비됩니다.
필요:
주문 후 30분 이내에 입금이 되지 않으면 주문이 취소되며 재고는 롤백됩니다.
신규 사용자 등록 성공 후 7일 후 인사말 문자를 보내주세요.
실현 방법:
타이머: 성능 저하 --- 데이터베이스 쿼리가 정기적으로 수행됩니다.
지연 대기열
지연 대기열의 기능은 메시지 대기열을 통해 완료됩니다.
- 지연 대기열 기능은 RabbitMQ에서 제공되지 않습니다.
- 그러나 다음을 사용할 수 있습니다. TTL + 배달 못한 편지 대기열 조합을 사용하여 지연 대기열의 효과를 얻을 수 있습니다.
데모:
대기열이 비어 있습니다
스프링부트 프로젝트 생성
구성 파일
#rabbitmq的配置 spring.rabbitmq.host=192.168.75.129 spring.rabbitmq.username=guest spring.rabbitmq.password=guest spring.rabbitmq.virtual-host=/
(1) 종속성 도입
<dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.83</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.amqp</groupId> <artifactId>spring-rabbit-test</artifactId> <scope>test</scope> </dependency> </dependencies>
(2) 주문 시뮬레이션을 위한 OrderController.java 생성
package com.wqg.controller; import com.alibaba.fastjson.JSON; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.UUID; /** * @ fileName:OrderController * @ description:订单 * @ author:wqg * @ createTime:2023/7/13 16:24 */ @RestController @RequestMapping("/order") public class OrderController { @Autowired private RabbitTemplate rabbitTemplate; @GetMapping("/saveOrder") public String save(Integer pid, Integer num) { //生成一个订单号 String orderId = UUID.randomUUID().toString().replace("-", ""); System.out.println("下单成功,订单号为===" + orderId); HashMap<Object, Object> map = new HashMap<>(); map.put("orderId", orderId); map.put("pid", pid); map.put("num", num); rabbitTemplate.convertAndSend("pt_exchange", "qy165.aaa", JSON.toJSONString(map)); return "下单成功"; } }
(3) 청취 시뮬레이션을 위한 MyListener.java 생성
package com.wqg.listener; import com.alibaba.fastjson.JSON; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; import java.util.HashMap; /** * @ fileName:MyListener * @ description: * @ author:wqg * @ createTime:2023/7/13 16:35 */ @Component public class MyListener { @RabbitListener(queues = {"dead_queue"}) public void hello(Message message){ byte[] body = message.getBody(); String s = new String(body); HashMap hashMap = JSON.parseObject(s, HashMap.class); System.out.println("message==="+hashMap); //取消订单 System.out.println("取消订单号为==="+hashMap.get("orderId")); } }
시험
콘솔
![]()
3. 소비자가 반복적으로 메시지를 소비하는 것을 방지하는 방법
메시지의 멱등성---작업을 몇 번 수행해도 결과는 동일합니다.
- 글로벌 ID를 생성하여 redis 또는 데이터베이스에 저장하고 소비자가 메시지를 소비하기 전에 메시지가 소비되었는지 확인하십시오.
- 메시지가 소비된 경우 mq에 메시지가 소비되었음을 알리고 메시지를 폐기합니다(수동 ack).
- 소비되지 않은 경우 메시지를 소비하고 소비 레코드를 redis 또는 데이터베이스에 씁니다.
요구 사항을 간략하게 설명합니다.주문이 완료되면 사용자에게 포인트가 적립되어야 하며 포인트가 반복적으로 적립되지 않도록 해야 합니다. 그런 다음 mq가 메시지를 소비하기 전에 먼저 데이터베이스로 이동하여 메시지가 소비되었는지 확인하고 소비된 경우 메시지를 직접 폐기합니다.
데모:
생산자
import com.alibaba.fastjson.JSONObject; import com.xiaojie.score.entity.Score; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import java.util.UUID; /** * @author * @version 1.0 * @description:发送积分消息的生产者 * @date */ @Component @Slf4j public class ScoreProducer implements RabbitTemplate.ConfirmCallback { @Autowired private RabbitTemplate rabbitTemplate; //定义交换机 private static final String SCORE_EXCHANGE = "ykq_score_exchaneg"; //定义路由键 private static final String SCORE_ROUTINNGKEY = "score.add"; /** * @description: 订单完成 * @param: * @return: java.lang.String * @author xiaojie * @date: */ public String completeOrder() { String orderId = UUID.randomUUID().toString(); System.out.println("订单已完成"); //发送积分通知 Score score = new Score(); score.setScore(100); score.setOrderId(orderId); String jsonMSg = JSONObject.toJSONString(score); sendScoreMsg(jsonMSg, orderId); return orderId; } /** * @description: 发送积分消息 * @param: * @param: message * @param: orderId * @return: void * @author * @date: */ @Async public void sendScoreMsg(String jsonMSg, String orderId) { this.rabbitTemplate.setConfirmCallback(this); rabbitTemplate.convertAndSend(SCORE_EXCHANGE, SCORE_ROUTINNGKEY, jsonMSg, message -> { //设置消息的id为唯一 message.getMessageProperties().setMessageId(orderId); return message; }); } @Override public void confirm(CorrelationData correlationData, boolean ack, String s) { if (ack) { log.info(">>>>>>>>消息发送成功:correlationData:{},ack:{},s:{}", correlationData, ack, s); } else { log.info(">>>>>>>消息发送失败{}", ack); } } }
소비자
import com.alibaba.fastjson.JSONObject; import com.rabbitmq.client.Channel; import com.xiaojie.score.entity.Score; import com.xiaojie.score.mapper.ScoreMapper; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.Headers; import org.springframework.stereotype.Component; import java.io.IOException; import java.util.Map; /** * @author * @version 1.0 * @description: 积分的消费者 * @date */ @Component @Slf4j public class ScoreConsumer { @Autowired private ScoreMapper scoreMapper; @RabbitListener(queues = {"ykq_score_queue"}) public void onMessage(Message message, @Headers Map<String, Object> headers, Channel channel) throws IOException { String orderId = message.getMessageProperties().getMessageId(); if (StringUtils.isBlank(orderId)) { return; } log.info(">>>>>>>>消息id是:{}", orderId); String msg = new String(message.getBody()); Score score = JSONObject.parseObject(msg, Score.class); if (score == null) { return; } //执行前去数据库查询,是否存在该数据,存在说明已经消费成功,不存在就去添加数据,添加成功丢弃消息 Score dbScore = scoreMapper.selectByOrderId(orderId); if (dbScore != null) { //证明已经消费消息,告诉mq已经消费,丢弃消息 channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); return; } Integer result = scoreMapper.save(score); if (result > 0) { //积分已经累加,删除消息 channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); return; } else { log.info("消费失败,采取相应的人工补偿"); } } }