springboot+redis整合rabbitmq

什么是MQ

MQ(IBM MQ)代表消息队列,是一种应用程序对应用程序的通信方法;
通过消息传递队列发送和接收消息数据,支持应用程序,系统,服务和文件之间的信息交换。
这简化了业务应用程序的创建和维护。

RabbitMQ

RabbitMQ是目前非常热门的一款消息中间件,不管是互联网大厂还是中小企业都在大量使用。
RabbitMQ中的交换机有Direct Exchange(直连交换机)、Topic Exchange(主题交换器)、Fanout Exchange(广播式交换机)、Headers Exchange(Headers交换机)四种,常用的就前三种,本文将对直连交换机进行演示,并加入消息确认机制以及整合redis防止重复消费问题。

安装RabbitMQ

docker 方式安装

# 拉取镜像
docker pull rabbitmq:management
# 创建容器并运行
docker run --name rabbitmq -d -p 15672:15672 -p 5672:5672 rabbitmq:management

访问http://ip:15672,RabbitMQ默认的用户名:guest,密码:guest
springboot+redis整合rabbitmq_第1张图片
具体信息不在介绍,我们直接创建项目使用
首先创建一个rabbitmq-producer生成者springboot项目添加依赖

<dependency>
	<groupId>org.springframework.bootgroupId>
	<artifactId>spring-boot-starter-amqpartifactId>
dependency>

创建RabbitMQConfig.java配置类

import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.SerializerMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;

@Configuration
public class RabbitMQConfig {
    public static final String EXCHANGE_A = "exchange-A";
    public static final String QUEUE_A = "queue-a";
    public static final String ROUTINGKEY_A = "routing-key-A";

    /**
     * 设置交换机
     */
    @Bean
    public DirectExchange exchangeA() {
        return new DirectExchange(EXCHANGE_A);
    }


    /**
     * 设置队列
     */
    @Bean
    public Queue queueA() {
        return new Queue(QUEUE_A, true);
    }

    /**
     * 绑定
     */
    @Bean
    public Binding binding() {
        return BindingBuilder.bind(queueA()).to(exchangeA()).with(ROUTINGKEY_A);
    }


    @Bean
    @Scope("prototype")//通知Spring把被注解的Bean变成多例 表示每次获得bean都会生成一个新的对象
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate template = new RabbitTemplate(connectionFactory);
        template.setMandatory(true);
        template.setMessageConverter(new SerializerMessageConverter());
        return template;
    }

}

yml文件配置

spring:
  #RabbitMQ
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    #必须配置这个才会确认回调
    publisher-confirm-type: correlated
    #消息投递到队列失败是否回调
    publisher-returns: true

创建ConfirmCallbackService.java实现手动ack回执回调处理

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

@Component
public class ConfirmCallbackService implements RabbitTemplate.ConfirmCallback {
    private static final Logger log = LoggerFactory.getLogger(ConfirmCallbackService.class);

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (!ack) {
            log.error("消息发送异常!");
        } else {
            log.info("发送者已经收到确认,correlationData={} ,ack={}, cause={}", correlationData.getId(), ack, cause);
        }
    }
}

创建ReturnCallbackService.java实现消息投递到队列失败是否回调处理

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

@Component
public class ReturnCallbackService implements RabbitTemplate.ReturnCallback {
    private static final Logger log = LoggerFactory.getLogger(ReturnCallbackService.class);

    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        log.info("returnedMessage ===> replyCode={} ,replyText={} ,exchange={} ,routingKey={}", replyCode, replyText, exchange, routingKey);
    }
}

创建ProducerService.java发送消息处理

import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.UUID;

@Service
public class ProducerService {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private ConfirmCallbackService confirmCallbackService;

    @Autowired
    private ReturnCallbackService returnCallbackService;

