谷粒商城-订单服务

目录

商城业务-订单服务-RabbitMQ延时队列

商城业务-订单服务-延时队列定时关单模拟

商城业务-订单服务-创建业务交换机&队列

商城业务-订单服务-监听库存解锁

商城业务-订单服务-库存解锁逻辑

商城业务-订单服务-库存自动解锁完成

商城业务-订单服务-测试库存自动解锁

商城业务-订单服务-定时关单完成

商城业务-订单服务-消息丢失、积压、重复等解决方案


商城业务-订单服务-RabbitMQ延时队列

使用消息队列的目的是:保证数据的最终一致性

谷粒商城-订单服务_第1张图片

采用定时任务的方式:每隔一段时间进行全表的扫描,会消耗系统内存和增加数据库的压力,最致命的是存在较大的时间误差

假如:10:00定时任务开始执行,则10:01有用户下订单但未支付,10:30的时候定时任务再次执行,这个订单还差1分钟才能进行关单操作,因此,下一次扫描到它要等到11:00,存在着29分钟的误差时间。

谷粒商城-订单服务_第2张图片

采用消息队列可以完美的解决定时任务所带来的缺陷 

假如:10:00下订单,再下订单之前先给消息队列发送一条下单消息,等30分钟自动发送关闭订单消息,监听服务收到消息,去查看此订单是否完成支付,若未完成支付则关闭订单。误差也就一两分钟。对于解锁库存也是同理。

谷粒商城-订单服务_第3张图片

设置消息的过期时间: 在过期时间内都没有被消费则此消息将会被丢弃并称之为死信

设置队列的过期时间:在此过期时间内都没有队列被客户端连接则队列里的所有消息都被成为死信

谷粒商城-订单服务_第4张图片

死信路由: 消息过期未被消费的,则消息会被交给一个指定的路由器,这个路由器由于只接收死信所以被成为死信路由

谷粒商城-订单服务_第5张图片

RabbitMQ实现延时队列的原理:通过设置队列的过期时间使消息都变成死信,此队列是不能被任何服务监听的,当消息过期时,通过死信路由将死信路由给指定队列,指定队列只接收死信也就是延时消息,服务器专门监听指定队列从而达到定时任务的效果。

实现1:给队列设置过期时间,推荐使用

谷粒商城-订单服务_第6张图片

实现方式2:给消息设置过期时间,不推荐使用

不推荐使用的原因是:RabbitMQ采用的是懒检查,假如第一个消息设置的是5分钟过期,第二个消息设置的是2分钟过期,第三个消息设置的是30s过期,RabbitMQ过来一看消息5分钟后才过期,那么5分钟之后才会来将消息路由并不会关注后面消息的过期时间。

商城业务-订单服务-延时队列定时关单模拟

按照下图逻辑,模拟下单成功1分钟后,收到关闭订单的消息

谷粒商城-订单服务_第7张图片

谷粒商城-订单服务_第8张图片

编写队列、交换机,绑定关系 

容器中的 Binding、Queue、Exchange 都会自动创建(RabbitMQ没有的情况)

RabbitMQ只要有,@Bean声明的属性发生变化也不会覆盖

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 java.util.HashMap;
import java.util.Map;

@Configuration
public class MyMQConfig {

    @Bean
    public Queue orderDelayQueue(){
        Map 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);
        // String name, boolean durable, boolean exclusive, boolean autoDelete,
        //			@Nullable Map arguments
        Queue queue = new Queue("order.delay.queue", true, false, false, arguments);
        return queue;
    }


    @Bean
    public Queue orderReleaseOrderQueue(){
        Queue queue = new Queue("order.release.order.queue", true, false, false);
        return queue;
    }

    @Bean
    public Exchange orderEventExchange(){
        // String name, boolean durable, boolean autoDelete, Map arguments
        TopicExchange exchange = new TopicExchange("order-event-exchange", true, false);
        return exchange;
    }

    @Bean
    public Binding orderCreateOrderBinding(){
        // String destination, DestinationType destinationType, String exchange, String routingKey,
        //			@Nullable Map arguments
        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);
    }
}

监听关单事件

模拟订单完成 

谷粒商城-订单服务_第9张图片

商城业务-订单服务-创建业务交换机&队列

解锁库存的实现:

①库存服务导入RabbitMQ的依赖

  
        
            org.springframework.boot
            spring-boot-starter-amqp
        

② RabbitMQ的配置

spring:
  rabbitmq:
    host: 192.168.56.22
    virtual-host: /

 配置RabbitMQ的序列化机制

