第十三章 RabbitMQ

1. 目前存在的问题

1.1搜索与商品服务的问题

我们思考一下,是否存在问题?

- 商品的原始数据保存在数据库中,增删改查都在数据库中完成。

- 搜索服务数据来源是索引库,如果数据库商品发生变化,索引库数据不能及时更新。

如果我们在后台修改了商品的价格,搜索页面依然是旧的价格,这样显然不对。该如何解决?

这里有两种解决方案:

- 方案1:每当后台对商品做增删改操作,同时要修改索引库数据

- 方案2:搜索服务对外提供操作接口,后台在商品增删改后,调用接口

以上两种方式都有同一个严重问题:就是代码耦合,后台服务中需要嵌入搜索和商品页面服务,违背了微服务的独立原则。

所以,我们会通过另外一种方式来解决这个问题:消息队列

1.2订单服务取消订单问题

用户下单后,如果2个小时未支付,我们该如何取消订单

- 方案1:定时任务,定时扫描未支付订单,超过2小时自动关闭

- 方案2:使用延迟队列关闭订单

1.3分布式事务问题

如:用户支付订单,我们如何保证更新订单状态与扣减库存 ,三个服务数据最终一致!

二、消息队列解决什么问题

消息队列都解决了什么问题?

2.1异步

 2.2解耦

 2.3并行

 2.4排队

 

三、消息队列工具 RabbitMQ

3.1 常见MQ产品

- ActiveMQ:基于JMS协议,java语言,jdk

- RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好

- RocketMQ:基于JMS,阿里巴巴产品,目前交由Apache基金会

- Kafka:分布式消息系统,高吞吐量

3.2 RabbitMQ基础概念

 Broker:简单来说就是消息队列服务器实体

Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列

Queue:消息队列载体,每个消息都会被投入到一个或多个队列

Binding:绑定,它的作用就是把 exchange和 queue按照路由规则绑定起来

Routing Key:路由关键字, exchange根据这个关键字进行消息投递

vhost:虚拟主机,一个 broker里可以开设多个 vhost,用作不同用户的权限分离

producer:消息生产者,就是投递消息的程序

consumer:消息消费者,就是接受消息的程序

channel:消息通道,在客户端的每个连接里,可建立多个 channel,每个 channel代表一个会话任务

3.3 安装RabbitMQ

看电商软件环境安装.doc

3.4 五种消息模型

RabbitMQ提供了6种消息模型,但是第6种其实是RPC,并不是MQ,因此不予学习。那么也就剩下5种。

但是其实3、4、5这三种都属于订阅模型,只不过进行路由的方式不同。

 基本消息模型:生产者–>队列–>消费者

work消息模型:生产者–>队列–>多个消费者共同消费

订阅模型-Fanout:广播模式,将消息交给所有绑定到交换机的队列,每个消费者都会收到同一条消息

订阅模型-Direct:定向,把消息交给符合指定 rotingKey 的队列

订阅模型-Topic 主题模式:通配符,把消息交给符合routing pattern(路由模式) 的队列

我们项目使用的是第四种!

3.5 搭建mq测试环境service-mq

3.5.1 搭建service-mq服务

在service目录下搭建

3.5.2 修改配置pom.xml

<?xml version="1.0" encoding="UTF-8"?>
  <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <parent>
      <groupId>com.atguigu.gmall</groupId>
      <artifactId>service</artifactId>
      <version>1.0</version>
   </parent>

   <artifactId>service-mq</artifactId>
   <version>1.0</version>
   <packaging>jar</packaging>
   <name>service-mq</name>
   <description>service-mq</description>
   <build>
      <finalName>service-mq</finalName>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
      </plugins>
   </build>
  </project>

说明:引入依赖

3.5.3 添加配置文件

说明:rabbitmq默认端口5672,注意更改正确

bootstrap.properties

spring.application.name=service-mq
spring.profiles.active=dev
spring.cloud.nacos.discovery.server-addr=192.168.200.129:8848
spring.cloud.nacos.config.server-addr=192.168.200.129:8848
spring.cloud.nacos.config.prefix=${spring.application.name}
spring.cloud.nacos.config.file-extension=yaml
spring.cloud.nacos.config.shared-configs[0].data-id=common.yaml

3.5.4 启动类

package com.atguigu.gmall.mq;

  @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消据源自配置
  @ComponentScan({"com.atguigu.gmall"})
  @EnableDiscoveryClient
  public class ServiceMqApplication {
   public static void main(String[] args) {
      SpringApplication.run(ServiceMqApplication.class, args);
   }
}