    /**
     * 通用发送消息
     *
     * @param exchange   交换机
     * @param routingKey 路由key
     * @param msg        消息
     */
    public void sendMessage(String exchange, String routingKey, Object msg) {

        /**
         * 确保消息发送失败后可以重新返回到队列中
         * 注意:yml需要配置 publisher-returns: true
         */
        rabbitTemplate.setMandatory(true);

        /**
         * 消费者确认收到消息后,手动ack回执回调处理
         */
        rabbitTemplate.setConfirmCallback(confirmCallbackService);

        /**
         * 消息投递到队列失败回调处理
         */
        rabbitTemplate.setReturnCallback(returnCallbackService);

        /**
         * 发送消息
         */
        rabbitTemplate.convertAndSend(exchange, routingKey, msg,
                message -> {
                    message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
                    return message;
                },
                new CorrelationData(UUID.randomUUID().toString()));
    }

}

创建RabbitMQController.java类发送消息接口

@RestController
public class RabbitMQController {
    private static final Logger log = LoggerFactory.getLogger(RabbitMQController.class);

    private static final String SUCCESS = "success";

    @Autowired
    private ProducerService producerService;

    @GetMapping("send")
    public String send() {
        producerService.sendMessage(DirectRabbitMQConfig.EXCHANGE_A, DirectRabbitMQConfig.ROUTINGKEY_A, "hello 你好!!!");
        return SUCCESS;
    }
}

启动项目访问http://localhost:8080/send,页面打印success说明发送成功
可以登录http://ip:15672查看
springboot+redis整合rabbitmq_第2张图片

这里我刚才点了三次,所以有三个
下面我们在创建一个springboot项目rabbitmq-consumer进行消息的消费,引入依赖相同
配置yml

server:
  port: 8081
spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    #消费端配置
    listener:
      simple:
        # 同一个队列启动几个消费者
        concurrency: 5
        # 消费者最大数量
        max-concurrency: 10
        # 限流 多数据量同时只能过来一条
        prefetch: 1
        #手动确认
        acknowledge-mode: manual
        default-requeue-rejected: true
    template:
      mandatory: true

创建类ReceiverMessage.java 监听消息

import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.stereotype.Component;

@Component
public class ReceiverMessage {
    private static final Logger log = LoggerFactory.getLogger(ReceiverMessage.class);

    @RabbitListener(queues = "queue-a")
    public void processHandler1(String msg, Message message, Channel channel) throws Exception {
        log.info("消费者A收到消息:{}", msg);
        MessageHeaders headers = message.getHeaders();
        Long tag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
        try {
            //TODO 具体业务

            //手动确认消息
            channel.basicAck(tag, false);
        } catch (Exception e) {
            log.error("Exception:" + e.getMessage(), e);
            boolean flag = (boolean) headers.get(AmqpHeaders.REDELIVERED);
            if (flag) {
                log.error("消息已重复处理失败,拒绝再次接收...");
                channel.basicAck(tag, false);
            } else {
                log.error("消息即将再次返回队列处理...");
                channel.basicNack(tag, false, true);
            }
        }
    }
}

启动项目后可以看到控制台打印
在这里插入图片描述
说明消息消费成功
发送和接受实体类必须保证发送者和接收者bean对象都必须序列化,bean定义必须一模一样,包括bean所在路径

下面以实体类演示一下 延迟队列发送
在两个项目相同的包下面创建Order类并实现Serializable

import java.io.Serializable;

public class Order implements Serializable{

    private String orderId; // 订单id

    private Integer orderStatus; // 订单状态 0:未支付,1:已支付,2:订单已取消

    private String orderName; // 订单名字

    public String getOrderId() {
        return orderId;
    }

    public void setOrderId(String orderId) {
        this.orderId = orderId;
    }

    public Integer getOrderStatus() {
        return orderStatus;
    }

    public void setOrderStatus(Integer orderStatus) {
        this.orderStatus = orderStatus;
    }

    public String getOrderName() {
        return orderName;
    }

    public void setOrderName(String orderName) {
        this.orderName = orderName;
    }

    @Override
    public String toString() {
        return "Order{" +
                "orderId='" + orderId + '\'' +
                ", orderStatus=" + orderStatus +
                ", orderName='" + orderName + '\'' +
                '}';
    }
}

