springboot整合rabbitmq和延时队列(二)

springboot整合rabbitmq

  • 一、案例结构
  • 二、集成流程
    • 1.1、依赖
    • 1.2、配置文件
    • 1.3、配置类-序列化机制和定制RabbitTemplate
  • 三、全局常量类和实体类
    • 1.1、常量类 MqConstant
    • 1.2、实体类
  • 四、测试案例一
    • 1.1、创建和发送
    • 1.2、监听消费
  • 三、延时队列测试
    • 1.1、采用@bean创建
    • 1.2、发送消息
    • 1.3、监听消费
  • 四、订单库存案例(消息事务)
    • 1.1、创建

一、案例结构

springboot整合rabbitmq和延时队列(二)_第1张图片


第一部分==>rabbitMQ介绍与安装说明(一)

二、集成流程

1.1、依赖

<!--rabbitmq -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>


<!--slf4j核心依赖 日志门面-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>
<!-- logback依赖 日志实现-->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

<!--lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.8</version>
</dependency>

<!--mysql依赖85多了ssl-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.19</version>
</dependency>

<!--mybatis-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>
<!--分页插件-->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.2.12</version>
</dependency>

<!--fastjson-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.15</version>
</dependency>

1.2、配置文件

#======================↓↓↓↓rabbitmq配置-开始↓↓↓↓======================
# rabbitmq地址
spring.rabbitmq.host=192.168.10.10
# rabbitmq端口
spring.rabbitmq.port=5672
# 虚拟主机
spring.rabbitmq.virtual-host=/
# 账号密码默认guest-这边我是新建了个超级管理员的用户
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
# 开启发送端确认
spring.rabbitmq.publisher-confirms=true
# 开启发送端消息抵达队列的确认
spring.rabbitmq.publisher-returns=true
# 只要抵达队列,以异步发送优先回调我们这个returnconfirm
spring.rabbitmq.template.mandatory=true
# 手动ack消息
spring.rabbitmq.listener.simple.acknowledge-mode=manual


#======================↓↓↓↓mysql配置-开始↓↓↓↓=================================
# 配置数据源 mysql8
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/mq_test?serverTimezone\=Asia/Shanghai&useUnicode\=true&characterEncoding\=utf8&allowMultiQueries\=true&rewriteBatchedStatements\=true
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver



#======================↓↓↓↓mybatis配置-开始↓↓↓↓=================================
# 映射文件xml的位置
mybatis.mapper-locations=classpath:mapper/*Mapper.xml
# 使全局的映射器启用或禁用缓存
mybatis.configuration.cache-enabled=true
# 全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载
mybatis.configuration.lazy-loading-enabled=true
# 当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载。
mybatis.configuration.aggressive-lazy-loading=false
# 是否允许单条sql 返回多个数据集 (取决于驱动的兼容性) 默认:true
mybatis.configuration.multiple-result-sets-enabled=true
# 是否可以使用列的别名 (取决于驱动的兼容性) 默认:true
mybatis.configuration.use-column-label=true
# 指定MyBatis如何自动映射 数据基表的列 NONE:不隐射 PARTIAL:部分 FULL:全部
mybatis.configuration.auto-mapping-behavior=partial
# 这是默认的执行类型 (SIMPLE: 简单; REUSE: 执行器可能重复使用prepared statements语句;BATCH: 执行器可以重复执行语句和批量更新)
mybatis.configuration.default-executor-type=simple
# sql执行时间超时时间
mybatis.configuration.default-statement-timeout=25
# 允许在嵌套语句中使用分页(RowBounds)。如果允许使用则设置为false。
mybatis.configuration.safe-row-bounds-enabled=false
# 是否使用驼峰命名法转换字段
mybatis.configuration.map-underscore-to-camel-case=false
# 设置本地缓存范围 session:就会有数据的共享 statement:语句范围 (这样就不会有数据的共享 ) 默认:session
mybatis.configuration.local-cache-scope=session
# 设置当JDBC类型为空时,某些驱动程序 要指定值,默认:OTHER。当写入null值的字段时,部分数据库需要指定null的数据类型.mysql不用设置。【oracle需要设置】对应org.apache.ibatis.type.JdbcType的枚举值
mybatis.configuration.jdbc-type-for-null=other
# 指定对象哪个的方法触发一次延迟加载
mybatis.configuration.lazy-load-trigger-methods=equals,clone,hashCode,toString
# [默认false,推荐使用true] 如果数据为空的字段,则该字段省略不显示,查询数据映射数据类型使用的是Map。当字段值为null时,mybatis映射返回字段的时候会忽略,而原接口是null值也返回,为了兼容,需要设置不忽略null字段
mybatis.configuration.call-setters-on-nulls=true
# mybatis指定日志输出的前缀
mybatis.configuration.log-prefix=mybatis_
# 打印sql-开发时启用,会影响性能【使用boot默认的logback】
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

# 将java类型(javaType)转化为jdbc类型(jdbcType),或者将jdbc类型(jdbcType)转化为java类型(javaType)。【这个参数还没搞得很明白】
# mybatis.type-handlers-package=com.mapper.typehandler
# 允许JDBC生成主键。需要驱动器支持。如果设为了true,这个设置将强制使用被生成的主键,有一些驱动器不兼容不过仍然可以执行。 默认:false
# mybatis.configuration.use-generated-keys=false
# 指定代理工厂:mybatis 实现2个:CglibProxyFactory和JavassistProxyFactory(默认)
# mybatis.configuration.proxy-factory=CGLIB


#======================↓↓↓↓pagehelper配置-开始↓↓↓↓=================================
# 分页
pagehelper.helperDialect=mysql
pagehelper.reasonable=true
pagehelper.supportMethodsArguments=true
pagehelper.params=count=countSql

1.3、配置类-序列化机制和定制RabbitTemplate

  1. rabbitmq默认采用jdk的序列化机制,我们改成json
  2. 定制RabbitTemplate-开启ack模式

MyRabbitConfig01

package sqy.config;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

/**
 * @author suqinyi
 * @Date 2022/3/8
 * rabbitmq配置
 *  1、更换成json序列化
 *  2、定制RabbitTemplate-开启ack模式
 */