四、消息不丢失

消息的不丢失,在MQ角度考虑,一般有三种途径:

1,生产者不丢数据

2,MQ服务器不丢数据

3,消费者不丢数据

保证消息不丢失有两种实现方式:

  1. 开启事务模式
  2. 消息确认模式

说明:开启事务会大幅降低消息发送及接收效率,使用的相对较少,因此我们生产环境一般都采取消息确认模式,以下我们只是讲解消息确认模式

4.1 消息确认

4.1.1 消息持久化

如果希望RabbitMQ重启之后消息不丢失,那么需要对以下3种实体均配置持久化

Exchange

声明exchange时设置持久化(durable = true)并且不自动删除(autoDelete = false)

Queue

声明queue时设置持久化(durable = true)并且不自动删除(autoDelete = false)

message

发送消息时通过设置deliveryMode=2持久化消息

4.1.2 发送确认

有时,业务处理成功,消息也发了,但是我们并不知道消息是否成功到达了rabbitmq,如果由于网络等原因导致业务成功而消息发送失败,那么发送方将出现不一致的问题,此时可以使用rabbitmq的发送确认功能,即要求rabbitmq显式告知我们消息是否已成功发送。

4.1.3 手动消费确认

有时,消息被正确投递到消费方,但是消费方处理失败,那么便会出现消费方的不一致问题。比如:订单已创建的消息发送到用户积分子系统中用于增加用户积分,但是积分消费方处理却都失败了,用户就会问:我购买了东西为什么积分并没有增加呢?

要解决这个问题,需要引入消费方确认,即只有消息被成功处理之后才告知rabbitmq以ack,否则告知rabbitmq以nack

4.2 消息确认业务封装

4.2.1 service-mq修改配置

开启rabbitmq消息确认配置,在common的配置文件中都已经配置好了!

rabbitmq:
  host: 192.168.200.129
  port: 5672
  username: guest
  password: guest
  publisher-confirms-type: correlated // 交换机的确认
  publisher-returns: true // 队列的确认
  listener:
    simple:
      acknowledge-mode: manual #下消息消者是自消息的,如果要手消息需要修改确模式manual
      prefetch: 1 # 者每次从队取的消息量。此时为轮询1:公平分

4.2.2 搭建rabbit-util模块

由于消息队列是公共模块,我们把mq的相关业务封装到该模块,其他service微服务模块都可能使用,因此我们把他封装到一个单独的模块,需要使用mq的模块直接引用该模块即可

搭建方式如:common-util,导入常量类 MqConst

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
  <project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>common</artifactId>
        <groupId>com.atguigu.gmall</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>rabbit-util</artifactId>
    <dependencies>
        <!--rabbitmq消息队列-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--rabbitmq 协议-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>
    </dependencies>
  </project>

4.2.3 service-mq引入rabbit-util模块依赖

<!--rabbitmq消息-->
  <dependency>
   <groupId>com.atguigu.gmall</groupId>
   <artifactId>rabbit-util</artifactId>
   <version>1.0</version>
  </dependency>

4.2.4 封装发送端消息确认

在rabbit-util 中添加类

package com.atguigu.gmall.common.config;
  /**
 * @Description 消息送确
 * ConfirmCallback  只确消息是否正确到Exchange 中
 * ReturnCallback   消息有正确到达队时触发,如果正确到达队列不
 * 1. 如果消息有到exchange,confirm,ack=false
 * 2. 如果消息到exchange,confirm,ack=true
 * 3. exchangequeue成功,不回return
 * 4. exchangequeue,return
 */
  @Component
@Slf4j
  public class MQProducerAckConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;

      // 修饰一个非静态的void()方法,在服务器加载Servlet的时候运行,并且只会被服务器执行一次在构造函数之后执行,init()方法之前执行。
    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(this);            //指定 ConfirmCallback
        rabbitTemplate.setReturnCallback(this);             //指定 ReturnCallback
    }

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            log.info("消息发送成功:" + JSON.toJSONString(correlationData));
        } else {
            log.info("消息发送失败:" + cause + " 数据:" + JSON.toJSONString(correlationData));
        }
    }

    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        // 反序列化出
        System.out.println("消息主体: " + new String(message.getBody()));
        System.out.println("应答码: " + replyCode);
        System.out.println("描述:" + replyText);
        System.out.println("消息使用的交换器 exchange : " + exchange);
        System.out.println("消息使用的路由键 routing : " + routingKey);
    }
 }

