RabbitMQ 消息队列可靠性代码实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wanghang88/article/details/82840340

1,实现rabbitmq所需要依赖包

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-amqp</artifactId>
 <version>${spring-boot.version}</version>
</dependency>

2,生产者基本配置

spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
# 开启发送确认(生产者发送消息相关)
spring.rabbitmq.publisher-confirms=true
# 开启发送失败退回(生产者发送消息相关)
spring.rabbitmq.publisher-returns=true

  a),队列

@Configuration
public class RabbitConfig {
 public final static String queueName = "ad_queue";
}

b),发送消息

@Component
public class Sender implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback{
 private static Map<String, Integer> map = new ConcurrentHashMap<>();
 private final Logger emailLogger = LoggerFactory.getLogger("emailLogger");
 @Autowired
 private RabbitTemplate rabbitTemplate;
 public void send(String routingKey, String content) {
 this.rabbitTemplate.setMandatory(true);
 this.rabbitTemplate.setConfirmCallback(this);
 this.rabbitTemplate.setReturnCallback(this);
 this.rabbitTemplate.setRoutingKey(routingKey);
 //这样我们就能知道,发送失败的是哪条消息了
 this.rabbitTemplate.correlationConvertAndSend(content, new CorrelationData(content));
// this.rabbitTemplate.convertAndSend(routingKey, content);
 }
 /**
 * 确认后回调:
 * @param correlationData
 * @param ack
 * @param cause
 */
 @Override
 public void confirm(CorrelationData correlationData, boolean ack, String cause) {
 if (!ack) {
 /**
 * 我们这里仅通过打印日志、发送邮件来预警,并没有实现自动重试机制:
 * 1、将发送失败重新发送到一个队列中:fail-queue,然后可以定时对这些消息进行重发
 * 2、在本地定义一个缓存map对象,定时进行重发
 * 3、为了更安全,可以将所有发送的消息保存到db中,并设置一个状态(是否发送成功),定时扫描检查是否存在未成功发送的信息
 * 这块知识,我们后期讲"分布式事务"的时候,在深入讲解这块内容
 */
 emailLogger.error("send ack fail, cause = {}, correlationData = {}", cause, correlationData.getId());
 } else {
 System.out.println("send ack success");
 }
 }
 /**
 * 失败后return回调:
 *
 * @param message
 * @param replyCode
 * @param replyText
 * @param exchange
 * @param routingKey
 */
 @Override
 public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
 emailLogger.error("send fail return-message = " + new String(message.getBody()) + ", replyCode: " + replyCode + ", replyText: " + replyText + ", exchange: " + exchange + ", routingKey: " + routingKey);
 String str = new String(message.getBody());
 retrySend(str, 3);
 }
 private void retrySend(String content, int retryTime){
 if(map.containsKey(content)){
 int count = map.get(content);
 count++;
 map.put(content, count);
 } else {
 map.put(content, 1);
 }
 if(map.get(content) <= retryTime) {
 send(RabbitConfig.queueName, content);
 }
 }
}

3,消费者

a)基本配置

spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
# 开启ACK(消费者消息确认机制)
spring.rabbitmq.listener.simple.acknowledge-mode=manual

b)队列(包括死信队列):

@Configuration
public class RabbitConfig {
 public final static String queueName = "ad_queue";
 /**
 * 死信队列:
 */
 public final static String deadQueueName = "ad_dead_queue";
 public final static String deadRoutingKey = "ad_dead_routing_key";
 public final static String deadExchangeName = "ad_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";
 @Bean
 public Queue helloQueue() {
 //将普通队列绑定到私信交换机上
 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(queueName, true, false, false, args);
 return queue;
 }
 /**
 * 死信队列:
 */
 @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);
 }
}

c)消费者消费消息:

@Component
@RabbitListener(queues = RabbitConfig.queueName)
public class Receiver {
 private Logger logger = LoggerFactory.getLogger(Receiver.class);
 private final Logger emailLogger = LoggerFactory.getLogger("emailLogger");
 @Resource
 UpdateRedisServiceImpl updateRedisService;
 @RabbitHandler
 public void process(String content, Channel channel, Message message) {
 logger.info("handle msg begin = {}", content);
 AdMessage adMessage = JSON.parseObject(content, AdMessage.class);
 Long id = adMessage.getId();
 int retryTimes = 0;
 while (retryTimes < 5) {
 //消费者做幂等处理(当然这只是对单台机器而言没有问题,如果是分布式集群环境,这种是不行的,后续我们会继续优化这块):防止相同类型的广告id更新问题
 synchronized (AdLock.cacheLock) {
 //更新redis数据:
 if(!updateRedisService.updateRedis(id)){
 retryTimes++;
 }
 }
 break;
 }
 if (retryTimes >= 3) {
 //当有多次更新失败的时候,发送邮件通知:
 emailLogger.error("处理MQ[" + content + "]失败[" + retryTimes + "]次");
 }
 try {
 if (retryTimes >= 5) {
 //当有很多次更新失败的时候,丢弃这条消息或者发送到死信队列中
 channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
 }else {
 //告诉服务器收到这条消息 已经被我消费了 可以在队列删掉;否则消息服务器以为这条消息没处理掉 后续还会在发
 channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
 }
 } catch (Exception e){
 logger.error("消息确认失败", e);
 }
 logger.info("handle msg finished = {}", content);
 }
}

猜你喜欢

转载自blog.csdn.net/wanghang88/article/details/82840340