@Configuration
public class MyRabbitConfig01 {

    RabbitTemplate rabbitTemplate;

    /**
     * 默认使用jdk的-使用默认的jdk序列化传输对象的时候实体类要实现序列化接口【implements Serializable】
     * 更换成使用JSON序列化机制,进行消息转换
     */
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }


    @Primary//注入容器的意思-和@Autowired意思差不多,但是Primary优先级更高
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        this.rabbitTemplate = rabbitTemplate;
        rabbitTemplate.setMessageConverter(messageConverter());
        initRabbitTemplate();
        return rabbitTemplate;
    }


    /**
     * TODO 定制RabbitTemplate-开启ack模式
     * 1、服务器收到消息就回调
     *      1、spring.rabbitmq.publisher-confirms=true
     *      2、设置确认回调ConfirmCallback
     * 2、消息正确抵达队列进行回调
     *      1、 spring.rabbitmq.publisher-returns=true
     *          spring.rabbitmq.template.mandatory=true
     *      2、设置确认回调ReturnCallback
     *
     * 3、消费端确认(保证每个消息被正确消费,此时才可以broker删除这个消息)。
     *      spring.rabbitmq.listener.simple.acknowledge-mode=manual 手动签收
     *      1、默认是自动确认的,只要消息接收到,客户端会自动确认,服务端就会移除这个消息
     *          问题:
     *              我们收到很多消息,自动回复给服务器ack,只有一个消息处理成功,宕机了。就会发生消息丢失;
     *              消费者手动确认模式。只要我们没有明确告诉MQ,货物被签收。没有Ack,
     *                  消息就一直是unacked状态。即使Consumer宕机。消息不会丢失,会重新变为Ready,下一次有新的Consumer连接进来就发给他
     *      2、如何签收:
     *          channel.basicAck(deliveryTag,false);签收;业务成功完成就应该签收
     *          channel.basicNack(deliveryTag,false,true);拒签;业务失败,拒签
     */
