消息队列-RabbitMQ以及在项目中的应用

一、MQ应用场景

1、异步处理

2、应用解耦 比如订单服务

3、流量控制 比如秒杀系统

二、MQ概述

三、RabbitMQ相关概念

RabbitMQ操作流程:

四、RabbitMQ运行机制

Exchange类型:

五、spring boot整合RabbitMQ

引入依赖

<!--RabbitMQ队列场景启动器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

配置文件

spring.rabbitmq.host=192.168.80.133
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/

# confirmCallback(\u5F00\u542F\u53D1\u9001\u7AEF\u786E\u8BA4)
spring.rabbitmq.publisher-confirm-type=correlated
# returnCallback(\u5F00\u542F\u53D1\u9001\u7AEF\u6D88\u606F\u62B5\u8FBE\u961F\u5217\u7684\u786E\u8BA4)
spring.rabbitmq.publisher-returns=true
# \u53EA\u8981\u62B5\u8FBE\u961F\u5217\uFF0C\u4EE5\u5F02\u6B65\u53D1\u9001\u4F18\u5148\u56DE\u8C03\u6211\u4EEC\u8FD9\u4E2Areturnfirm
spring.rabbitmq.template.mandatory=true
# \u624B\u52A8ack\u6D88\u606F
spring.rabbitmq.listener.simple.acknowledge-mode=manual

测试发送消息:

import com.xhh.greymall.order.entity.OrderEntity;
import com.xhh.greymall.order.entity.OrderReturnReasonEntity;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.core.AmqpAdmin;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

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

@Slf4j
@SpringBootTest
class GreymallOrderApplicationTests {

    @Autowired
    AmqpAdmin amqpAdmin;

    @Autowired
    RabbitTemplate rabbitTemplate;

    @Test
    void sendMessageTest() {
//        OrderReturnReasonEntity reasonEntity = new OrderReturnReasonEntity();
//        reasonEntity.setId(1L);
//        reasonEntity.setCreateTime(new Date());
//        reasonEntity.setName("哈哈");
        //1、发送消息,如果发送的消息是个对象,我们会使用序列化机制,将对象写出去。Serializable
//        String msg = "hello world";
        for (int i = 0; i < 10; i++) {
            if (i % 2 == 0) {
                OrderReturnReasonEntity reasonEntity = new OrderReturnReasonEntity();
                reasonEntity.setId(1L);
                reasonEntity.setCreateTime(new Date());
                reasonEntity.setName("哈哈-" + i);
                //2、发送的对象类型
                rabbitTemplate.convertAndSend("hello-java-exchange", "hello.java", reasonEntity);
            } else {
                OrderEntity orderEntity = new OrderEntity();
                orderEntity.setOrderSn(UUID.randomUUID().toString());
                rabbitTemplate.convertAndSend("hello-java-exchange", "hello.java", orderEntity);
            }
            log.info("消息发送完成");
        }
//        //2、发送的对象类型
//        rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",reasonEntity);
//        log.info("消息发送完成{}",reasonEntity);
    }

    /**
     * String name, boolean durable, boolean autoDelete, Map<String, Object> arguments
     */
    @Test
    void createExchange() {
        //amqpAdmin
        //Exchange
        DirectExchange directExchange =
                new DirectExchange("hello-java-exchange", true, false);
        amqpAdmin.declareExchange(directExchange);
        log.info("Exchange[{}]创建成功", "hello-java-exchange");

    }

    /**
     * String name, boolean durable, boolean exclusive, boolean autoDelete,
     *
     * @Nullable Map<String, Object> arguments
     */
    @Test
    void createQueue() {
        Queue queue = new Queue("hello-java-queue", true, false, false);
        amqpAdmin.declareQueue(queue);
        log.info("Queue[{}]创建成功", "hello-java-queue");
    }

    /**
     * String destination, 【目的地】
     * DestinationType destinationType,【目的地类型】
     * String exchange, 【交换机】
     * String routingKey,【路由键】
     *
     * @Nullable Map<String, Object> arguments 【自定义参数】
     */
    @Test
    void createBinding() {
        Binding binding = new Binding(
                "hello-java-queue",
                Binding.DestinationType.QUEUE,
                "hello-java-exchange",
                "hello.java",
                null);
        amqpAdmin.declareBinding(binding);
        log.info("Binding[{}]创建成功", "hello-java-binding");
    }
}