4.2.5 封装消息发送

在rabbit-util 中添加类

package com.atguigu.gmall.common.service;

  @Service
  public class RabbitService {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    /**
     *  送消
     * @param exchange 机
     * @param routingKey 路由键
     * @param message 消息
     */
    public boolean sendMessage(String exchange, String routingKey, Object message) {
        rabbitTemplate.convertAndSend(exchange, routingKey, message);
        return true;
    }
  }

4.2.6 发送确认消息测试

在service-mq编写测试代码

消息发送端

package com.atguigu.gmall.mq.controller;

  @RestController
@RequestMapping("/mq")
  public class MqController {
   @Autowired
   private RabbitService rabbitService;
   /**
    * 消息送
    */
   //http://localhost:8282/mq/sendConfirm
   @GetMapping("sendConfirm")
   public Result sendConfirm() {
      rabbitService.sendMessage("exchange.confirm", "routing.confirm", "来人了,开始接客吧!");
      return Result.ok();
   }

}

消息接收端

在service-mq 中编写

package com.atguigu.gmall.mq.receiver;

   @Component
public class ConfirmReceiver {
  @SneakyThrows
@RabbitListener(bindings=@QueueBinding(
        value = @Queue(value = "queue.confirm",autoDelete = "false"),
        exchange = @Exchange(value = "exchange.confirm",autoDelete = "true"),
        key = {"routing.confirm"}))
  public void process(Message message, Channel channel){
    System.out.println("RabbitListener:"+new String(message.getBody()));

        // false 确认一个消息,true 批量确认
  channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}}

测试:http://localhost:8282/mq/sendConfirm

4.2.7 消息发送失败,设置重发机制

实现思路:借助redis来实现重发机制

在rabbit-util 模块中添加依赖

<!-- redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- spring2.X集成redis所需common-pool2-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
</dependency>


自定义一个实体类来接收消息

@Data
public class GmallCorrelationData extends CorrelationData {

   
//  消息主体
    private Object message;
   
//  交换机
    private String exchange;
   
//  路由键
    private String routingKey;
   
//  重试次数
    private int retryCount = 0;
   
//  消息类型  是否是延迟消息
    private boolean isDelay = false;
   
//  延迟时间
    private int delayTime = 10;
}

修改发送方法

//  封装一个发送消息的方法
public Boolean sendMsg(String exchange,String routingKey, Object msg){
   
//  将发送的消息 赋值到 自定义的实体类
    GmallCorrelationData gmallCorrelationData = new GmallCorrelationData();
    // 
声明一个correlationId的变量
    String correlationId = UUID.randomUUID().toString().replaceAll("-","");
    gmallCorrelationData.setId(correlationId);
    gmallCorrelationData.setExchange(exchange);
    gmallCorrelationData.setRoutingKey(routingKey);
    gmallCorrelationData.setMessage(msg);

    // 
发送消息的时候,将这个gmallCorrelationData 对象放入缓存。
    redisTemplate.opsForValue().set(correlationId, JSON.toJSONString(gmallCorrelationData),10, TimeUnit.MINUTES);
    //  调用发送消息方法
this.rabbitTemplate.convertAndSend(exchange,routingKey,msg);
   
this.rabbitTemplate.convertAndSend(exchange,routingKey,msg,gmallCorrelationData);
   
//  默认返回true
   
return true;
}

发送失败调用重发方法  MQProducerAckConfig 类中修改

@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
   
//  ack = true 说明消息正确发送到了交换机
    if (ack){
        System.
out.println("们你来.");
       
log.info("消息送到了交");
    }
else {
       
//  消息没有到交换机
        log.info("消息没发送到交");
       
//  调用重试发送方法
        this.retrySendMsg(correlationData);
    }

}

@Override
public void returnedMessage(Message message, int code, String codeText, String exchange, String routingKey) {
    System.
out.println("消息主体: " + new String(message.getBody()));
    System.
out.println(": " + code);
    System.
out.println("描述:" + codeText);
    System.
out.println("消息使用的交 exchange : " + exchange);
    System.
out.println("消息使用的路由 routing : " + routingKey);

   
//  获取这个CorrelationData对象的Id  spring_returned_message_correlation
   
String correlationDataId = (String) message.getMessageProperties().getHeaders().get("spring_returned_message_correlation");
   
//  因为在发送消息的时候,已经将数据存储到缓存,通过 correlationDataId 来获取缓存的数据
    String strJson = (String) this.redisTemplate.opsForValue().get(correlationDataId);
   
//  消息没有到队列的时候,则会调用重试发送方法
    GmallCorrelationData gmallCorrelationData = JSON.parseObject(strJson,GmallCorrelationData.class);
   
//  调用方法  gmallCorrelationData 这对象中,至少的有,交换机,路由键,消息等内容.
   
this.retrySendMsg(gmallCorrelationData);
}