//    @PostConstruct //MyRabbitConfig对象创建完成以后,执行这个方法
    public void initRabbitTemplate(){
        //设置确认回调
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             * todo 只要消息抵达Broker就ack=true
             * @param correlationData 当前消息的唯一关联数据(这个是消息的唯一id)
             * @param ack  消息是否成功收到
             * @param cause 失败的原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                /**
                 * 1、做好消息确认机制(pulisher,consumer【手动ack】)
                 * 2、每一个发送的消息都在数据库做好记录。定期将失败的消息再次发送一遍
                 */
                //服务器收到了;
                //修改消息的状态
                System.out.println("服务器收到...correlationData["+correlationData+"]==>ack["+ack+"]==>cause["+cause+"]");
            }
        });

        //设置消息抵达队列的确认回调
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            /**
             * todo 只要消息没有投递给指定的队列,就触发这个失败回调
             * @param message   投递失败的消息详细信息
             * @param replyCode 回复的状态码
             * @param replyText 回复的文本内容
             * @param exchange  当时这个消息发给哪个交换机
             * @param routingKey 当时这个消息用哪个路由键
             */
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                //接收不到消息这边会接收到错误信息。【可以结合数据库-修改数据库当前消息的状态->记录错误然后重试】
                System.out.println("Fail Message["+message+"]==>replyCode["+replyCode+"]==>replyText["+replyText+"]===>exchange["+exchange+"]===>routingKey["+routingKey+"]");
            }
        });
    }


}


三、全局常量类和实体类

1.1、常量类 MqConstant

package sqy.constant;

/**
 * @author suqinyi
 * @Date 2022/3/13
 * mq里面的交换机、队列、绑定、路由key的常量值
 */
public class MqConstant {

    /**
     * 测试1--常量1
     * Test01CreateController
     * Test01Listener
     */
    public static final String C01_EXCHANGE_NAME = "hello-exchange";
    public static final String C01_QUEUE_NAME = "hello-queue";
    public static final String C01_ROUTING_KEY = "helloMQ.java";
    public static final String C01_ERROR_ROUTING_KEY = "helloMQ.java.golang";


    //------------------------------------

    /**
     * 测试2-常量
     * MyRabbitConfig02
     * Test02DelayController
     * Test02DelayMqListener
     */
    public static final String C02_TOPIC_EXCHANGE = "test.topic.exchange";
    public static final String C02_DELAY_QUEUE_A = "test.delay.queue.a";
    public static final String C02_RELEASE_QUEUE_B = "test.release.queue.b";
    public static final String C02_DELAY_ROUTINGKEY = "test.delay.routingKey";
    public static final String C02_RELEASE_ROUTINGKEY = "test.release.routingKey";
    public static final Integer C02_MEG_TTL = 1 * 60 * 1000;//1分钟-60000毫秒

    //------------------------------------
    /**
     * 案例3
     */
    public static final String C03_TOPIC_EXCHANGE = "demo.topic.exchange";
    //....

}

1.2、实体类

Student

package sqy.pojo;
import lombok.Data;
import java.util.Date;
/**
 * @author suqinyi
 * @Date 2022/3/9
 */
@Data
public class Student  {
    private String studentNo;
    private String studentName;
    private Date studentBirth;
}

Teacher

package sqy.pojo;
import lombok.Data;
import java.util.Date;

/**
 * @author suqinyi
 * @Date 2022/3/9
 */
@Data
public class Teacher {
    private String teacherNo;
    private String teacherName;
    private Date teacherBirth;
}

MqMessage

package sqy.pojo.demo;
import lombok.Data;
import lombok.ToString;
import java.util.Date;

/**
 * @author suqinyi
 * @Date 2022/3/16
 * mq的消息表,防止消息丢失,重复消费,也能用来处理消息积压的问题
 */
@Data
@ToString
public class MqMessage {

    private String messageId;//消息的唯一id
    private String content;//消息内容
    private String toExchange;//交换机
    private String routingKey;//路由键
    private String classType;//实体类类型
    private String messageStatus;//0-新建 1-已发送 2-错误抵达 3-已抵达
    private Date createTime;//创建时间
    private Date updateTime;//更新时间
}

Order

package sqy.pojo.demo;
import lombok.Data;
import lombok.ToString;

/**
 * @author suqinyi
 * @Date 2022/3/16
 * 订单表
 */
@Data
@ToString
public class Order {
    private String orderId;//订单号
    private String orderUserId;//用户id
    private String orderStatus;//订单状态【0->待付款;1->待发货;2->已发货;3->已完成;4->已关闭;5->无效订单】
}

OrderWareTask

package sqy.pojo.demo;

import lombok.Data;
import lombok.ToString;