测试接收消息:

 /**
     * queues:声明需要监听的所有队列
     *
     * org.springframework.amqp.core.Message
     *
     * 参数可以写以下类型
     * 1、Message message:原生消息详细详细。头+体
     * 2、T<发送的消息的类型> OrderReturnReasonEntity content;
     * 3、Channel channel:当前传输数据的通道
     *
     * Queue:可以很多人都来监听。只要收到消息,队列删除消息,而且只能有一个收到此消息
     *
     * 1、订单服务启动多个:同一个消息,只能有一个客户端能收到
     * 2、只有一个消息完全处理完,方法运行结束,我们就可以接收下一个消息
     */
//    @RabbitListener(queues = {"hello-java-queue"})
    @RabbitHandler
    public void receiveMessage(Message message,
                               OrderReturnReasonEntity content,
                               Channel channel){


        //消息头属性信息
        System.out.println("接收到消息" + content);
        MessageProperties messageProperties = message.getMessageProperties();
        System.out.println("消息处理完成-》" + content.getName());

        //channel内顺序自增的
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        System.out.println("deliveryTag==>" + deliveryTag);
        //签收货物,非批量模式
        try {
            if(deliveryTag %2 == 0){
                //收货
                channel.basicAck(deliveryTag,false);
                System.out.println("签收了货物..." + deliveryTag);
            } else {
                //退货 requeue=false 丢弃, requeue=true 发回服务器,服务器重新入队
                channel.basicNack(deliveryTag,false,true);
//                channel.basicReject();
                System.out.println("没有签收了货物..." + deliveryTag);
            }

        } catch (IOException e) {
            //网络中断
            e.printStackTrace();
        }
    }

    @RabbitHandler
    public void receiveMessage2(OrderEntity content){
        //消息头属性信息
        System.out.println("接收到消息" + content);
    }

六、Rabbit MQ消息确认机制-可靠抵达

1、可靠投递发送端确认:

    /**
     * 定制RabbitTemplate
     * 1、服务收到消息就回调
     *     1、spring.rabbitmq.publisher-confirm-type=correlated
     *     2、设置确认回调
     * 2、消息正确抵达队列进行回调
     *     1、spring.rabbitmq.publisher-returns=true
     *        spring.rabbitmq.template.mandatory=true
     *     2、设置确认回调
     *
     * 3、消费端确认(保证每个消息被正确消费,此时才可以broker删除这个消息)
     *     1、默认是自动确认的,只要消息接收到,客户端会自动确认,服务端就会移除这个消息
     *
     * @PostConstruct:构造器创建完成后执行这个方法
     */
//    @PostConstruct
    public void initRabbitTemplate(){
        //设置确认回调
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             *1、只要消息抵达Broker就ack=true
             * @param correlationData 当前消息的唯一关联数据(这个是消息的唯一id)
             * @param ack 消息是否成功收到
             * @param cause 失败的原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                /**
                 * 1.做好消息确认机制
                 * 2.每一个发送的消息都在数据库做好记录,定期将失败的消息再次发送
                 */
                //RabbitMQ服务器收到了
                System.out.println("confirm...correlationData[" + correlationData + "]=>ack[" + ack + "]=>cause[" + cause + "]");
            }
        });

        //设置消息抵达队列的确认回调
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            /**
             * 只要消息没有投递给指定的队列,就触发这个失败回调
             * @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);
            }
        });
    }

可靠投递消费端确认:

消费端确认(保证每个消息被正确消费,此时才可以broker删除这个消息)
*     1、默认是自动确认的,只要消息接收到,客户端会自动确认,服务端就会移除这个消息
*      问题:收到很多消息,自动回复给服务器ack,只有一个消息处理成功然后宕机了发生消息丢失
*      手动确认

修改配置:

代码:

七、Rabbit MQ延时队列---最终目的解决事务最终一致性

建议采用给队列设置过期时间,因为Rabbit MQ是惰性检查机制

升级版:

参照以上场景:

创建队列和交换机:

//@Bean Binding,Queue,Exchange

    /**
     * 容器中的Binding,Queue,Exchange都会自动创建(RabbitMQ没有的情况)
     * @return
     */
    @Bean
    public Queue orderDelayQueue(){
        Map<String,Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange","order-event-exchange");
        arguments.put("x-dead-letter-routing-key","order.release.order");
        arguments.put("x-message-ttl",60000);

        return new Queue("order.delay.queue", true, false, false,arguments);
    }
    @Bean
    public Queue orderReleaseQueue(){
        return new Queue("order.release.order.queue", true, false, false);
    }

    @Bean
    public Exchange orderEventExchange(){
        return new TopicExchange("order-event-exchange",true,false);
    }

    @Bean
    public Binding orderCreateOrderBinding(){
        return new Binding("order.delay.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.create.order",
                null);
    }
    @Bean
    public Binding orderReleaseOrderBinding(){
        return new Binding("order.release.order.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.release.order",
                null);
    }