/**
 *
重试发送方法
 * @param correlationData   父类对象  它下面还有个子类对象 GmallCorrelationData
 */

private void retrySendMsg(CorrelationData correlationData) {
   
//  数据类型转换  统一转换为子类处理
    GmallCorrelationData gmallCorrelationData = (GmallCorrelationData) correlationData;
   
//  获取到重试次数 初始值 0
   
int retryCount = gmallCorrelationData.getRetryCount();
   
//  判断
    if (retryCount>=3){
       
//  不需要重试了
        log.error("已到,送消息失:"+JSON.toJSONString(gmallCorrelationData));
    }
else {
       
//  变量更新
        retryCount+=1;
       
//  重新赋值重试次数 第一次重试 0->1 1->2 2->3
       
gmallCorrelationData.setRetryCount(retryCount);
        System.
out.println("\t"+retryCount);

       
//  更新缓存中的数据
        this.redisTemplate.opsForValue().set(gmallCorrelationData.getId(),JSON.toJSONString(gmallCorrelationData),10, TimeUnit.MINUTES);

       
//  调用发送消息方法 表示发送普通消息  发送消息的时候,不能调用 new RabbitService().sendMsg() 这个方法
            this.rabbitTemplate.convertAndSend(gmallCorrelationData.getExchange(),gmallCorrelationData.getRoutingKey(),gmallCorrelationData.getMessage(),gmallCorrelationData);
  
    }
}

4.3 改造商品搜索上下架

4.3.1 定义商品上下架常量

rabbit-util模块中导入常量类MqConst
/**
 * 商品上下架.
 */
  public static final String EXCHANGE_DIRECT_GOODS = "exchange.direct.goods";
  public static final String ROUTING_GOODS_UPPER = "goods.upper";
  public static final String ROUTING_GOODS_LOWER = "goods.lower";
  //public static final String QUEUE_GOODS_UPPER  = "queue.goods.upper";
  public static final String QUEUE_GOODS_LOWER  = "queue.goods.lower";

4.3.2 service-list与service-product引入依赖与配置

<!--rabbitmq消息-->
  <dependency>
   <groupId>com.atguigu.gmall</groupId>
   <artifactId>rabbit-util</artifactId>
   <version>1.0</version>
  </dependency>

4.3.3 service-product发送消息

我在商品上架与商品添加时发送消息

商品上架

实现类

@Override
@Transactional
  public void onSale(Long skuId) {
    // 更改状态
    SkuInfo skuInfoUp = new SkuInfo();
    skuInfoUp.setId(skuId);
    skuInfoUp.setIsSale(1);
    skuInfoMapper.updateById(skuInfoUp);

    //商品上架
    rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_GOODS, MqConst.ROUTING_GOODS_UPPER, skuId);

}

商品下架

实现类

@Override
@Transactional
  public void cancelSale(Long skuId) {

    // 更改状态
    SkuInfo skuInfoUp = new SkuInfo();
    skuInfoUp.setId(skuId);
    skuInfoUp.setIsSale(0);
    skuInfoMapper.updateById(skuInfoUp);

    //商品下架
    rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_GOODS, MqConst.ROUTING_GOODS_LOWER, skuId);

}

4.3.4 service-list消费消息

package com.atguigu.gmall.list.receiver;
@Component
  public class ListReceiver {

  //  开启消息监听 监听商品上架!
  @SneakyThrows
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = MqConst.QUEUE_GOODS_UPPER,durable = "true",autoDelete = "false"),
        exchange = @Exchange(value = MqConst.EXCHANGE_DIRECT_GOODS),
        key = {MqConst.ROUTING_GOODS_UPPER}
))

  public void upperGoodsToEs(Long skuId, Message message, Channel channel){
    //  获取到skuId,并判断
    try {
        if (skuId!=null){
            //  则调用商品上架的方法!
            searchService.upperGoods(skuId);
        }
    } catch (Exception e) {
        //  写入日志或将这条消息写入数据库,短信接口
        e.printStackTrace();
    }

    //  确认消费者消费消息!
    channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}

  //  编写商品下架代码
  @SneakyThrows
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = MqConst.QUEUE_GOODS_LOWER,durable = "true",autoDelete = "false"),
        exchange = @Exchange(value = MqConst.EXCHANGE_DIRECT_GOODS),
        key = {MqConst.ROUTING_GOODS_LOWER}
))

  public void lowerGoodsToEs (Long skuId, Message message, Channel channel){
    try {
        //  判断skuId
        if (skuId!=null){
            //  调用商品下架方法
            searchService.lowerGoods(skuId);
        }
    } catch (Exception e) {
        //  写入日志或将这条消息写入数据库,短信接口
        e.printStackTrace();
    }
    //  消息确认
    channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}