/**
 * @author suqinyi
 * @Date 2022/3/16
 * 订单和库存的关联消息表-用于mq的消息事务
 */
@Data
@ToString
public class OrderWareTask {
    private int id;//自增主键
    private String orderId;//订单号-关联订单表
    private String productId;//商品id-关联商品
    private String productNum;//商品数量
    private String wareId;//仓库id
    private String lockStatus;//任务状态-1锁定 2解锁  [数据库锁-乐观锁]
}

Product

package sqy.pojo.demo;
import lombok.Data;
import lombok.ToString;

/**
 * @author suqinyi
 * @Date 2022/3/16
 * 商品信息表
 */
@Data
@ToString
public class Product {
    private String productId;
    private String productName;
    private String productPrice;
}

Warehouse

package sqy.pojo.demo;
import lombok.Data;
import lombok.ToString;
import java.math.BigDecimal;

/**
 * @author suqinyi
 * @Date 2022/3/16
 * 仓库表
 */
@Data
@ToString
public class Warehouse {
    private String wareId;//仓库id
    private String wareName;//仓库名称
    private String wareAddr;//地址
    private String purchaseProductId;//采购物品id
    private String purchaseProductName;//采购物品name
    private BigDecimal purchaseProductPrice;//采购价格
}


四、测试案例一

1.1、创建和发送

package sqy.controller;


import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import sqy.constant.MqConstant;
import sqy.pojo.Student;
import sqy.pojo.Teacher;

import java.util.Date;
import java.util.UUID;

/**
 * @author suqinyi
 * @Date 2022/3/8
 * 测试rabbitmq方法-入门测试
 * 控制台ip:15672
 */
@Slf4j
@RestController
public class Test01CreateController {

    @Autowired
    AmqpAdmin amqpAdmin;

    @Autowired
    RabbitTemplate rabbitTemplate;

    /**
     * public static final String C01_EXCHANGE_NAME = "hello-exchange";
     * public static final String C01_QUEUE_NAME = "hello-queue";
     * public static final String C01_ROUTING_KEY = "helloMQ.java";
     * public static final String C01_ERORR_ROUTING_KEY = "helloMQ.java.golang";
     */