import org.springframework.amqp.support.converter.AbstractMessageConverter;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyRabbitMQConfig {
    
    @Bean
    public AbstractMessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

④ 开启RabbitMQ

谷粒商城-订单服务_第10张图片

⑤ 按照下图创建交换机、队列、绑定关系

谷粒商城-订单服务_第11张图片

import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.converter.AbstractMessageConverter;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

@Configuration
public class MyRabbitMQConfig {

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

    }


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


    @Bean
    public Exchange stockEventExchange(){
        // String name, boolean durable, boolean autoDelete, Map arguments
        return new TopicExchange("stock-event-exchange",true,false);
    }

    @Bean
    public Queue stockReleaseStockQueue(){
        // String name, boolean durable, boolean exclusive, boolean autoDelete,@Nullable Map arguments
        return new Queue("stock.release.stock.queue",true,false,false,null);
    }

    @Bean
    public Queue stockDelayQueue(){
        Map 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);
        // String name, boolean durable, boolean exclusive, boolean autoDelete,@Nullable Map arguments
        return new Queue("stock.delay.queue",true,false,false,arguments);
    }

    @Bean
    public Binding stockReleaseBinding(){
        // String destination, DestinationType destinationType, String exchange, String routingKey,@Nullable Map arguments
        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);
    }
}

出现问题: 并没创建交换机、队列、绑定关系

出现问题的原因:只有当第一次连接上RabbitMQ时,发现没有这些东西才会创建

解决方案:监听队列

谷粒商城-订单服务_第12张图片

交换机、队列、绑定关系创建成功后,将上述代码注释

商城业务-订单服务-监听库存解锁

库存解锁的两种场景:

①下单成功,订单过期没有支付被系统自动取消、被用户手动取消。都要解锁

②下单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁

 加上全参和无参构造器注解

谷粒商城-订单服务_第13张图片

② 保存工作单详情方便回溯

谷粒商城-订单服务_第14张图片③ Common服务中创建To,方便MQ发送消息

谷粒商城-订单服务_第15张图片

如果To仅仅保存这个两个数据的话,会存在一些问题, 当1号订单在1号仓库扣减1件商品成功,2号订单在2号仓库扣减2件商品成功,3号订单在3号仓库扣减3件商品失败时,库存工作单的数据将会回滚,此时,数据库中将查不到1号和2号订单的库存工作单的数据,但是库存扣减是成功的,导致无法解锁库存

解决方案: 保存库存工作详情To

谷粒商城-订单服务_第16张图片

谷粒商城-订单服务_第17张图片④ 向MQ发送库存锁定成功的消息

 

谷粒商城-订单服务_第18张图片

商城业务-订单服务-库存解锁逻辑&库存自动解锁完成&测试库存自动解锁

解锁场景:

1.下单成功,库存锁定成功,接下来的业务调用失败导致订单回滚。之前锁定的库存就要自动解锁。

2.锁库存失败无需解锁

解决方案:通过查询订单的锁库存信息,如果有则仅仅说明库存锁定成功,还需判断是否有订单信息,如果有订单信息则判断订单状态,若订单状态已取消则解锁库存,反之:不能解锁库存,如果没有订单信息则需要解锁库存,如果没有锁库存信息则无需任何操作。

1.编写Vo,通过拷贝订单实体,用于接收订单信息

谷粒商城-订单服务_第19张图片

2. 远程服务编写,获取订单状态

谷粒商城-订单服务_第20张图片

谷粒商城-订单服务_第21张图片 3.监听事件

谷粒商城-订单服务_第22张图片