4.3.5 测试

启动后台管理页面

http://localhost:8888/#/product/sku/list

操作商品的上架,下架。动态更改es中的数据。

可以通过http://192.168.200.128:5601/app/kibana#/dev_tools/console?_g=() 观察功能是否实现!

五、延迟消息

延迟消息有两种实现方案:

  1. 基于死信队列
  2. 集成延迟插件

5.1 基于死信实现延迟消息

使用RabbitMQ来实现延迟消息必须先了解RabbitMQ的两个概念:消息的TTL和死信Exchange,通过这两者的组合来实现延迟队列

5.1.1 消息的TTL(Time To Live)

消息的TTL就是消息的存活时间。RabbitMQ可以对队列和消息分别设置TTL。对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。

如何设置TTL:

我们创建一个队列queue.temp,在Arguments 中添加x-message-ttl 为5000 (单位是毫秒),那所在压在这个队列的消息在5秒后会消失。

5.1.2 死信交换机  Dead Letter Exchanges

一个消息在满足如下条件下,会进死信路由,记住这里是路由而不是队列,一个路由可以对应很多队列。

(1) 一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不会被再次放在队列里,被其他消费者使用。

(2)上面的消息的TTL到了,消息过期了。

(3)队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上。

Dead Letter Exchange其实就是一种普通的exchange,和创建其他exchange没有两样。只是在某一个设置Dead Letter Exchange的队列中有消息过期了,会自动触发消息的转发,发送到Dead Letter Exchange中去。

 我们现在可以测试一下延迟队列。

(1)创建死信队列

(2)创建交换机

(3)建立交换器与队列之间的绑定

(4)创建队列

5.1.3 代码实现

5.1.3.1 在service-mq 中添加配置类

package com.atguigu.gmall.mq.config;
  import org.springframework.amqp.core.DirectExchange;
  import org.springframework.amqp.core.Queue;
  import org.springframework.context.annotation.Bean;
  import org.springframework.context.annotation.Configuration
  @Configuration
  public class DeadLetterMqConfig {

    // 声明一些变量
    public static final String exchange_dead = "exchange.dead";
    public static final String routing_dead_1 = "routing.dead.1";
    public static final String routing_dead_2 = "routing.dead.2";
    public static final String queue_dead_1 = "queue.dead.1";
    public static final String queue_dead_2 = "queue.dead.2";

    // 定义交换机
    @Bean
    public DirectExchange exchange(){
        return new DirectExchange(exchange_dead,true,false,null);
    }

    @Bean
    public Queue queue1(){
        // 设置如果队列一 出现问题,则通过参数转到exchange_deadrouting_dead_2 上!
        HashMap<String, Object> map = new HashMap<>();
        // 参数绑定 此处的key 固定值,不能随意写
        map.put("x-dead-letter-exchange",exchange_dead);
        map.put("x-dead-letter-routing-key",routing_dead_2);
        // 设置延迟时间
        map.put("x-message-ttl", 10 * 1000);

        // 队列名称,是否持久化,是否独享、排外的【true:只可以在本次连接中访问】,是否自动删除,队列的其他属性参数
        return new Queue(queue_dead_1,true,false,false,map);
    }

    @Bean
    public Binding binding(){
        // 将队列一 通过routing_dead_1 key 绑定到exchange_dead 交换机上
        return BindingBuilder.bind(queue1()).to(exchange()).with(routing_dead_1);
    }

    // 这个队列二就是一个普通队列
    @Bean
    public Queue queue2(){
        return new Queue(queue_dead_2,true,false,false,null);
    }

    // 设置队列二的绑定规则
    @Bean
    public Binding binding2(){
        // 将队列二通过routing_dead_2 key 绑定到exchange_dead交换机上!
        return BindingBuilder.bind(queue2()).to(exchange()).with(routing_dead_2);
    }
}