测试:

@RabbitListener(queues = "order.release.order.queue")
    public  void listener(OrderEntity entity,Channel channel,Message message) throws IOException {
        System.out.println("收到过期消息,准备关闭订单"+entity.getOrderSn());
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
    }






@ResponseBody
    @GetMapping("/test/createOrder")
    public String createOrderTest(){
        //订单下单成功
        OrderEntity entity = new OrderEntity();
        entity.setOrderSn(UUID.randomUUID().toString());
        entity.setModifyTime(new Date());
//        System.out.println(entity);
        //给MQ发消息
        rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",entity);
        return "OK";
    }

效果:

在项目中应用------解锁库存:

在库存服务选编写Rabbit MQ配置:

@Configuration
public class MyRabbitConfig {

    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }

//    @RabbitListener(queues = "stock.release.stock.queue")
//    public void handle(Message message){
//
//    }


    @Bean
    public Exchange stockEventExchange(){
        return new TopicExchange("stock-event-exchange",true,false);
    }

    @Bean
    public Queue stockReleaseStockQueue(){
        return new Queue("stock.release.stock.queue",true,false,false);
    }

    @Bean
    public Queue stockDelayQueue(){
        Map<String,Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange","stock-event-exchange");
        arguments.put("x-dead-letter-routing-key","stock.release");
        arguments.put("x-message-ttl",120000); //2分钟

        return new Queue("stock.delay.queue",true,false,false,arguments);
    }

    @Bean
    public Binding stockReleaseBinding(){
        return new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.release.#",
                null);
    }

    @Bean
    public Binding stockLockedBinding(){
        return new Binding("stock.delay.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.locked",
                null);
    }
}

监听库存解锁:

库存锁定:

for (SkuWareHasStock hasStock : collect) {
            boolean skuStocked = false;
            Long skuId = hasStock.getSkuId();
            List<Long> wareIds = hasStock.getWareId();
            if (wareIds == null || wareIds.size() == 0) {
                //没有任何仓库有这个商品的库存
                throw new NoStockException(skuId);
            }
            //1、如果每个商品都锁定成功,将当前商品锁定了几件的工作单记录发送给MQ
            //2、如果锁定失败,前面保存的工作的回滚,发送出去的消息,即使要解锁记录,由于去数据库查不到id,所以就不用解锁
            //
            for (Long wareId : wareIds) {
                //成功就返回1,否则为0
                Long count = wareSkuDao.lockSkuStock(skuId,wareId,hasStock.getNum());
                if(count == 1){
                    skuStocked = true;
                    // TODO 告诉MQ库存锁定成功
                    WareOrderTaskDetailEntity entity =
                            new WareOrderTaskDetailEntity(null, skuId, null, hasStock.getNum(), taskEntity.getId(), wareId, 1);
                    wareOrderTaskDetailService.save(entity);
                    StockLockedTo lockedTo = new StockLockedTo();
                    lockedTo.setId(taskEntity.getId());
                    StockDetailTo stockDetailTo = new StockDetailTo();
                    BeanUtils.copyProperties(entity,stockDetailTo);
                    //只发id不行,防止回滚以后找不到数据
                    lockedTo.setDetail(stockDetailTo);

                    rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);
                    break;
                } else {
                    //当前仓库锁失败,重试下一个仓库
                }
            }
            if(skuStocked == false){
                //当前商品所有仓库都没有锁住
                throw new NoStockException(skuId);
            }
        }

解锁库存:

@RabbitHandler
    public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
        System.out.println("收解锁库存的消息");
        try {
            wareSkuService.unlockStock(to);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }

    }