    /**
     * 创建交换机Exchange
     * 创建队列Queue
     * 绑定交换机和队列-binding
     * 

交换机类型 * DirectExchange:按照routingkey分发到指定队列 * TopicExchange:多关键字匹配 * FanoutExchange: 将消息分发到所有的绑定队列,无routingkey的概念 * HeadersExchange :通过添加属性key-value匹配 *

交换机参数说明 * DirectExchange(String name[交换机名称], boolean durable[是否持久化], * boolean autoDelete[是否自动删除], Map arguments[其他参数]) *

队列参数 * public Queue(String name[队列名称], boolean durable[是否持久化], boolean exclusive[是否独占], * boolean autoDelete[是否自动删除], Map arguments[其他参数]) *

绑定参数 * public Binding(String destination[目的地], Binding.DestinationType destinationType[目的地类型-枚举], * String exchange[交换机], String routingKey[路由键], @Nullable Map arguments[自定义参数]) */ //localhost:8080/testCreateMQ @GetMapping("/testCreateMQ") public String testCreateMQ() { //也可以用 BindingBuilder 、ExchangeBuilder、QueueBuilder来创建 //[创建-声明]交换机 DirectExchange directExchange = new DirectExchange(MqConstant.C01_EXCHANGE_NAME, true, false); amqpAdmin.declareExchange(directExchange); log.info("Exchange[{}]创建成功", MqConstant.C01_EXCHANGE_NAME); //[创建-声明]队列 Queue queue = new Queue(MqConstant.C01_QUEUE_NAME, true, false, false); amqpAdmin.declareQueue(queue); log.info("Queue[{}]创建成功", MqConstant.C01_QUEUE_NAME); //将exchange指定的交换机和destination目的地进行绑定,使用routingKey作为指定的路由键【绑定】 Binding binding = new Binding(MqConstant.C01_QUEUE_NAME, Binding.DestinationType.QUEUE, MqConstant.C01_EXCHANGE_NAME, MqConstant.C01_ROUTING_KEY, null); amqpAdmin.declareBinding(binding); log.info("Binding创建成功-路由key为:[{}]", MqConstant.C01_ROUTING_KEY); return "创建交换机-队列成功,并指定 <路由key> 绑定"; } /** * 发送20条消息 * localhost:8080/sendMq */ @GetMapping("/sendMq") public String sendMq(@RequestParam(value = "num", defaultValue = "20") Integer num) { for (int i = 0; i < num; i++) { if (i<=10){ if (i % 2 == 0) { Student student = new Student(); student.setStudentNo(i+"号"); student.setStudentName("学生" + i); student.setStudentBirth(new Date()); //public void convertAndSend(String exchange【交换机】, String routingKey【路由key】, Object object【消息】, @Nullable CorrelationData correlationData)【消息的唯一标识<推荐使用分布式id>】 rabbitTemplate.convertAndSend(MqConstant.C01_EXCHANGE_NAME, MqConstant.C01_ROUTING_KEY, student, new CorrelationData("学生"+UUID.randomUUID().toString().substring(0,7))); } else { Teacher teacher=new Teacher(); teacher.setTeacherNo(i+"号"); teacher.setTeacherName("教师"+i); //使用正常的路由键 rabbitTemplate.convertAndSend(MqConstant.C01_EXCHANGE_NAME, MqConstant.C01_ROUTING_KEY, teacher, new CorrelationData("教师"+UUID.randomUUID().toString().substring(0,7))); //使用错误的路由键-模拟进入队列失败 //rabbitTemplate.convertAndSend(MqConstant.C01_EXCHANGE_NAME, MqConstant.C01_ERROR_ROUTING_KEY, teacher, new CorrelationData("教师"+UUID.randomUUID().toString().substring(0,7))); } }else { if (i % 2 != 0) { Student student = new Student(); student.setStudentNo(i+"号"); student.setStudentName("学生" + i); student.setStudentBirth(new Date()); //交换机、路由key、消息、消息的唯一标识<推荐使用分布式id> rabbitTemplate.convertAndSend(MqConstant.C01_EXCHANGE_NAME, MqConstant.C01_ROUTING_KEY, student, new CorrelationData("学生"+UUID.randomUUID().toString().substring(0,7))); } else { Teacher teacher=new Teacher(); teacher.setTeacherNo(i+"号"); teacher.setTeacherName("教师"+i); //使用正常的路由键- rabbitTemplate.convertAndSend(MqConstant.C01_EXCHANGE_NAME, MqConstant.C01_ROUTING_KEY, teacher, new CorrelationData("教师"+UUID.randomUUID().toString().substring(0,7))); //使用错误的路由键-模拟进入队列失败 //rabbitTemplate.convertAndSend(MqConstant.C01_EXCHANGE_NAME, MqConstant.C01_ERROR_ROUTING_KEY, teacher, new CorrelationData("教师"+UUID.randomUUID().toString().substring(0,7))); } } } return "发送20条消息完成"; } }

1.2、监听消费

package sqy.service;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import sqy.constant.MqConstant;
import sqy.pojo.Student;
import sqy.pojo.Teacher;

/**
 * @author suqinyi
 * @Date 2022/3/9
 */
//监听队列
@RabbitListener(queues = {MqConstant.C01_QUEUE_NAME})
@Service
public class Test01Listener {