5.1.3.2 配置发送消息

package com.atguigu.gmall.mq.controller; 

  @RestController
@RequestMapping("/mq")
@Slf4j
  public class MqController {

   @Autowired
   private RabbitTemplate rabbitTemplate;

   @Autowired
   private RabbitService rabbitService;

 @GetMapping("sendDeadLettle")
   public Result sendDeadLettle() {
      SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
     this.rabbitTemplate.convertAndSend(DeadLetterMqConfig.exchange_dead, DeadLetterMqConfig.routing_dead_1, "ok");
      System.out.println(sdf.format(new Date()) + " Delay sent.");
      return Result.ok();
   }
}

5.1.3.3 消息接收方

package com.atguigu.gmall.mq.receiver;

  @Component
@Configuration
  public class DeadLetterReceiver {

    @RabbitListener(queues = DeadLetterMqConfig.queue_dead_2)
    public void get(String msg) {
        System.out.println("Receive:" + msg);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("Receive queue_dead_2: " + sdf.format(new Date()) + " Delay rece." + msg);
    }
}

5.2 基于延迟插件实现延迟消息

Rabbitmq实现了一个插件x-delay-message来实现延时队列

5.2.1 插件安装

1. 首先我们将刚下载下来的rabbitmq_delayed_message_exchange-3.9.0.ez文件上传到RabbitMQ所在服务器,下载地址:https://www.rabbitmq.com/community-plugins.html

2. 切换到插件所在目录,执行 docker cp rabbitmq_delayed_message_exchange-3.9.0.ez rabbitmq:/plugins 命令,将刚插件拷贝到容器内plugins目录下

3. 执行 docker exec -it rabbitmq /bin/bash 命令进入到容器内部,并 cd plugins 进入plugins目录

4. 执行 ls -l|grep delay  命令查看插件是否copy成功

5. 在容器内plugins目录下,执行 rabbitmq-plugins enable rabbitmq_delayed_message_exchange  命令启用插件

6. exit命令退出RabbitMQ容器内部,然后执行 docker restart rabbitmq 命令重启RabbitMQ容器

5.2.2 代码实现

在service-mq 中添加类

配置队列

package com.atguigu.gmall.mq.config;

  @Configuration
  public class DelayedMqConfig {

    public static final String exchange_delay = "exchange.delay";
    public static final String routing_delay = "routing.delay";
    public static final String queue_delay_1 = "queue.delay.1";

     @Bean
    public Queue delayQeue1() {
        // 第一个参数建的queue的名字,第二个参数是是否支持持久化
        return new Queue(queue_delay_1, true);
    }

    @Bean
    public CustomExchange delayExchange() {
        Map<String, Object> args = new HashMap<String, Object>();
        args.put("x-delayed-type", "direct");
        return new CustomExchange(exchange_delay, "x-delayed-message", true, false, args);
    }
  
    @Bean
    public Binding delayBbinding1() {
        return BindingBuilder.bind(delayQeue1()).to(delayExchange()).with(routing_delay).noargs();

    }
}

发送消息

@GetMapping("sendDelay")
  public Result sendDelay() {
   SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
   this.rabbitTemplate.convertAndSend(DelayedMqConfig.exchange_delay, DelayedMqConfig.routing_delay, sdf.format(new Date()), new MessagePostProcessor() {
      @Override
      public Message postProcessMessage(Message message) throws AmqpException {
         message.getMessageProperties().setDelay(10 * 1000);
         System.out.println(sdf.format(new Date()) + " Delay sent.");
         return message;
      }
   });
   return Result.ok();
}

接收消息

package com.atguigu.gmall.mq.receiver;

  @Component
  public class DelayReceiver {

    @RabbitListener(queues = DelayedMqConfig.queue_delay_1)
    public void get(String msg) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("Receive queue_delay_1: " + sdf.format(new Date()) + " Delay rece." + msg);
    }
}

5.3 基于延迟插件实现取消订单

service-order模块

5.3.1 业务配置与接口封装

rabbit-util模块配置常量MqConst
/**
 * 取消订单送延迟队列
 */

  public static final String EXCHANGE_DIRECT_ORDER_CANCEL = "exchange.direct.order.cancel";//"exchange.direct.order.create" test_exchange;
  public static final String ROUTING_ORDER_CANCEL = "order.create";
  //取消订单队public static final String QUEUE_ORDER_CANCEL  = "queue.order.cancel";
  //取消订单 迟时间 单位:秒
  public static final int DELAY_TIME  = 10;