@Override
    public void unlockStock(StockLockedTo to) {

        StockDetailTo detail = to.getDetail();
        Long detailId = detail.getId();
        //解锁
        //1、查询数据库关于这个订单的锁定库存信息
        //有:证明库存锁定成功
        //    解锁:订单情况:1.没有这个订单,必须解锁
        //                 2.有这个订单。不是解锁库存。
        //                      订单状态:已取消:解锁库存
        //                              没取消:不能解锁
        //没有:库存锁定失败,库存回滚了,这种情况无需解锁
        WareOrderTaskDetailEntity byId = wareOrderTaskDetailService.getById(detailId);
        if(byId != null){
            //解锁
            Long id = to.getId();
            WareOrderTaskEntity taskEntity = wareOrderTaskService.getById(id);
            String orderSn = taskEntity.getOrderSn();//根据订单号查询订单状态
            R r = orderFeignService.getOrderStatus(orderSn);
            if(r.getCode() == 0){
                //订单数据返回成功
                OrderVo data = r.getData(new TypeReference<OrderVo>() {
                });
                if(data == null || data.getStatus() == 4){
                    //订单不存在,订单已经被取消了,才能解锁库存
                    //detailId
                    if(byId.getLockStatus() == 1){
                        //当前库存工作单详情,状态1 已锁定但是未解锁才可以解锁
                        unLockStock(detail.getSkuId(),detail.getWareId(),detail.getSkuNum(),detailId);
                    }
                }
            } else {
                //消息拒绝以后重新放到队列里面,让别人继续消费解锁
                throw new RuntimeException("远程服务失败");
            }
        } else {
            //无需解锁
        }
    }
 private void unLockStock(Long skuId, Long wareId, Integer num, Long taskDetailId){
        //库存解锁
        wareSkuDao.unLockStock(skuId,wareId,num);
        //更新库存工作单的状态
        WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity();
        entity.setId(taskDetailId);
        entity.setLockStatus(2); //变为已解锁
        wareOrderTaskDetailService.updateById(entity);
    }

定时关单:

关单监听器:

@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {

    @Autowired
    OrderService orderService;

    @RabbitHandler
    public void listener(OrderEntity entity, Channel channel, Message message) throws IOException {
        System.out.println("收到过期的订单消息,准备关闭订单:" + entity.getOrderSn());
        try {
            orderService.closeOrder(entity);
            //手动接消息
            //手动调用支付宝收单

            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
}

关闭订单:

@RabbitHandler
    public void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {
        System.out.println("订单关闭,准备解锁库存...");
        try {
            wareSkuService.unlockStock(orderTo);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
 @Override
    public void closeOrder(OrderEntity entity) {
        //查询当前这个订单的最新状态
        OrderEntity orderEntity = this.getById(entity.getId());
        if(orderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()){
            //关单
            OrderEntity update = new OrderEntity();
            update.setId(entity.getId());
            update.setStatus(OrderStatusEnum.CANCEL.getCode());
            this.updateById(update);
            OrderTo orderTo = new OrderTo();
            BeanUtils.copyProperties(orderEntity,orderTo);
            //发给MQ一个
            try {
                //TODO 保证消息一定会发送出去,每一个消息都可以做好日志记录(给数据库保存每个消息的详细消息)
                rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
            } catch (Exception e) {
                //TODO 将没发送的消息进行重试发送。
                //while
            }
        }
    }
//防止订单服务卡顿,导致订单状态消息一直改不了,库存消息优先到期,查订单状态新建状态,
    //导致卡顿的订单,永远不能解锁库存
    @Transactional
    @Override
    public void unlockStock(OrderTo orderTo) {
        String orderSn = orderTo.getOrderSn();
        //查一下最新库存的状态,防止重复解锁库存
        WareOrderTaskEntity taskEntity = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);
        Long id = taskEntity.getId();
        //按照工作单找到所有没有解锁的库存,进行解锁
        List<WareOrderTaskDetailEntity> entities = wareOrderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>()
                .eq("task_id", id)
                .eq("lock_status", 1));
        for (WareOrderTaskDetailEntity entity : entities) {
            unLockStock(entity.getSkuId(),entity.getWareId(),entity.getSkuNum(),entity.getId());
        }
    }

消息丢失、积压、重复等解决方案:

针对第一种失败:

日志记录可以在数据库创建一张消息表:

然后定期扫描数据库,将没发出去的消息在重新发一次

 

 

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页