创建DelayRabbitConfig.java配置类

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * 延迟队列配置
 */
@Configuration
public class DelayRabbitConfig {
    /**
     * 延迟队列 TTL 名称
     */
    private static final String ORDER_DELAY_QUEUE = "user.order.delay.queue";
    /**
     * DLX,dead letter发送到的 exchange
     * 延时消息就是发送到该交换机的
     */
    public static final String ORDER_DELAY_EXCHANGE = "user.order.delay.exchange";
    /**
     * routing key 名称
     * 具体消息发送在该 routingKey 的
     */
    public static final String ORDER_DELAY_ROUTING_KEY = "order_delay";

    public static final String ORDER_QUEUE_NAME = "user.order.queue";
    public static final String ORDER_EXCHANGE_NAME = "user.order.exchange";
    public static final String ORDER_ROUTING_KEY = "order";

    /**
     * 延迟队列配置
     * 

* 1、params.put("x-message-ttl", 5 * 1000); * 第一种方式是直接设置 Queue 延迟时间 但如果直接给队列设置过期时间,这种做法不是很灵活,(当然二者是兼容的,默认是时间小的优先) * 2、rabbitTemplate.convertAndSend(book, message -> { * message.getMessageProperties().setExpiration(2 * 1000 + ""); * return message; * }); * 第二种就是每次发送消息动态设置延迟时间,这样我们可以灵活控制 **/ @Bean public Queue delayOrderQueue() { Map<String, Object> params = new HashMap<>(); // x-dead-letter-exchange 声明了队列里的死信转发到的DLX名称, params.put("x-dead-letter-exchange", ORDER_EXCHANGE_NAME); // x-dead-letter-routing-key 声明了这些死信在转发时携带的 routing-key 名称。 params.put("x-dead-letter-routing-key", ORDER_ROUTING_KEY); return new Queue(ORDER_DELAY_QUEUE, true, false, false, params); } /** * 需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配。 * 这是一个完整的匹配。如果一个队列绑定到该交换机上要求路由键 “dog”,则只有被标记为“dog”的消息才被转发, * 不会转发dog.puppy,也不会转发dog.guard,只会转发dog。 * @return DirectExchange */ @Bean public DirectExchange orderDelayExchange() { return new DirectExchange(ORDER_DELAY_EXCHANGE); } @Bean public Binding dlxBinding() { return BindingBuilder.bind(delayOrderQueue()).to(orderDelayExchange()).with(ORDER_DELAY_ROUTING_KEY); } @Bean public Queue orderQueue() { return new Queue(ORDER_QUEUE_NAME, true); } /** * 将路由键和某模式进行匹配。此时队列需要绑定要一个模式上。 * 符号“#”匹配一个或多个词,符号“*”匹配不多不少一个词。因此“audit.#”能够匹配到“audit.irs.corporate”,但是“audit.*” 只会匹配到“audit.irs”。 **/ @Bean public TopicExchange orderTopicExchange() { return new TopicExchange(ORDER_EXCHANGE_NAME); } @Bean public Binding orderBinding() { // TODO 如果要让延迟队列之间有关联,这里的 routingKey 和 绑定的交换机很关键 return BindingBuilder.bind(orderQueue()).to(orderTopicExchange()).with(ORDER_ROUTING_KEY); } }

在ProducerService类中添加一行代码

message.getMessageProperties().setExpiration(1000 * 30 + "");

springboot+redis整合rabbitmq_第3张图片

在RabbitMQController类中 添加接口测试发送消息

@GetMapping("sendDelay")
public String sendDelay() {
    Order order = new Order();
    order.setOrderId("123456");
    order.setOrderName("一加9");
    order.setOrderStatus(1);
    log.info("【订单生成时间】" + new Date().toString() + "【1分钟后检查订单是否已经支付】" + order.toString());
    producerService.sendMessage(DelayRabbitConfig.ORDER_DELAY_EXCHANGE, DelayRabbitConfig.ORDER_DELAY_ROUTING_KEY, order);
    return SUCCESS;
}

在消费者项目rabbitmq-consumer的ReceiverMessage类中中添加队列监听

@RabbitListener(queues = "user.order.queue")
public void processHandler2(Order order, Message message, Channel channel) throws Exception {
    log.info("消费者A收到消息:{}", order.toString());
    MessageHeaders headers = message.getHeaders();
    Long tag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
    try {
        //TODO 具体业务

        //手动确认消息
        channel.basicAck(tag, false);
    } catch (Exception e) {
        log.error("Exception:" + e.getMessage(), e);
        boolean flag = (boolean) headers.get(AmqpHeaders.REDELIVERED);
        if (flag) {
            log.error("消息已重复处理失败,拒绝再次接收...");
            channel.basicAck(tag, false);
        } else {
            log.error("消息即将再次返回队列处理...");
            channel.basicNack(tag, false, true);
        }
    }
}

重启两个项目访问http://localhost:8080/sendDelay,消费发送成功等待一分钟后查看控制台,打印如下
在这里插入图片描述![在这里插入图片描述](https://img-blog.csdnimg.cn/20200714154116486.png
延迟队列发送成功,延迟队列原博文https://blog.csdn.net/lizc_lizc/article/details/80722763

下面通过整合redis来防止重复消费
在rabbitmq-consumer项目pom.xml文件中加入redis依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
dependency>

yml文件中加入redis配置,完整配置如下

server:
  port: 8081
spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    #消费端配置
    listener:
      simple:
        # 同一个队列启动几个消费者
        concurrency: 5
        # 消费者最大数量
        max-concurrency: 10
        # 限流 多数据量同时只能过来一条
        prefetch: 1
        #手动确认
        acknowledge-mode: manual
  # Redis数据库索引(默认为0)
  redis:
    database: 0
    # Redis服务器地址
    host: 192.168.0.150
    # Redis服务器连接端口
    port: 6379
    # Redis服务器连接密码(默认为空)
    password:
    # 链接超时时间 单位 ms(毫秒)
    timeout: 3000

添加队列监听,如果有多个方法监听同一个队列,是采用轮询的方式消费的,这里我已经把原来的方法注释掉了
这里消费后添加到redis中,再次消费先查询redis是否存在,这里采用1/0使程序报错然后再次放入到队列消费进行演示

@Autowired
private RedisTemplate redisTemplate;

@RabbitListener(queues = "queue-a")
public void processHandler3(String msg, Message message, Channel channel) throws Exception {
    log.info("消费者A收到消息:{}", msg);
    MessageHeaders headers = message.getHeaders();
    Long tag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
    try {
        //TODO 具体业务
        String msgId = (String) headers.get("spring_returned_message_correlation");//发送者 需发送一个唯一id
        if (redisTemplate.opsForHash().entries("test").containsKey(msgId)) {
            //redis 中包含该 key,说明该消息已经被消费过
            log.info(msgId + ":消息已经被消费");
            channel.basicAck(tag, false);//确认消息已消费
            return;
        }
        //添加到redis
        redisTemplate.opsForHash().put("test", msgId, "testDelay");

        int i = 1 / 0;//走到这里报错

        //手动确认消息
        channel.basicAck(tag, false);
    } catch (Exception e) {
        log.error("Exception:" + e.getMessage());
        boolean flag = (boolean) headers.get(AmqpHeaders.REDELIVERED);
        if (flag) {
            log.error("消息已重复处理失败,拒绝再次接收...");
            channel.basicAck(tag, false);
        } else {
            log.error("消息即将再次返回队列处理...");
            channel.basicNack(tag, false, true);
        }

    }
}

在这里插入图片描述
发送消息后可以看到不会在重复消费
如果报错使用basicNack方法重新放回队列,可以查看Message中amqp_redelivered参数变成true(首次为false),所以如果消息消费报错 也不会重复消费,防止死循环
好了,今天就讲到这里!!!
项目已上传https://gitee.com/hehedabiao/springboot-series

你可能感兴趣的:(#,SpringBoot,Java)