rabbit-util模块延迟接口封装
RabbitService

/*
 *
封装发送延迟消息方法
 * @param exchange
 
* @param routingKey
 
* @param msg
 
* @param delayTime
 
* @return
 
*/

public Boolean sendDelayMsg(String exchange,String routingKey, Object msg, int delayTime){
   
//  将发送的消息 赋值到 自定义的实体类
    GmallCorrelationData gmallCorrelationData = new GmallCorrelationData();
   
//  声明一个correlationId的变量
    String correlationId = UUID.randomUUID().toString().replaceAll("-","");
    gmallCorrelationData.setId(correlationId);
    gmallCorrelationData.setExchange(exchange);
    gmallCorrelationData.setRoutingKey(routingKey);
    gmallCorrelationData.setMessage(msg);
    gmallCorrelationData.setDelayTime(delayTime);
    gmallCorrelationData.setDelay(
true);

   
//  将数据存到缓存
    this.redisTemplate.opsForValue().set(correlationId,JSON.toJSONString(gmallCorrelationData),10,TimeUnit.MINUTES);

   
//  发送消息
    this.rabbitTemplate.convertAndSend(exchange,routingKey,msg,message -> {
       
//  设置延迟时间
        message.getMessageProperties().setDelay(delayTime*1000);
       
return message;
    },
gmallCorrelationData);

   
//  默认返回
    return true;
}

修改retrySendMsg方法 – 添加判断是否属于延迟消息

//  判断是否属于延迟消息
if (gmallCorrelationData.isDelay()){
   
//  属于延迟消息
    this.rabbitTemplate.convertAndSend(gmallCorrelationData.getExchange(),gmallCorrelationData.getRoutingKey(),gmallCorrelationData.getMessage(),message -> {
       
//  设置延迟时间
        message.getMessageProperties().setDelay(gmallCorrelationData.getDelayTime()*1000);
       
return message;
    },
gmallCorrelationData);
}
else {
   
//  调用发送消息方法 表示发送普通消息  发送消息的时候,不能调用 new RabbitService().sendMsg() 这个方法
    this.rabbitTemplate.convertAndSend(gmallCorrelationData.getExchange(),gmallCorrelationData.getRoutingKey(),gmallCorrelationData.getMessage(),gmallCorrelationData);
}

利用封装好的工具类 测试发送延迟消息

//  基于延迟插件的延迟消息
@GetMapping("sendDelay")
public Result sendDelay(){
   
//  声明一个时间对象
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    System.
out.println("时间"+simpleDateFormat.format(new Date()));
    this.rabbitService.sendDelayMsg(DelayedMqConfig.exchange_delay,DelayedMqConfig.routing_delay,"iuok",3);
   
return Result.ok();
}

结果会 回发送三次,也被消费三次!

如何保证消息幂等性?

  1. 使用数据方式
  2. 使用redis setnx 命令解决  --- 推荐

@SneakyThrows
@RabbitListener
(queues = DelayedMqConfig.queue_delay_1)
public void getMsg2(String msg,Message message,Channel channel){

   
//  使用setnx 命令来解决 msgKey = delay:iuok
   
String msgKey = "delay:"+msg;
    Boolean result =
this.redisTemplate.opsForValue().setIfAbsent(msgKey, "0", 10, TimeUnit.MINUTES);
   
//  result = true : 说明执行成功,redis 里面没有这个key ,第一次创建, 第一次消费。
    //  result = false : 说明执行失败,redis 里面有这个key
    // 
不能: 那么就表示这个消息只能被消费一次!  那么第一次消费成功或失败,我们确定不了!  --- 只能被消费一次!
        //        if (result){
        //            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //            System.out.println("
接收时间:"+simpleDateFormat.format(new Date()));
        //            System.out.println("
接收的消息:"+msg);
        //            // 
手动确认消息
        //            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        //        } else {
        //          //   
不能消费!
        //        }
    // 
能: 保证消息被消费成功    第二次消费,可以进来,但是要判断上一个消费者,是否将消息消费了。如果消费了,则直接返回,如果没有消费成功,我消费。
    //  在设置key 的时候给了一个默认值 0 ,如果消费成功,则将key的值 改为1
   
if (!result){
       
//  获取缓存key对应的数据
        String status = (String) this.redisTemplate.opsForValue().get(msgKey);
       
if ("1".equals(status)){
           
//  手动确认
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
           
return;
        }
else {
           
//  说明第一个消费者没有消费成功,所以消费并确认
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            System.
out.println("接收时间"+simpleDateFormat.format(new Date()));
            System.
out.println("接收的消息:"+msg);
           
//  修改redis 中的数据
            this.redisTemplate.opsForValue().set(msgKey,"1");
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),
false);
           
return;
        }
    }
    SimpleDateFormat simpleDateFormat =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    System.