    /**
     * queues:声明需要监听的所有队列
     * 

* org.springframework.amqp.core.Message *

* 参数可以写一下类型 * 1、Message message:原生消息详细信息。头+体 * 2、T<发送的消息的类型> OrderReturnReasonEntity content; * 3、Channel channel:当前传输数据的通道 *

* Queue:可以很多人都来监听。只要收到消息,队列删除消息,而且只能有一个收到此消息 * 场景: * 1)、订单服务启动多个;同一个消息,只能有一个客户端收到 * 2)、 只有一个消息完全处理完,方法运行结束,我们就可以接收到下一个消息 */ @RabbitHandler public void receiveMessage01(Message message, Student student, Channel channel) throws InterruptedException { byte[] body = message.getBody();//消息体 MessageProperties properties = message.getMessageProperties();//消息头属性信息 Thread.sleep(2000);//模拟业务耗时 // System.out.println("接受学生消息=>" + student); //channel内按顺序自增的。 long deliveryTag = message.getMessageProperties().getDeliveryTag(); System.out.println("信道自增交货标签:" + deliveryTag); //todo basicAck和basicNack实际场景不会一起使用 try { if (deliveryTag % 2 == 0) { //模拟系统中断,debug停止,后面代码还是会执行。所以模拟的话也可以用kill后台进程 if (deliveryTag > 6) { System.out.println("开始闪断-网络中断........."); System.exit(1);//模拟项目突然挂了,队列里面消息的情况 } //信道里面-手动签收 basicAck(long deliveryTag[消息的id], boolean multiple[是否批量签收-以可以理解为签收当前>=deliveryTag的消息]) channel.basicAck(deliveryTag, false); System.out.println("学生-签收.." + student.getStudentName()); } else { /** * basicNack(long deliveryTag[消息id], boolean multiple[批量签收], boolean requeue[是否重新入队]) * requeue=false 丢弃 requeue=true 发回服务器,服务器重新入队。 */ // channel.basicReject();//不签收法1 channel.basicNack(deliveryTag, false, false);//不签收法2--参数多用这个 System.out.println("学生--没有签收.." + student.getStudentName()); } } catch (Exception e) { //网络中断 } } /** * 开启手动确认后 * 如果没手动确认-消息会一直消费,不会删除 * * @param teacher 接受的消息体-这里用实体类接收 */ @RabbitHandler public void receiveMessage02(Message message, Teacher teacher, Channel channel) throws Exception { Thread.sleep(2000);//模拟业务耗时 long deliveryTag = message.getMessageProperties().getDeliveryTag(); //System.out.println("接收到消息..." + teacher); //签收 channel.basicAck(deliveryTag, false); System.out.println("教师-签收了=>" + teacher.getTeacherName()); } }

三、延时队列测试

1.1、采用@bean创建

MyRabbitConfig02

package sqy.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import sqy.constant.MqConstant;

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

/**
 * @author suqinyi
 * @Date 2022/3/13
 * spring也可以用bean的方式创建交换机、队列、绑定
 * 也可以用 BindingBuilder 、ExchangeBuilder、QueueBuilder来创建
 * 也可以用 new Binding、new xxxExchange、new Queue的方式创建
 */
@Configuration
public class MyRabbitConfig02 {

    /**
     * TODO 使用@Bean的方式创建交换机、队列、绑定
     *  rabbitmq是懒加载-这种创建方式,只会在rabbitmq第一次连上主动创建,不然得有监听者才会创建出来
     *  RabbitMQ 只要有交换机队列绑定。@Bean声明属性发生变化也不会覆盖
     */

    /**
     * 消息携带路由键进行匹配
     * 复用交换机
     * 流程:发送一个消息-->交换机a-->队列a【延迟队列】
     *      队列a的消息1分钟过期-->交换机a-->交给队列b【释放队列】
     *      客户来监听队列b。
     * 一个消息-->交换机a-->【延迟队列】--(消息过期了)-->交换机a--->队列b<-----客户端监听
     *
     * 其他过期的消息称为死信,队列b为存放死信,客户端监听队列b
     *
     * <>消息根据路由键来匹配
     * 2队列[1个延迟队列a、1个释放队列b]、延迟队列消息到期交给释放队列
     * 1话题交换机(对其复用)
     * 2个路由键、
     * 2个绑定、
     * 1个监听
     *
     */


    //发布订阅模式的交换机
    @Bean
    public Exchange testTopicExchange() {
        //String name[名称], boolean durable[持久化], boolean autoDelete[自动删除], Map arguments[其他参数]
        return new TopicExchange(MqConstant.C02_TOPIC_EXCHANGE, true, false);
    }


    //延迟队列a,存放有效期为1分钟的消息
    @Bean
    public Queue testDelayQueueA() {
        Map<String, Object> arguments = new HashMap<>();
        /**
         * 官网
         * x-dead-letter-exchange: 交换机名称
         * x-dead-letter-routing-key: 死信的路由键
         * x-message-ttl: 过期时间-毫秒
         */
        arguments.put("x-dead-letter-exchange", MqConstant.C02_TOPIC_EXCHANGE);
        arguments.put("x-dead-letter-routing-key", MqConstant.C02_RELEASE_ROUTINGKEY);
        arguments.put("x-message-ttl", MqConstant.C02_MEG_TTL);
        //String name[名称], boolean durable[持久化], boolean exclusive[独占], boolean autoDelete[自动删除], Map arguments[其他参数]
        Queue queue = new Queue(MqConstant.C02_DELAY_QUEUE_A, true, false, false, arguments);
        return queue;
    }

    //释放队列b,存放过期消息的队列-客户端监听-用于释放消息
    @Bean
    public Queue testReleaseQueueB() {
        Queue queue = new Queue(MqConstant.C02_RELEASE_QUEUE_B, true, false, false);
        return queue;
    }


    //绑定队列a和交换机-指定路由键
    @Bean
    public Binding testDelayQueueABingding() {
        //String destination[目的地], DestinationType destinationType[枚举-目的地类型], String exchange[交换机名称],
        //      String routingKey[路由键], Map arguments[其他参数]
        return new Binding(MqConstant.C02_DELAY_QUEUE_A,
                Binding.DestinationType.QUEUE,
                MqConstant.C02_TOPIC_EXCHANGE,
                MqConstant.C02_DELAY_ROUTINGKEY,
                null);
    }

    //绑定队列b和交换机-指定路由键
    @Bean
    public Binding testReleaseQueueBBingding() {
        return new Binding(MqConstant.C02_RELEASE_QUEUE_B,
                Binding.DestinationType.QUEUE,
                MqConstant.C02_TOPIC_EXCHANGE,
                MqConstant.C02_RELEASE_ROUTINGKEY,
                null);
    }
}

1.2、发送消息

package sqy.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.RestController;

/**
 * @author suqinyi
 * @Date 2022/3/13
 * 测试下延时队列
 */
@RestController
public class Test02DelayController {

    @Autowired
    RabbitTemplate rabbitTemplate;

    /**
     * 给队列a发送消息
     */
    //http://127.0.0.1:8080/sendMsgToQueueA
    @GetMapping("/sendMsgToQueueA")
    public String sendMsgToQueueA() {
        rabbitTemplate.convertAndSend("test.topic.exchange", "test.delay.routingKey", "这个一条存活时间为1分钟的消息");
        return "向队列a发送1条消息成功";
    }
}

1.3、监听消费

package sqy.service;

import com.alibaba.fastjson.JSON;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import sqy.constant.MqConstant;

/**
 * @author suqinyi
 * @Date 2022/3/13
 */

@Service
public class Test02DelayMqListener {

    @RabbitListener(queues = {MqConstant.C02_RELEASE_QUEUE_B})
    public void receiveMessage02(Message message, Channel channel) throws Exception {
        //System.out.println(JSON.toJSONString(message.getBody()));
        System.out.println("这个已过期的被转交给队列b的消息:==>" + message);
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        //签收
        channel.basicAck(deliveryTag, false);
        System.out.println("释放了消息");
    }
}


四、订单库存案例(消息事务)

1.1、创建

package sqy.config;
import org.springframework.context.annotation.Configuration;


/**
 * @author suqinyi
 * @Date 2022/3/13
 * spring也可以用bean的方式创建交换机、队列、绑定
 * 也可以用 BindingBuilder 、ExchangeBuilder、QueueBuilder来创建
 * 也可以用 new Binding、new xxxExchange、new Queue的方式创建
 */
@Configuration
public class MyRabbitConfig03 {
    /**
     * TODO 使用@Bean的方式创建交换机、队列、绑定
     *  rabbitmq是懒加载-这种创建方式,只会在rabbitmq第一次连上主动创建,不然得有监听者才会创建出来
     *  RabbitMQ 只要有交换机队列绑定。@Bean声明属性发生变化也不会覆盖
     *
     *  模拟下订单、锁库存的场景
     *  表:订单表、库存表、仓库表【仓库-采购单-详情...这边简化模拟】、商品表、任务消息表、消息记录表
     *
     *  流程:1、用户下单【订单+商品+数量】-->未付款-->锁定库存-->1分钟后还没付款-->取消订单-->解锁库存
     *       2、用户下单【订单+商品+数量】-->用户取消订单-->修改订单状态-->库存数量回滚
     *       采用主动+被动补偿的机制
     *
     *  如何保证消息不丢失-发送消息就记录,开启ack模式。消息接收失败会在回调里面收到消息-可以记录起来
     *
     *   TODO 有空在写demo
     */



}

后面补充…


上一篇:rabbitMQ介绍与安装说明(一)

你可能感兴趣的:(消息队列,rabbitmq,spring,boot,java)