谷粒商城-订单服务_第23张图片

 /**
     * 解锁库存服务
     * @param stockLockedTo
     */
    @Override
    public void unlockStock(StockLockedTo stockLockedTo){
        StockDetailTo detail = stockLockedTo.getDetail();
        Long detailId = detail.getId();
        // 查询库存工作单的信息 有:解锁库存 没有:库存锁定失败,数据自定义回滚无需解锁
        WareOrderTaskDetailEntity detailEntity = wareOrderTaskDetailService.getById(detailId);
        if (null!=detailEntity){
            // 有,解锁库存
            Long id = stockLockedTo.getId(); // 库存工作单的id
            WareOrderTaskEntity wareOrderTaskEntity = wareOrderTaskService.getById(id);
            String orderSn = wareOrderTaskEntity.getOrderSn();
            // 远程服务调用,获取订单状态信息
            // 先判断订单是否存在
            R r = orderFeignService.getOrderStatus(orderSn);
            if (r.getCode().equals(0)){
                OrderVo orderVo = r.getData(new TypeReference() {});
                if (null==orderVo || orderVo.getStatus().equals(4)){
                    // 订单不存在或者订单被关闭,都需要去解锁库存
                    // 当且仅当 锁定状态为 已锁定 时 才去解锁
                    if (detailEntity.getLockStatus().equals(1)){
                        releaseStock(detailEntity.getSkuId(),detailEntity.getWareId(),detail.getSkuNum(),detailEntity.getId());
                    }
                }
            }else {
                // 远程服务调用失败,抛出异常 将消息放回消息队列中
                throw new RuntimeException("远程调用订单服务失败!!!");
            }
        }else {
            // 订单的库存详情信息不存在,无需解锁
        }
    }

    private void releaseStock(Long skuId, Long wareId, Integer skuNum,Long taskDetailId) {
        // 解锁库存
        wareSkuDao.unLockStock(skuId,wareId,skuNum);
        // 更新库存工作单状态
        WareOrderTaskDetailEntity wareOrderTaskDetailEntity = new WareOrderTaskDetailEntity();
        wareOrderTaskDetailEntity.setId(taskDetailId);
        wareOrderTaskDetailEntity.setLockStatus(2);
        wareOrderTaskDetailService.updateById(wareOrderTaskDetailEntity);
    }

4. 远程服务调用可能会出现失败,需要设置手动ACK,确保其它服务能消费此消息

#手动ACK设置
spring.rabbitmq.listener.simple.acknowledge-mode=manual

谷粒商城-订单服务_第24张图片

出现问题: 远程调用订单服务时被拦截器拦截

解决方案:请求路径适配放行

谷粒商城-订单服务_第25张图片

商城业务-订单服务-定时关单完成

1.定时关单代码编写

①订单创建成功,给MQ发送关单消息

谷粒商城-订单服务_第26张图片② 监听事件,进行关单

谷粒商城-订单服务_第27张图片

谷粒商城-订单服务_第28张图片谷粒商城-订单服务_第29张图片  

订单释放和库存解锁逻辑: 当订单创建成功之后,向MQ发送关单消息,过期时间为1分钟,向MQ发送解锁库存消息,过期时间为2分钟,关单操作完成之后,过了1分钟解锁库存操作。

存在问题:由于机器卡顿、消息延迟等导致关单消息未延迟发送,解锁库存消息正常发送和监听,导致解锁库存消息被消费,当执行完关单操作后便无法再执行解锁库存操作,导致卡顿的订单永远无法解锁库存。

解决方案:采取主动补偿的策略。当关单操作正常完成之后,主动去发送解锁库存消息给MQ,监听解锁库存消息进行解锁。

谷粒商城-订单服务_第30张图片

③ 按上图创建绑定关系

谷粒商城-订单服务_第31张图片④ common服务中,创建CreateTo(拷贝order实体) 

谷粒商城-订单服务_第32张图片

⑤ 向MQ发送解锁库存消息

谷粒商城-订单服务_第33张图片

⑥ 解锁库存操作

谷粒商城-订单服务_第34张图片谷粒商城-订单服务_第35张图片

商城业务-订单服务-消息丢失、积压、重复等解决方案

谷粒商城-订单服务_第36张图片

情况一: 消息发送出去但是由于网络原因未到达服务器,解决方案:采用try-catch将发送失败的消息持久化到数据库中,采用定期扫描重发的方式。

drop table if exists mq_message;
CREATE TABLE `mq_message` (
	`message_id` CHAR(32) NOT NULL,
	`content` TEXT,
	`to_exchange` VARCHAR(255) DEFAULT NULL,
	`routing_key` VARCHAR(255) DEFAULT NULL,
	`class_type` VARCHAR(255) DEFAULT NULL,
	`message_status` INT(1) DEFAULT '0' COMMENT '0-新建  1-已发送  2-错误抵达  3-已抵达',
	`create_time` DATETIME DEFAULT NULL,
	`update_time` DATETIME DEFAULT NULL,
	PRIMARY KEY (`message_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4

情况二:消息抵达服务器的队列中才算完成消息的持久化,解决方案publish的ack机制

谷粒商城-订单服务_第37张图片情况三: 防止自动ack带来的缺陷,采用手动ack,解决方案上面都有这里不再细说

谷粒商城-订单服务_第38张图片

消息被成功消费,ack时宕机,消息由unack变成ready,Broker又重新发送。解决方案:将消费者的业务消费接口应该设计为幂等性的,比如扣库存有工作单的状态标志。

谷粒商城-订单服务_第39张图片

消息积压即消费者的消费能力不够, 上线更多的消费者进行正常的消费。

你可能感兴趣的:(尚硅谷谷粒商城,rabbitmq,分布式)