out.println("接收时间"+simpleDateFormat.format(new Date()));
    System.
out.println("接收的消息:"+msg);

   
//  修改redis 中的数据
    this.redisTemplate.opsForValue().set(msgKey,"1");
   
//  手动确认消息
    channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}

5.3.2 改造订单service-order模块

service-order模块配置队列

添加依赖

<!--rabbitmq消息队列-->
<dependency>
    <groupId>com.atguigu.gmall</groupId>
    <artifactId>rabbit-util</artifactId>
    <version>1.0</version>
</dependency>

package com.atguigu.gmall.order.receiver;

  @Configuration
  public class OrderCanelMqConfig {

    @Bean
    public Queue delayQueue() {

        // 第一个参数建的queue的名字,第二个参数是是否支持持久化
        return new Queue(MqConst.QUEUE_ORDER_CANCEL, true);
    }

    @Bean
    public CustomExchange delayExchange() {
        Map<String, Object> args = new HashMap<String, Object>();
        args.put("x-delayed-type", "direct");
        return new CustomExchange(MqConst.EXCHANGE_DIRECT_ORDER_CANCEL, "x-delayed-message", true, false, args);
    }

    @Bean
    public Binding bindingDelay() {
        return BindingBuilder.bind(delayQueue()).to(delayExchange()).with(MqConst.ROUTING_ORDER_CANCEL).noargs();

    }

}

5.3.3 发送消息

创建订单时,发送延迟消息

修改保存订单方法

@Override
@Transactional
  public Long saveOrderInfo(OrderInfo orderInfo) {

    .....
    //送延迟队列,如果定未支付,取消订单
 rabbitService.sendDelayMessage(MqConst.EXCHANGE_DIRECT_ORDER_CANCEL, MqConst.ROUTING_ORDER_CANCEL, orderInfo.getId(), MqConst.DELAY_TIME);

    // 返回
    return orderInfo.getId();
}

5.3.4 接收消息

package com.atguigu.gmall.order.receiver;

  @Component
  public class OrderReceiver {

    @Autowired
    private OrderService orderService;

    //  监听的消息
        @SneakyThrows
    @RabbitListener(queues = MqConst.QUEUE_ORDER_CANCEL)
    public void cancelOrder(Long orderId , Message message, Channel channel){

        //  判断当前订单Id 不能为
        try {
            if (orderId!=null){
                //  发过来的是订单Id那么你就需要判断一下当前的订单是否已经支付了
                //  未支付的情况下关闭订单
                //  根据订单Id 查询orderInfo select * from order_info where id = orderId
                //  利用这个接口IService  实现类ServiceImpl 完成根据订单Id 查询订单信息 ServiceImpl 类底层还是使用的mapper
                OrderInfo orderInfo = orderService.getById(orderId);
                //  判断支付状态,进度状态
                if (orderInfo!=null && "UNPAID".equals(orderInfo.getOrderStatus())
                        && "UNPAID".equals(orderInfo.getProcessStatus())){
                    //  关闭订单
                    //  int i = 1/0;
                    orderService.execExpiredOrder(orderId);
                }
            }

        } catch (Exception e) {
            //  消息没有正常被消费者处理 记录日志后续跟踪处理! 
            e.printStackTrace();
        }
        //  手动确认消息 如果不确认有可能会到消息残留

        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
    }

}

5.3.5 编写取消订单接口与实现类

/**
 * 订单

 * @param orderId
 */
  void execExpiredOrder(Long orderId);
/**
 * 根据订单Id 修改订单状态
 * @param orderId
 * @param processStatus
 */
  void updateOrderStatus(Long orderId, ProcessStatus processStatus);
@Override
  public void execExpiredOrder(Long orderId) {
    // orderInfo
    updateOrderStatus(orderId, ProcessStatus.CLOSED);

}
@Override
  public void updateOrderStatus(Long orderId, ProcessStatus processStatus) {

    OrderInfo orderInfo = new OrderInfo();
    orderInfo.setId(orderId);
    orderInfo.setProcessStatus(processStatus.name());
    orderInfo.setOrderStatus(processStatus.getOrderStatus().name());
    orderInfoMapper.updateById(orderInfo);
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值