Day05_谷粒商城(谷粒商城高级篇四)摘要

文章目录

  • 笔记链接:
  • P247 MQ的简介
  • P248 RabbitMQ的简介
  • P249 RabbitMQ的流程
  • P250 docker安装RabbitMQ
  • P251 绑定一个交换机和队列
  • P252 Direct的手动演示
  • P253 Fanout的手动演示
  • P254 Topic的手动演示
  • P255 SpringBoot整合RabbitMQ
  • P256 AmqpAdmin绑定队列和交换机
  • P257 RabbitTemplate发送消息
  • P258 RabbitListener&RabbitHandler接收消息
      • 两个注解
      • 示例一
      • 关于接收消息的参数
      • @RabbitHandler的使用
      • 小插曲
  • P259 可靠投递-发送端确认
      • 1.配置confirmCallback(服务端收到消息就回调)
      • 2.配置returnCallback(消息抵达队列才回调)
      • 3.关于回调
  • P260 可靠投递-消费端确认
  • P261 订单服务—环境搭建
  • P262 订单服务—整合SpringSession
  • P263 订单服务—订单业务说明
  • P264 订单确认页—登录拦截
  • P265 订单确认页—数据获取1
  • P266 订单确认页—数据获取2
  • P267 订单确认页—Feign远程调用丢失请求头问题
  • P268 订单确认页—Feign异步调用丢失请求头问题
  • P269 订单确认页—细节微调
  • P270 订单确认页—前台页面的展示
  • P271 订单确认页—库存查询
  • P272 订单确认页—模拟运费效果1
  • P273 订单确认页—模拟运费效果2
  • P274 订单确认页—接口幂等性讨论
  • P275 订单确认页—添加防重令牌token
  • P275 提交订单页—完成订单提交1
  • P276 提交订单页—完成订单提交2
  • P277 提交订单页—完成订单提交3
  • P278 提交订单页—完成订单提交4
  • P279 提交订单页—完成订单提交5
  • P280 提交订单页—完成订单提交6
  • P281 提交订单页—完成订单提交7
  • P282 提交订单页—完成订单提交8
      • 1.关于代码与笔记
      • 2.说明
      • 2.本节内容总说
      • 4.`submitOrder()`的解说
  • P283 (概念)本地事务在分布式下的问题
  • P284 (概念)本地事务的事务隔离级别、传播行为复习
      • 1.事务的基本性质(ACID):
      • 2.事务的隔离级别
      • 3.事务的传播行为
      • 3.springboot中本地事务失效问题:
  • P285 (概念)分布式事务—CAP和Raft的概念
  • P286 (概念)分布式事务—BASE
  • P287 (概念)分布式事务—分布式事务常见解决方案
  • P288 分布式事务—Seata使用1
  • P289 分布式事务—Seata使用2
  • P290 可靠消息+最终一致性方案
  • P291 RabbitMQ延时队列的讲解
  • P292 “可靠消息+最终一致性”的实现——添加RabbitMQ1
      • 1.创建gulimall-order里的RabbitMQ
      • 2.测试队列
  • P293 “可靠消息+最终一致性”的实现—添加RabbitMQ2
      • 1.创建gulimall-ware的RabbitMQ
      • 2.环境配置
  • P294 “可靠消息+最终一致性”的实现—锁定库存
      • 1.锁定库存
  • P295 “可靠消息+最终一致性”的实现—库存自动解锁1
  • P296 “可靠消息+最终一致性”的实现—库存自动解锁2
  • P297 “可靠消息+最终一致性”的实现—库存自动解锁3
      • 1.库存自动解锁
      • 2.测试
  • P298 “可靠消息+最终一致性”的实现—定时关单
          • 1)、提交订单
          • 2)、监听队列
          • 3)、定时关单
          • 4)、解锁库存
  • P299 “可靠消息+最终一致性”的实现—保证消息的可靠
      • 1.消息丢失问题:
      • 2.消息重复问题
      • 3.消息积压问题

笔记链接:

谷粒商城-个人笔记(高级篇四)

P247 MQ的简介

p247—p249是概念,不建议回看视频,宁愿你温习一下以往的笔记 RabbitMQ—基础部分 和 RabbitMQ—高级部分

P248 RabbitMQ的简介

P249 RabbitMQ的流程

P250 docker安装RabbitMQ

Day05_谷粒商城(谷粒商城高级篇四)摘要_第1张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第2张图片

P251 绑定一个交换机和队列

Day05_谷粒商城(谷粒商城高级篇四)摘要_第3张图片

P252 Direct的手动演示

p252—p254是在192.168.56.106:15672官网上演示,也就是手动演示,没有涉及到代码

P253 Fanout的手动演示

P254 Topic的手动演示

P255 SpringBoot整合RabbitMQ

讲了SpringBoot整合RabbitMQ的具体过程

Day05_谷粒商城(谷粒商城高级篇四)摘要_第4张图片

P256 AmqpAdmin绑定队列和交换机

p256是用AmqpAdmin绑定队列和交换机。

Day05_谷粒商城(谷粒商城高级篇四)摘要_第5张图片

P257 RabbitTemplate发送消息

之前讲了用AmqpAdmin绑定队列和交换机,p257就是用RabbitTemplate发送消息,
发消息就一行代码rabbitTemplate.convertAndSend(xxx,xxxx,xxx);

Day05_谷粒商城(谷粒商城高级篇四)摘要_第6张图片

P258 RabbitListener&RabbitHandler接收消息

之前讲了用AmqpAdmin绑定队列和交换机,用RabbitTemplate发送消息,那么现在就是用RabbitListener&RabbitHandler来接收消息。

Day05_谷粒商城(谷粒商城高级篇四)摘要_第7张图片

两个注解

监听消息:使用@RabbitListener注解; 主启动类必须有@EnableRabbit

    @RabbitListener: 标在类 或 方法上(监听哪些队列)

    @RabbitHandler: 标在方法上(可以区分队列里的不同的消息)

@RabbitListener(queues = {“queue1”,“queue2”}),可以监听多个队列

示例一

1.编写发送消息

@Slf4j
@SpringBootTest
class GulimallOrderApplicationTests {
     
 
    @Autowired
    AmqpAdmin amqpAdmin;
 
    @Autowired
    RabbitTemplate rabbitTemplate;
 
 
    /**
     *  让某个类发送消息
     */
    @Test
    public void sendMessageTest(){
     
        OrderReturnApplyEntity orderReturnApplyEntity = new OrderReturnApplyEntity();
        orderReturnApplyEntity.setId(1L);
        orderReturnApplyEntity.setCreateTime(new Date());
        orderReturnApplyEntity.setReturnName("哈哈哈");

        rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",orderReturnApplyEntity);
        log.info("消息发送完成{}",orderReturnApplyEntity);
    }
}    

2.编写接收消息

Day05_谷粒商城(谷粒商城高级篇四)摘要_第8张图片

3.测试结果

在这里插入图片描述
在这里插入图片描述

关于接收消息的参数

/**
 * 接收消息的receiverMessage()方法的参数可以写以下内容
 * 1、Message message:这个message包括消息头+消息体
 * 2、OrderReturnApplyEntity content:发送的消息类型 (可选参数)
 * 3、Channel channel 当前传输数据的通道(可选参数)
 */
    @RabbitListener(queues = {
     "hello-java-queue"})
    public void receiverMessage(Message message,OrderReturnApplyEntity content,
                                Channel channel) throws InterruptedException {
     
        
        byte[] body = message.getBody();//从message中获取消息体
        MessageProperties properties = message.getMessageProperties(); //从message中消息头属性信息
        
        System.out.println("接收到消息...内容:" + content);
        System.out.println("消息处理完成=》"+content.getReturnName());
    }

Day05_谷粒商城(谷粒商城高级篇四)摘要_第9张图片

@RabbitHandler的使用

1.编写发送消息
如果是偶数就发送OrderReturnApplyEntity类型的数据,如果是基数就发送OrderEntity类型的数据

@RestController
public class RabbitController {
     
 
    @Autowired
    RabbitTemplate rabbitTemplate;
    
    @GetMapping("/sendMq")
    public String sendMq(@RequestParam(value = "num",defaultValue = "10") Integer num){
     
    
        for (int i = 0; i < num; i++){
     
            if (i%2==0){
     
                OrderReturnApplyEntity orderReturnApplyEntity = new OrderReturnApplyEntity();
                orderReturnApplyEntity.setId(1L);
                orderReturnApplyEntity.setCreateTime(new Date());
                orderReturnApplyEntity.setReturnName("哈哈哈");
                rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",orderReturnApplyEntity, new CorrelationData(UUID.randomUUID().toString()));
            }else {
     
                OrderEntity entity = new OrderEntity();
                entity.setOrderSn(UUID.randomUUID().toString());
                rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",entity, new CorrelationData(UUID.randomUUID().toString()));
            }
        }
        return "OK";
    }
 
}

2.编写接收消息

Day05_谷粒商城(谷粒商城高级篇四)摘要_第10张图片

3.测试

Day05_谷粒商城(谷粒商城高级篇四)摘要_第11张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第12张图片

小插曲

本节内容一开始,老师启动的服务端口被占用

Day05_谷粒商城(谷粒商城高级篇四)摘要_第13张图片
在这里插入图片描述
在这里插入图片描述

P259 可靠投递-发送端确认

Day05_谷粒商城(谷粒商城高级篇四)摘要_第14张图片

Day05_谷粒商城(谷粒商城高级篇四)摘要_第15张图片

1.配置confirmCallback(服务端收到消息就回调)

1.在yml中配置

#开启发送端确认
spring.rabbitmq.publisher-confirms=true

2.添加配置类“com.atguigu.gulimall.order.config.MyRabbitConfig”,代码如下:

@Configuration
public class MyRabbitConfig {
     

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

    @PostConstruct   
    public void initRabbitTemplate(){
     
    
        //设置消息抵达服务器的确认回调
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
     
            /**
             * 只要消息抵达Broker就b = true
             * @param correlationData 当前消息的唯一关联数据(这个消息的唯一id)
             * @param b  消息是否成功收到
             * @param s 失败的原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean b, String s) {
     
                System.out.println("confirm...correlationData["+correlationData+"]==>b["+b+"]s==>["+s+"]");
            }
        });
    }
}

2.配置returnCallback(消息抵达队列才回调)

1.修改application.properties

#开启发送端确认
spring.rabbitmq.publisher-confirms=true
#开启发送端抵达队列确认
spring.rabbitmq.publisher-returns=true
#只要抵达队列,以异步发送优先回调我们这个returnConfirm
spring.rabbitmq.template.mandatory=true

2.修改配置类“com.atguigu.gulimall.order.config.MyRabbitConfig”,代码如下:

@Configuration
public class MyRabbitConfig {
     

    @Autowired
    RabbitTemplate rabbitTemplate;
    
    @Bean
    public MessageConverter messageConverter(){
     
        return new Jackson2JsonMessageConverter();
    }
 
    @PostConstruct   
    public void initRabbitTemplate(){
     
    
        //设置消息抵达服务器的确认回调
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
     
            /**
             * 只要消息抵达Broker就b = true
             * @param correlationData 当前消息的唯一关联数据(这个消息的唯一id)
             * @param b  消息是否成功收到
             * @param s 失败的原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean b, String s) {
     
                System.out.println("confirm...correlationData["+correlationData+"]==>b["+b+"]s==>["+s+"]");
            }
        });
 
        //设置消息抵达队列的确认回调
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
     
            /**
             * 只要消息没有投递给指定的队列,就触发这个失败回调
             * @param message 投递失败的消息详细信息
             * @param i 回复的状态码
             * @param s 回复的文本内容
             * @param s1 当时这个消息发给哪个交换机
             * @param s2 当时这个消息用哪个路由键
             */
            @Override
            public void returnedMessage(Message message, int i, String s, String s1, String s2) {
     
                System.out.println("Fail Message["+message+"]==>i["+i+"]==>s["+s+"]==>s1["+s1+"]==>s2["+s2+"]");
            }
        });
    }
}

3.关于回调

1、服务器收到消息就回调

        1、yml配置文件中:
        	spring.rabbitmq.publisher-confirms=true

        2、配置类中:
           设置确认回调ConfirmCallback

2、消息抵达队列就回调

        1、yml配置文件中:
         spring.rabbitmq.publisher-returns=true
         spring.rabbitmq.template.mandatory=true

       2、配置类中:
       	 设置确认回调ReturnCallback

3、消费端确认(保证每个消息被正确消费,此时才可以保证broker删除这个消息)

P260 可靠投递-消费端确认

Day05_谷粒商城(谷粒商城高级篇四)摘要_第16张图片

1、默认是自动确认的,只要消息接收到,客户端会自动确认,服务端就会移除这个消息

问题:

    我们收到很多消息,自动回复给服务器ack,发送了5条消息即使服务端只处理了一条,此时宕机了,就会发生消息丢失。
    

2、手动确认模式。只要我们没有明确告诉MQ,货物被签收,没有ACK,消息就一直unacked状态,

    即使Consumer宕机。消息不会丢失,会重新变为Ready,下一次有新的Consumer连接进来就发给他

2、消费端的手动确认

      1)、yml配置:

      spring.rabbitmq.listener.simple.acknowledge-mode=manual

     2)、手动确认:
     channel.basicAck(deliveryTag,false);签收;业务成功完成就应该签收

     channel.basicNack(deliveryTag,false,true);拒签:业务失败,拒签

1.添加application.properties

#手动确认收货(ack)
spring.rabbitmq.listener.simple.acknowledge-mode=manual

2.编写接收消息

@RabbitListener(queues = {
     "hello-java-queue"})
public class ListenerTest {
     

    @RabbitHandler
    public void receiverMessage(Message message,OrderReturnApplyEntity content,
                                Channel channel) throws InterruptedException {
     

        //channel内按顺序自增的
        long deliveryTag = message.getMessageProperties().getDeliveryTag();

        //签收货物,非批量模式
        try{
     
            if (deliveryTag % 2 == 0){
     
                //收货
                channel.basicAck(deliveryTag,false);
                System.out.println("签收了货物。。。"+deliveryTag);
            }else {
     
                //退货requeue=false 丢弃  requeue=true发挥服务器,服务器重新入队。
                channel.basicNack(deliveryTag,false,true);
                System.out.println("没有签收货物..."+deliveryTag);
            }
 
        }catch (Exception e){
     
            //网络中断
        }
 
    }

P261 订单服务—环境搭建

Day05_谷粒商城(谷粒商城高级篇四)摘要_第17张图片

P262 订单服务—整合SpringSession

要支付,先登录,登录要用SpringSession

Day05_谷粒商城(谷粒商城高级篇四)摘要_第18张图片

P263 订单服务—订单业务说明

Day05_谷粒商城(谷粒商城高级篇四)摘要_第19张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第20张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第21张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第22张图片

P264 订单确认页—登录拦截

注意我们的登陆拦截器还有返回值的,返回了用户信息的,下面要用到。

Day05_谷粒商城(谷粒商城高级篇四)摘要_第23张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第24张图片

P265 订单确认页—数据获取1

P266 订单确认页—数据获取2

在这里插入图片描述

Day05_谷粒商城(谷粒商城高级篇四)摘要_第25张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第26张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第27张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第28张图片

在这里插入图片描述
Day05_谷粒商城(谷粒商城高级篇四)摘要_第29张图片

总说:
前端发来请求访问http://order.gulimall.com/toTrade,后台要带着必要的信息重定向到订单确认页。需要哪些信息呢?

  • 收货人地址信息 List address,需要使用OpenFeign远程查询gulimall-member,gulimall-member查询数据库ums_member_receive_address表即可
  • 所有选中的购物项List items,需要使用OpenFeign远程查询gulimall-cart,在gulimall-cart里面当初我们把登录信息存到session中、把购物车信息存到redis中,所以根据登录拦截器返回的用户信息,只需要拿着用户信息查询redis中该用户的购物车中选中去结账的商品,然后给这些商品结账时还要查询这些商品最新的价格(你添加商品到购物车一个月现在才结账,谁知道商品价格变了没),查询商品价格需要使用OpenFeign远程查询gulimall-product的pms_sku_info表即可。
  • 积分信息 Integer integration;,我们登录拦截器返回来的用户信息里面就包含用户积分信息
  • 订单总额BigDecimal total;,(商品数量x价格)=订单总额
  • 应付价格BigDecimal payPrice; ,先不说
  • 防重令牌String orderToken;,你提交订单页面如果刷新多次就会造成提交多次订单,所以需要防重令牌。

P267 订单确认页—Feign远程调用丢失请求头问题

在这里插入图片描述

问题
p265的代码和思路没有问题,但是测试的时候发现gulimall-order使用OpenFeign远程查询gulimall-cart的购物车信息时,经过gulimall-cart的登录拦截器时居然是没有登陆状态。你明明已经在gulimall-order中登录了,为什么会被远程调用的gulimall-cart的登录拦截器拦截下来?

解释
因为浏览器先发送http://order.gulimall.com/toTrade给gulimall-order时携带了请求头(有请求头就有cookie,有cookie就能查到session),但是使用OpenFeign远程查询gulimall-cart的购物车信息时会新创建一个请求头,丢弃原来的请求头。

解决办法
使用OpenFeign远程查询gulimall-cart的购物车信息时会新创建一个请求头,我们写一个feign的拦截器,在拦截器里面为请求加上cookie。

P268 订单确认页—Feign异步调用丢失请求头问题

Day05_谷粒商城(谷粒商城高级篇四)摘要_第30张图片

1.原来的代码

    @Override
    public OrderConfirmVo confirmOrder() {
     
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        MemberResponseVO memberResponseVO = LoginUserInterceptor.loginUser.get();
 
        //1、远程查询所有的收货地址列表
        List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVO.getId());
        confirmVo.setAddress(address);
 
        //2、远程查询购物车所有选中的购物项
        List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
        confirmVo.setItems(items);
 
        //3、查询用户积分
        Integer integration = memberResponseVO.getIntegration();
        confirmVo.setIntegration(integration);
 
        //4、其他数据自动计算
        return confirmVo;
    }

2.把原来的代码改为异步的方式

    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
     
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        MemberResponseVO memberResponseVO = LoginUserInterceptor.loginUser.get();

        //异步任务编排
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
     
            //1、远程查询所有的收货地址列表
            List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVO.getId());
            confirmVo.setAddress(address);
        }, executor);
 
        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
     
            //2、远程查询购物车所有选中的购物项
            List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(items);
        }, executor);
 
        //3、查询用户积分
        Integer integration = memberResponseVO.getIntegration();
        confirmVo.setIntegration(integration);
 
        CompletableFuture.allOf(getAddressFuture,cartFuture).get();
        
        return confirmVo;
    }

3.存在的问题

Day05_谷粒商城(谷粒商城高级篇四)摘要_第31张图片

4.解决

    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
     
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        MemberResponseVO memberResponseVO = LoginUserInterceptor.loginUser.get();

        //获取之前的请求
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        

        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
     

            RequestContextHolder.setRequestAttributes(requestAttributes);//每一个线程都来共享之前的请求数据
            
            List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVO.getId());
            confirmVo.setAddress(address);
        }, executor);
 
        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
     
            
            RequestContextHolder.setRequestAttributes(requestAttributes);//每一个线程都来共享之前的请求数据
            
            List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(items);
        }, executor);
 
        Integer integration = memberResponseVO.getIntegration();
        confirmVo.setIntegration(integration);
 
        CompletableFuture.allOf(getAddressFuture,cartFuture).get();
        
        return confirmVo;
    }

P269 订单确认页—细节微调

这一节仅仅4分钟,解决了上节课留下的报错(远程调用要用R对象封装),笔记上没有

P270 订单确认页—前台页面的展示

Day05_谷粒商城(谷粒商城高级篇四)摘要_第32张图片

P271 订单确认页—库存查询

Day05_谷粒商城(谷粒商城高级篇四)摘要_第33张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第34张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第35张图片
稍微修改gulimall-order的“OrderServiceImpl”类里面的confirmOrder()方法
Day05_谷粒商城(谷粒商城高级篇四)摘要_第36张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第37张图片

P272 订单确认页—模拟运费效果1

P273 订单确认页—模拟运费效果2

Day05_谷粒商城(谷粒商城高级篇四)摘要_第38张图片

总说:
前台给gulimall-ware发过来/fare请求(http://gulimall.com/api/ware/wareinfo/fare),携带的参数是addrId(收货地址id),后台使用OpenFeign远程调用gulimall-member根据addrId(收货地址id)查询到addrInfo(收货地址信息)封装到MemberAddressVo里面返回,然后我们根据MemberAddressVo里面的电话的最后一位模拟计算运费

Day05_谷粒商城(谷粒商城高级篇四)摘要_第39张图片

P274 订单确认页—接口幂等性讨论

学习视频:接口幂等性讨论


何为幂等性?同一份订单提交一次和提交100次结果是一样的,用户不能因为网络不好提交了多词就下单多次。

1.什么是幂等性
接口幂等性就是用户对同一操作发起的一次请求和多次请求结果是一致的,不会因为多次点击而产生了副作用。比如支付场景,用户购买了商品,支付扣款成功,但是返回结果的时候出现了网络异常,此时钱已经扣了,用户再次点击按钮,此时就会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。。。这就没有保证接口幂等性

2.哪些情况需要考虑幂等性?

  • 用户多次点击按钮
  • 用户页面回退再次提交
  • 微服务互相调用,由于网络问题,导致请求失败,feign触发重试机制
  • 其他业务情况

3.什么情况不用考虑幂等性?

Day05_谷粒商城(谷粒商城高级篇四)摘要_第40张图片

4.幂等性的解决方法

Day05_谷粒商城(谷粒商城高级篇四)摘要_第41张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第42张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第43张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第44张图片

P275 订单确认页—添加防重令牌token

每访问http://order.gulimall.com/toTrade后台就会随机生成一个uuid(模拟token),然后存到redis中,也存到OrderConfirmVo中交给前端页面,在这个订单确认页面用户填好收货地址、支付方式以后,点击提交订单就把token带过去,后台比较前端带来的token和redis中的是否一致。假如用户点击多次“提交订单”,他每次带来的token都一样(因为页面并没有刷新),只有第一次能和redis匹配成功,其他的几次就是失败的,就能保证幂等性。

令牌机制的危险性:
前端发来“提交订单”的请求,我们要先根据用户id从redis中拿到token,然后比对前端的token和redis中的token,然后删掉redis中的令牌。
用户手速很快连点几次“提交订单”,第一次请求来了,后台根据用户id从redis中拿到token,和前台传进来的token进行比对,比对完了,还没有来得及删掉令牌,结果第二次带着token进来了又和redis匹配成功了,此时即使你删掉令牌也已经有两个业务进来了。
所以:从redis中拿token、比对token、删除token,一定要是原子性操做。

Day05_谷粒商城(谷粒商城高级篇四)摘要_第45张图片

P275 提交订单页—完成订单提交1

给订单确认页添加防重令牌token,然后建立提交订单页的初步逻辑

P276 提交订单页—完成订单提交2

原子验证令牌

P277 提交订单页—完成订单提交3

讲了builderOrder()方法

P278 提交订单页—完成订单提交4

讲了builderOrderItem()方法

P279 提交订单页—完成订单提交5

讲了createOrder() 和 computePrice()方法

P280 提交订单页—完成订单提交6

saveOrder()方法、锁库存

P281 提交订单页—完成订单提交7

锁库存

P282 提交订单页—完成订单提交8

1.关于代码与笔记

这一节内容你看IDEA中的代码可以;看笔记也可以,一定要注意人家笔记里面的序号,我觉得非常地条理

Day05_谷粒商城(谷粒商城高级篇四)摘要_第46张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第47张图片

2.说明

购物车页选择要结算的商品,然后来到了订单确认页
订单确认页选择收货人地址信息、支付方式、显示商品、显示运费等等,然后点击“提交订单”后,就携带着数据来到了提交订单页
提交订单页就是要锁定库存进行提交订单了,确认提交订单就会来到支付页
支付页进行支付。

2.本节内容总说

Day05_谷粒商城(谷粒商城高级篇四)摘要_第48张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第49张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第50张图片

Day05_谷粒商城(谷粒商城高级篇四)摘要_第51张图片

4.submitOrder()的解说

最复杂的莫过于OrderServiceImpl里的submitOrder()方法,因为它调用了八大方法

public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo)干了什么:
第一步,比较前台传过来的令牌和redis中的令牌是否一致,如果一致就继续往下执行
第二步,创建订单(创建订单时会再次从购物车里面查询用户下单的商品再次计算总价),和前端传来的价格进行比对,如果一致就继续往下执行,用到了下面的①②③④⑤个方法
第三步,保存订单,用到了下面的方法⑥
第四步,锁定库存,用到了下面的方法⑦⑧


submitOrder()方法中调用的八大方法

private OrderEntity builderOrder(String orderSn)给我orderSn,返回OrderEntity。而OrderEntity里面有相当多的属性:

  • 订单号(参数里有)
  • 会员id会员名(从LoginUserInterceptor中获取到)
  • 运费 还有 收货人姓名、电话、省份、地区、街道等等,这些属性FareVo里面就有,如何得到FareVo?
    从confirmVoThreadLocal中获取到提交上来的订单信息(里面包含了addr_id),调用gulimall-ware根据addr_id远程查询就会返回FareVo
  • 订单状态、确认时长、确认状态(这些属性无需查询,现场设置)

builderOrder()方法很明显,就你一个订单号,你要把用户信息、地址信息、订单状态信息全部生成到OrderEntity里返回


private OrderItemEntity builderOrderItem(OrderItemVo items)传来OrderItemVo返回OrderItemEntity,参数OrderItemVo就是购物车里面的购物项,OrderItemEntity里面有超多属性:

  • spu信息:根据OrderItemVo里的skuid就可以调用远程查询productFeignService.getSpuInfoBySkuId(skuId)得到spu信息(根据skuid查询pms_sku_info得到spuid,然后根据spuid查询pms_spu_info得到spu_info)
  • sku信息:OrderItemVo就有
  • 积分信息:OrderItemVo就有
  • 优惠价格:PromotionAmount、CouponAmount、IntegrationAmount设置为0就行了
  • 实际价格:总额 - 各种优惠价格

builderOrderItem()这个方法很明显就是给你一个具体商品,你后台查询它的信息还要计算它的价格,封装到OrderItemEntity里返回


public List builderOrderItems(String orderSn),传来订单号,返回List

  • 首先它会cartFeignService.getCurrentCartItems()远程获取当前用户的购物车中选中要结账的那些商品;
  • 然后把每个商品经过builderOrderItem(OrderItemVo item)处理(builderOrderItem()这个方法就是给你一个具体商品,你后台查询它的信息还要计算它的价格,封装到OrderItemEntity里返回)

builderOrderItems()这个方法很明显,就是给你订单号,然后你压根不用这个订单号,你会另辟蹊径拿到当前用户购物车里面选中要结账的商品以及它们的价格信息,封装到List< OrderItemEntity>返回,为什么这么做?因为你想要比对价格,你要再从购物车里面拿一次商品和价格来比对


private void computePrice(OrderEntity orderEntity, List orderItemEntities)
它会遍历List里面的每一个OrderItemEntity,把每一个商品涉及到的优惠价(PromotionAmount、CouponAmount、IntegrationAmount)叠加,把每一个商品涉及到的(积分、成长值)叠加,叠加后的值就是这个订单的总优惠价、总积分、总成长值
然后把总优惠价、总积分、总成长值、总价格、应付价格设置到OrderEntity里面


private OrderCreateTo createOrder(),没有参数,返回OrderCreateTo,具体流程 :
会先调用builderOrder(orderSn)方法生成订单信息OrderEntity
然后调用builderOrderItems(orderSn)拿到所有结账商品的信息List
然后计算价格computePrice(OrderEntity,List)
然后把OrderEntityList封装到OrderCreateTo里面返回


private void saveOrder(OrderCreateTo orderCreateTo)传进来OrderCreateTo,无返回值:
OrderCreateTo里面有OrderEntityList
OrderEntity里面的信息存到oms_order表中
OrderItemEntity里面的信息存到oms_order_item表中

Day05_谷粒商城(谷粒商城高级篇四)摘要_第52张图片


⑦WareSkuLockVo是锁定库存的vo,里面的属性可以从哪里获取呢?
OrderCreateTo里面有OrderEntityList
WareSkuLockVo里的订单号orderSn可以从OrderEntity里面获取
WareSkuLockVo里的OrderItemVo中的(skuid、title、count)可以从OrderItemEntity里面获取

Day05_谷粒商城(谷粒商城高级篇四)摘要_第53张图片

public boolean orderLockStock(WareSkuLockVo vo) 传过来WareSkuLockVo,返回boolean表示锁定库存成功了没有:
第一步,根据WareSkuLockVo里封装的OrderItemVo中的skuid,执行listWareIdHasSkuStock(skuId),查询仓库里有这件商品而且这件商品库存数量大于0的那些仓库,把这些仓库id封装到List里面
第二步,把(List)还有(skuid)还有(用户要购买的数量num)封装到SkuWareHasStock实体类里。(SkuWareHasStock实体类里面只有三个值:skuId、num、wareId,表示在某个仓库里面要锁定某个商品多少个)
第三步,遍历List,执行lockSkuStock(skuId,wareId,num),之前得到了仓库里有这件商品而且库存大于0的那些仓库,但是库存大于0假如库存是1,而用户买10件,你还是不行嘛,lockSkuStock(skuId,wareId,num)就是找到有库存而且商品数量足够支撑用户购买的数量;一旦找到一个符合条件的仓库就置为true,没有就继续遍历其它仓库。
注意,锁库存里面库存没锁住就抛异常,因为我们加了@Transactional事务注解,所以一旦抛了异常就立刻回滚,整个代码跟没执行一样。

Day05_谷粒商城(谷粒商城高级篇四)摘要_第54张图片

P283 (概念)本地事务在分布式下的问题

p283—p299不看别人的笔记,只看自己的

p275—p282只是“提交订单页”的初步代码,因为该服务调用了很多其他微服务,其中一个服务失败会自己回滚事务但如何保证其它服务也回滚事务呢?
p283—p289是使用分布式事务解决这个问题,一开始引入了seata,但发现seata可以解决事务但解决不了高并发的难题,
所以p290—p299就是使用“可靠消息+最终一致性方案”来解决这个问题(RabbitMQ+事务来解决)。

public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo)干了什么:
第一步,比较前台传过来的令牌和redis中的令牌是否一致,如果一致就继续往下执行
第二步,创建订单(创建订单时会再次从购物车里面查询用户下单的商品再次计算总价),和前端传来的价格进行比对,如果一致就继续往下执行
第三步,保存订单
第四步,锁定库存,远程调用gulimall-ware锁库存,锁库存是在gulimall-ware里面执行的,但它会返回状态码,根据状态码就可以找到远程锁库存成功了没有
第五步,远程扣减积分(这个功能还没有实现,但我们假装它已经实现了)

存在的分布式事务问题:

  • 1.假失败问题。远程调用gulimall-ware锁库存,本来那边锁库存成功了,但由于网络问题迟迟没有响应回来,我们这边以为它失败了所以就回滚事务,这边回滚了,gulimall-ware那边都已经写到数据库里面了怎么回滚?
  • 2.远程服务执行完成下面的其它方法出现问题。远程调用gulimall-ware锁库存,OK很顺利成功了;远程调用gulimall-coupon扣减积分,失败了,gulimall-ware那边是已经执行成功的远程请求肯定不能回滚。还是做不到全方位回滚。

我们用到的@Transactional都是本地事务,本地事务只能控制住自己的回滚,控制不了其它服务的回滚,在分布式事务下不使用。

P284 (概念)本地事务的事务隔离级别、传播行为复习

概念的讲解,讲了事务的基本性质事务的隔离级别事务的传播行为在springboot中本地事务时效问题

1.事务的基本性质(ACID):

原子性:下订单、减库存、扣积分这三个要么同时成功要么同时失败,这就是原子性。原子性就是一系列操做不可拆分,同成同败。
一致性:A给B转200,转完帐必须保证A和B的总额没有变,不能一方扣减另一方没有增加
隔离性:事物之间互相隔离,100个人同时下单,一个人下单失败,不能影响其他人。
持久性:一旦事务提交,那么它对数据库中的对应数据的状态的变更就会永久保存到数据库中。–即使发生系统崩溃或机器宕机等故障,只要数据库能够重新启动,那么一定能够将其恢复到事务成功结束的状态

事务的四大特征:
	1. 原子性(atomicity):一个事务要么全部提交成功,要么全部失败回滚,不能只执行其中的一部分操作,这就是事务的原子性。
	2. 一致性(consistency):事务操作前后,数据总量不变。如果数据库系统在运行过程中发生故障,有些事务尚未完成就被迫中断,这些未完成的事务对数据库所作的修改有一部分已写入物理数据库,这是数据库就处于一种不正确的状态,也就是不一致的状态
	3. 隔离性(isolation):多个事务之间相互独立。对应着事务的四种隔离级别
	4. 持久性(durability):一旦事务提交,那么它对数据库中的对应数据的状态的变更就会永久保存到数据库中。

2.事务的隔离级别

 * 概念:多个事务之间隔离的,相互独立的。但是如果多个事务操作同一批数据,则会引发一些问题,设置不同的隔离级别就可以解决这些问题。
 * 存在问题:
	1. 脏读:一个事务,读取到另一个事务中没有提交的数据
	2. 不可重复读(虚读):在同一个事务中,两次读取到的数据不一样。
	3. 幻读:一个事务操作(DML)数据表中所有记录,另一个事务添加了一条数据,则第一个事务查询不到自己的修改。
 * 隔离级别:
	1. read uncommitted:读未提交
		* 产生的问题:脏读、不可重复读、幻读
	2. read committed:读已提交 (Oracle* 产生的问题:不可重复读、幻读
	3. repeatable read:可重复读 (MySQL默认)
		* 产生的问题:幻读
	4. serializable:串行化
		* 可以解决所有的问题

	* 注意:隔离级别从小到大安全性越来越高,但是效率越来越低
  • 当我们事务的隔离级别是read uncommitted(读未提交)时,说明我们这个事务里面可以读到别人还未提交的数据,会出现脏读(事务回滚,你读到的都是假的)、不可重复读(别人正在改你正在读,下一次读到的肯定不是上一次读到的值)、幻读。
  • 当我们事务的隔离级别是read committed(读已提交)时,说明我们这个事务里面可以读到别人已经提交的数据,会出现不可重复读和幻读。
  • repeatable read:可重复读。意思就是在整个事务期间内,第一次读到100,只要事务没有结束读到的都是100,即使数据可能已经被修改为200了,但你读到的还是100。会出现幻读。
  • serializable:串行化。一个事务做完才能做下一个,导致事务没有并发能力了。但是好处就是没有任何问题。

3.事务的传播行为

① a、b、c三个方法都标注了@Transactional说明它们三个都是事务,b事务隔离级别是REQUIRED那么它共用a的事务,c事务隔离级别是REQUIRES_NEW那么它自己用自己的事务,现在a方法调用b、c方法且执行时出现除0异常,那么a和b会回滚,c不会回滚。

@Transactional
public void a(){
     
	b();
	c();
	int i = 10/0;
}

@Transactional(propagation=Propagation.REQUIRED)
public void b(){
     

}

@Transactional(propagation=Propagation.REQUIRES_NEW)
public void c(){
     

}

② a事务设置过期时间是30s,b事务设置过期时间是3s,由于b共用a的事务,所以b的实际过期时间也是30s

3.springboot中本地事务失效问题:

springboot中使用事务存在的坑:
查看 视频 的10:42以后

P285 (概念)分布式事务—CAP和Raft的概念

讲了CAP的概念Raft的领导选举、日志复制

概念这种东西,看视频不比看文字舒服?不必看文字理解的更深入?,所以回看视频即可

P286 (概念)分布式事务—BASE

强一致弱一致最终一致的概念

P287 (概念)分布式事务—分布式事务常见解决方案

2PC模式
TCC事务补偿方案
最大努力通知型方案(重点)
可靠消息+最终一致性方案(重点)

P288 分布式事务—Seata使用1

P289 分布式事务—Seata使用2

p283—p299不看别人的笔记,只看自己的

这里是引用

1.相关概念的解释

Day05_谷粒商城(谷粒商城高级篇四)摘要_第55张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第56张图片

2.环境准备

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

Day05_谷粒商城(谷粒商城高级篇四)摘要_第57张图片

Day05_谷粒商城(谷粒商城高级篇四)摘要_第58张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第59张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第60张图片

添加“com.atguigu.gulimall.order.config.MySeataConfig”类,代码如下:

@Configuration
public class MySeataConfig {
     
    @Autowired
    DataSourceProperties dataSourceProperties;
 
    @Bean
    public DataSource dataSource(DataSourceProperties dataSourceProperties){
     
        //得到数据源
        HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        if (StringUtils.hasText(dataSourceProperties.getName())){
     
            dataSource.setPoolName(dataSourceProperties.getName());
        }
        return new DataSourceProxy(dataSource);
    }
}

修改“com.atguigu.gulimall.ware.config.WareMybatisConfig”类,代码如下:

    @Bean
    public DataSource dataSource(DataSourceProperties dataSourceProperties){
     
        //得到数据源
        HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        if (StringUtils.hasText(dataSourceProperties.getName())){
     
            dataSource.setPoolName(dataSourceProperties.getName());
        }
        return new DataSourceProxy(dataSource);
    }

分别给gulimall-order和gulimall-ware加上file.conf和registry.conf这两个配置,并修改file.conf

Day05_谷粒商城(谷粒商城高级篇四)摘要_第61张图片

给分布式大事务的路口标注@GlobalTransactional; 每一个远程的小事务用 @Transactional

Day05_谷粒商城(谷粒商城高级篇四)摘要_第62张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第63张图片

OK了,这样下面的这两个分布式事务问题就解决了。

  • 1.假失败问题。远程调用gulimall-ware锁库存,本来那边锁库存成功了,但由于网络问题迟迟没有响应回来,我们这边以为它失败了所以就回滚事务,这边回滚了,gulimall-ware那边都已经写到数据库里面了怎么回滚?
  • 2.远程服务执行完成下面的其它方法出现问题。远程调用gulimall-ware锁库存,OK很顺利成功了;远程调用gulimall-coupon扣减积分,失败了,gulimall-ware那边是已经执行成功的远程请求肯定不能回滚。还是做不到全方位回滚。

但是,我们这个场景还是不适合使用seata,我们下单是典型的高并发模式,seata中后台使用了很多锁,导致高并发串行化(一个一个执行),高并发不使用2PCTCC模式,我们使用的就是最大努力通知型方案可靠消息+最终一致性方案,接下来我们使用了可靠消息+最终一致性方案

P290 可靠消息+最终一致性方案

p275—p282只是“提交订单页”的初步代码,因为该服务调用了很多其他微服务,其中一个服务失败会自己回滚事务但如何保证其它服务也回滚事务呢?
p283—p289是使用分布式事务解决这个问题,一开始引入了seata,但发现seata可以解决事务但解决不了高并发的难题,
所以p290—p299就是使用“可靠消息+最终一致性方案”来解决这个问题(RabbitMQ+事务来解决)。

p290就是比对了2PCTCC模式,还有最大努力通知型方案可靠消息+最终一致性方案,最后得出我们这个“订单确认页”要使用可靠消息+最终一致性方案,所以就开启了下一节RabbitMQ的讲解

P291 RabbitMQ延时队列的讲解

这一节是TTL、死信队列、延时队列的概念讲解,建议回看RabbitMQ—高级部分

P292 “可靠消息+最终一致性”的实现——添加RabbitMQ1

p283—p299不看别人的笔记,只看自己的

本节课给gulimall-order服务添加RabbitMQ

1.创建gulimall-order里的RabbitMQ

Day05_谷粒商城(谷粒商城高级篇四)摘要_第64张图片
p是订单服务,一下单成功就给RabbitMQ发消息,经过延时队列60000ms以后到达订单的释放服务

根据上幅图写出来下面的代码


@Configuration
public class MyRabbitmqConfig {
     
    @Bean
    public Exchange orderEventExchange() {
     

        return new TopicExchange("order-event-exchange", // name
                                 true,// durable
                                 false); // autoDelete  
        						 // 还有一个参数是Map arguments
    }

    /**
     * 延迟队列
     */
    @Bean
    public Queue orderDelayQueue() {
     
        HashMap<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); // 消息过期时间 1分钟
        return new Queue("order.delay.queue", // 队列名字
                         true, //是否持久化
                         false, // 是否排他
                         false, // 是否自动删除
                         arguments); // 属性map
    }

    /**
     * 普通队列
     */
    @Bean
    public Queue orderReleaseQueue() {
     

        Queue queue = new Queue("order.release.order.queue", true, false, false);
        return queue;
    }

    /**
     * 创建订单的binding
     */
    @Bean
    public Binding orderCreateBinding() {
     

        return new Binding("order.delay.queue", // 目的地(队列名或者交换机名字)
                           Binding.DestinationType.QUEUE,  // 目的地类型(Queue、Exhcange)
                           "order-event-exchange", // 交换器
                           "order.create.order", // 路由key
                           null); // 参数map
    }

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

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

2.测试队列

导入依赖

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

yml配置:

# RabbitMQ配置
spring.rabbitmq.host=192.168.56.106
spring.rabbitmq.port=5672
# 虚拟主机配置
spring.rabbitmq.virtual-host=/
# 手动ack消息,不使用默认的消费端确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual

主启动类添加注解

@EnableRabbit
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallWareApplication {
     
 
    public static void main(String[] args) {
     
        SpringApplication.run(GulimallWareApplication.class, args);
    }
 
}

写一个监听方:

@RabbitListener(queues="order.release.order.queue")
public void Listener(OrderEnitity entity,Channel channel,Message message){
     
	System.out.println("收到过期的订单信息,准备关闭订单:"+entity.getOrderSn());
	channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);//手动确认
}

写一个Controller:

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

结果:

Day05_谷粒商城(谷粒商城高级篇四)摘要_第65张图片

在这里插入图片描述
Day05_谷粒商城(谷粒商城高级篇四)摘要_第66张图片

P293 “可靠消息+最终一致性”的实现—添加RabbitMQ2

本节课给gulimall-ware服务添加RabbitMQ

1.创建gulimall-ware的RabbitMQ

Day05_谷粒商城(谷粒商城高级篇四)摘要_第67张图片
图片看不懂就回看视频,视频里面讲的相当清除。

根据上幅图写出了下面的代码:


@Configuration
public class MyRabbitMQConfig {
     

    //使用JSON序列化机制,进行消息转换
    @Bean
    public MessageConverter messageConverter() {
     
        return new Jackson2JsonMessageConverter();
    }


    //库存服务默认的交换机
    @Bean
    public Exchange stockEventExchange() {
     
        //参数:String name, boolean durable, boolean autoDelete, Map arguments
        TopicExchange topicExchange = new TopicExchange("stock-event-exchange", true, false);
        return topicExchange;
    }

    //普通队列
    @Bean
    public Queue stockReleaseStockQueue() {
     
        //参数:String name, boolean durable, boolean exclusive, boolean autoDelete, Map arguments
        Queue queue = new Queue("stock.release.stock.queue", true, false, false);
        return queue;
    }


    //延迟队列
    @Bean
    public Queue stockDelay() {
     

        HashMap<String, Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange", "stock-event-exchange");
        arguments.put("x-dead-letter-routing-key", "stock.release");
        // 消息过期时间 2分钟
        arguments.put("x-message-ttl", 120000);

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


    //交换机与普通队列绑定
    @Bean
    public Binding stockLocked() {
     
        //参数:String destination, DestinationType destinationType, String exchange, String routingKey,Map arguments
        Binding binding = new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.release.#",
                null);

        return binding;
    }


    //交换机与延迟队列绑定
    @Bean
    public Binding stockLockedBinding() {
     
        return new Binding("stock.delay.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.locked",
                null);
    }


}

2.环境配置

导入依赖

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

添加yml配置

# RabbitMQ配置
spring.rabbitmq.host=192.168.56.106
spring.rabbitmq.port=5672
# 虚拟主机配置
spring.rabbitmq.virtual-host=/
# 手动ack消息,不使用默认的消费端确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual

主启动类添加注解

@EnableRabbit
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallWareApplication {
     
 
    public static void main(String[] args) {
     
        SpringApplication.run(GulimallWareApplication.class, args);
    }
 
}

P294 “可靠消息+最终一致性”的实现—锁定库存

1.锁定库存

1.创建三个实体类

Day05_谷粒商城(谷粒商城高级篇四)摘要_第68张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第69张图片

"wms_ware_order_task"这张表就是用来记录哪一天给哪个订单执行了锁库存操做。
"wms_ware_order_task_detail"表就是记录哪个订单中的哪个商品在哪个仓库中锁定了多少库存。
这两张表是干什么的?我们要根据订单号查询到"wms_ware_order_task"表的task_id,然后拿着task_id去查"wms_ware_order_task_detail"表就可以得到这个订单下所有商品的库存锁定详情

①添加“com.xunqi.gulimall.ware.entity.WareOrderTaskEntity”,这个实体类和数据库的"wms_ware_order_task"表绑定,
这个表字段很多,但是只需要关注两个字段——“orderSn”和“createTime”,哪一天给哪个订单执行了锁库存操做.
这张表就是用来记录哪一天给哪个订单执行了锁库存操做。

@Data
@TableName("wms_ware_order_task")
public class WareOrderTaskEntity implements Serializable {
     
	private static final long serialVersionUID = 1L;

	/**
	 * id
	 */
	@TableId
	private Long id;
	/**
	 * order_id
	 */
	private Long orderId;
	/**
	 * order_sn
	 */
	private String orderSn;
	/**
	 * 收货人
	 */
	private String consignee;
	/**
	 * 收货人电话
	 */
	private String consigneeTel;
	/**
	 * 配送地址
	 */
	private String deliveryAddress;
	/**
	 * 订单备注
	 */
	private String orderComment;
	/**
	 * 付款方式【 1:在线付款 2:货到付款】
	 */
	private Integer paymentWay;
	/**
	 * 任务状态
	 */
	private Integer taskStatus;
	/**
	 * 订单描述
	 */
	private String orderBody;
	/**
	 * 物流单号
	 */
	private String trackingNo;
	/**
	 * create_time
	 */
	private Date createTime;
	/**
	 * 仓库id
	 */
	private Long wareId;
	/**
	 * 工作单备注
	 */
	private String taskComment;

}

②添加“com.atguigu.gulimall.ware.entity.WareOrderTaskDetailEntity”类,这个类和数据库的"wms_ware_order_task_detail"表绑定。
这个表就是记录哪个订单中的哪个商品在哪个仓库中锁定了多少库存

@AllArgsConstructor
@NoArgsConstructor
@Data
@TableName("wms_ware_order_task_detail")
public class WareOrderTaskDetailEntity implements Serializable {
     
 
	/**
	 * id
	 */
	@TableId
	private Long id;
	/**
	 * sku_id
	 */
	private Long skuId;
	/**
	 * sku_name
	 */
	private String skuName;
	/**
	 * 购买个数
	 */
	private Integer skuNum;
	/**
	 * 工作单id
	 */
	private Long taskId;
	/**
	 * 仓库id
	 */
	private long wareId;
	/**
	 * 锁定状态
	 */
	private Integer lockStatus;//(1表示锁定了,2表示解锁了,3表示锁定的库存已经扣减了)
}

添加StockLockedTo实体类,这个实体类相当于上面"wms_ware_order_task"表和"wms_ware_order_task_detail"表的结合

Day05_谷粒商城(谷粒商城高级篇四)摘要_第70张图片

2.锁定库存的方法
修改“com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl”类,

代码的逻辑是:
先说明“什么是锁定库存"?——假如顾客购买的商品之一是10件马甲,如果遍历了所有拥有马甲的仓库都没有找到一个剩余马甲数量大于10的仓库,那么这就是锁定库存失败;如果找到一个剩余马甲数量大于10的仓库,那么这就是锁定库存成功。
①submitOrder()方法里面锁定库存代码执行成功后结果下面的代码执行失败,submitOrder()方法就会回滚,锁定库存已经执行了没办法回滚,所以我们需要库存工作单表"wms_ware_order_task"记录哪一天给哪个订单执行了锁库存操做,从而做到让库存回滚。
②由于一个订单下有多个商品,我们给当前订单下的所有商品一个一个遍历进行锁定库存操做,当前商品锁定成功,就立刻做两件事:①给表"wms_ware_order_task_detail"保存工作单详情(记录哪个订单中的哪个商品在哪个仓库中锁定了多少库存)②将当前商品锁定了几件的工作单记录发送给MQ;
③某一件商品的锁定库存失败,那么抛出库存不足的异常,有异常代码就回滚,前面所有商品在数据库的"wms_ware_order_task"和"wms_ware_order_task_detail"这两张表中插入的数据就都被回滚了,但问题是前面商品的MQ消息发出去了,没关系,让MQ到数据库中用id查就可以,由于回滚了肯定查不到相关记录,只要查不到id就不用解锁

代码如下:

   @Transactional
@Override
public Boolean orderLockStock(WareSkuLockVo wareSkuLockVo) {
     

//***********************************************************************************
    //记录哪一天给哪个订单执行了锁库存操做
    WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
    taskEntity.setOrderSn(wareSkuLockVo.getOrderSn());
    taskEntity.setCreateTime(new Date());
    wareOrderTaskService.save(taskEntity);
//***********************************************************************************

    List<OrderItemVo> itemVos = wareSkuLockVo.getLocks();
    List<SkuLockVo> lockVos = itemVos.stream().map((item) -> {
     
        SkuLockVo skuLockVo = new SkuLockVo();
        skuLockVo.setSkuId(item.getSkuId());
        skuLockVo.setNum(item.getCount());
        List<Long> wareIds = baseMapper.listWareIdsHasStock(item.getSkuId(), item.getCount());
        skuLockVo.setWareIds(wareIds);
        return skuLockVo;
    }).collect(Collectors.toList());
 
    for (SkuLockVo lockVo : lockVos) {
     
        boolean lock = true;
        Long skuId = lockVo.getSkuId();
        List<Long> wareIds = lockVo.getWareIds();
        if (wareIds == null || wareIds.size() == 0) {
     
            throw new NoStockException(skuId);
        }else {
     
            for (Long wareId : wareIds) {
     
                Long count=baseMapper.lockWareSku(skuId, lockVo.getNum(), wareId);
                if (count==0){
     
                    lock=false;//库存锁定失败
                }else {
     
                
   		//***********************************************************************************
                    //1.锁定成功,保存工作单详情(记录哪个订单中的哪个商品在哪个仓库中锁定了多少库存)
                    WareOrderTaskDetailEntity detailEntity = WareOrderTaskDetailEntity.builder()
                            .skuId(skuId)
                            .skuName("")
                            .skuNum(lockVo.getNum())
                            .taskId(taskEntity.getId())
                            .wareId(wareId)
                            .lockStatus(1).build();
                    wareOrderTaskDetailService.save(detailEntity);
                    
                    //2.一旦锁库存成功,就发送库存锁定消息至延迟队列
                    StockLockedTo lockedTo = new StockLockedTo();
                    lockedTo.setId(taskEntity.getId());
                    StockDetailTo detailTo = new StockDetailTo();
                    BeanUtils.copyProperties(detailEntity,detailTo);
                    lockedTo.setDetailTo(detailTo);
                    rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);//发送消息给gulimall-ware的RabbitMQ
 
                    lock = true;
                    break;
		//***********************************************************************************
                }
            }
        }
        if (!lock) throw new NoStockException(skuId);
    }
    return true;
}

3.测试一下

Day05_谷粒商城(谷粒商城高级篇四)摘要_第71张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第72张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第73张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第74张图片

Day05_谷粒商城(谷粒商城高级篇四)摘要_第75张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第76张图片

P295 “可靠消息+最终一致性”的实现—库存自动解锁1

P296 “可靠消息+最终一致性”的实现—库存自动解锁2

P297 “可靠消息+最终一致性”的实现—库存自动解锁3

p283—p299不看别人的笔记,只看自己的

1.库存自动解锁

Day05_谷粒商城(谷粒商城高级篇四)摘要_第77张图片

  • 解锁库存的方法要监听"stock.release.stock.queue",这个队列一旦来消息了,就调用解锁库存方法。
  • 为保证消息的可靠到达,我们使用手动确认消息的模式,在解锁成功后确认消息,若出现异常则重新归队。

gulimall-ware的Lintener包下写一个监听器

@Component
@RabbitListener(queues = {
     "stock.release.stock.queue"})
public class StockReleaseListener {
     
 
    @Autowired
    private WareSkuService wareSkuService;
 
    @RabbitHandler
    public void handleStockLockedRelease(StockLockedTo stockLockedTo, Message message, Channel channel) throws IOException {
     
        log.info("************************收到库存解锁的消息********************************");
        try {
     
            wareSkuService.unlock(stockLockedTo); //调用解锁库存方法,后面会讲
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); //签收消息
        } catch (Exception e) {
     
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true); //解锁库存失败,拒收消息,拒收的消息重新放到队列里面
        }
    }
}

1.库存解锁的场景:
1)、下订单成功,库存锁定成功,submitOrder()整个方法都执行无误,但由于订单过期没有支付被系统自动取消、或被用户手动取消。需要解锁库存。
2)、库存锁定成功,但submitOrder()方法中锁库存后面的代码执行失败,导致submitOrder()方法回滚。之前锁定的库存就要解锁。我们可以根据"wms_ware_order_task"和"wms_ware_order_task_detail"这两张表中记录进行解锁。
3)、库存锁定失败,导致ubmitOrder()方法回滚。库存锁定失败,那么锁定库存的代码会回滚,那么"wms_ware_order_task"和"wms_ware_order_task_detail"这两张表中插入的数据就都被回滚了,但问题是前面商品的MQ消息发出去了,没关系,让MQ到数据库中用id查就可以,由于回滚了肯定查不到相关记录,只要查不到id就不用解锁。

2.编写unlock()代码

unlock()代码的逻辑:

  • 如果"wms_ware_order_task_detail"表不为空,说明该库存锁定成功。我们需要判断是否需要解锁库存。
    • 查询最新的订单,如果订单不存在,说明submitOrder()方法出现异常回滚,我们需要对已锁定的库存进行解锁
    • 如果订单存在,进一步查询订单状态,如果订单状态是“已取消“,说明订单过期没有支付被系统自动取消、或被用户手动取消,那么需要解锁库存。
  • 如果"wms_ware_order_task_detail"表为空,说明执行锁定库存的时候就异常回滚了,库存未锁定,自然无需解锁

为保证幂等性,我们分别对订单的状态和工作单的状态都进行了判断,只有当订单过期且工作单显示当前库存处于锁定的状态时,才进行库存的解锁

com.atguigu.gulimall.ware.service.impl

@Override
    public void unlock(StockLockedTo stockLockedTo) {
     
        StockDetailTo detailTo = stockLockedTo.getDetailTo();
        WareOrderTaskDetailEntity detailEntity = wareOrderTaskDetailService.getById(detailTo.getId());
        //如果工作单详情不为空,说明该库存锁定成功
        if (detailEntity != null) {
     
            WareOrderTaskEntity taskEntity = wareOrderTaskService.getById(stockLockedTo.getId());
            R r = orderFeignService.getOrderStatus(taskEntity.getOrderSn());//根据订单号远程查询订单状态(后面会将)
            if (r.getCode() == 0) {
     
            	//R状态码为0说明远程查询成功
                OrderTo order = r.getData("order", new TypeReference<OrderTo>() {
     });
                //没有这个订单||订单状态已经取消 解锁库存
                if (order == null||order.getStatus()== OrderStatusEnum.CANCLED.getCode()) {
     
                    //工作单详情有三种状态(1是已锁定,2是已解锁,3是已扣减),只有工作单详情状态是1才需要给它解锁
                    if (detailEntity.getLockStatus()== WareTaskStatusEnum.Locked.getCode()){
     
                    	//调用"解锁库存方法"(后面会讲)
                        unlockStock(detailTo.getSkuId(), detailTo.getSkuNum(), detailTo.getWareId(), detailEntity.getId());
                    }
                }
            }else {
     
                throw new RuntimeException("远程调用订单服务失败");
            }
        }else {
     
            //无需解锁
        }
    }

3.gulimall-ware远程调用gulimall-order查询订单的状态

添加“com.atguigu.gulimall.ware.feign.OrderFeignService”类,代码如下:

@FeignClient("gulimall-order")
public interface OrderFeignService {
     
    @GetMapping("/order/order/status/{orderSn}")
    R getOrderStatus(@PathVariable("orderSn") String orderSn);
}

添加“com.atguigu.gulimall.order.controller.OrderController”类,代码如下:

    @GetMapping("/status/{orderSn}")
    public R getOrderStatus(@PathVariable("orderSn") String orderSn){
     
        OrderEntity orderEntity = orderService.getOrderByOrderSn(orderSn);
        return R.ok().setData(orderEntity);
    }

修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”类,代码如下:

    @Override
    public OrderEntity getOrderByOrderSn(String orderSn) {
     
        OrderEntity order_sn = this.getOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderSn));
        return order_sn;
    }

4.解锁库存unLockStock()方法

//根据skuid、num、wareid、task_id来解锁库存

    public void unLockStock(Long skuId,Long wareId,Integer num,Long taskDetailId) {
     

        //库存解锁
        wareSkuDao.unLockStock(skuId,wareId,num);

        //更新工作单的状态
        WareOrderTaskDetailEntity taskDetailEntity = new WareOrderTaskDetailEntity();
        taskDetailEntity.setId(taskDetailId);
        taskDetailEntity.setLockStatus(2);//变为已解锁
        wareOrderTaskDetailService.updateById(taskDetailEntity);

    }

修改“com.atguigu.gulimall.ware.dao.WareSkuDao”类,代码如下:

void unlockStock(@Param("skuId") Long skuId, @Param("wareId") Long wareId, @Param("num") Integer num, @Param("taskDetailId") Long taskDetailId);
     <update id="unlockStock">
        update wms_ware_sku set stock_locked = stock_locked - #{
     num}
        where sku_id = #{
     skuId} and ware_id = #{
     wareId}
    </update>

5.调试
测试时出现异常,先调试

由于gulimall-order添加了拦截器,只要使用该服务必须登录才行。因为gulimall-ware需要远程调用订单,但不需要登录,所以给这个路径放行
修改gulimall-order的interceptor的LoginUserInterceptor.java

@Component
public class LoginUserInterceptor implements HandlerInterceptor {
     
 
    public static ThreadLocal<MemberResponseVO> loginUser = new ThreadLocal<>();
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
     
    	//放行远程调用订单的url
        String uri = request.getRequestURI();
        boolean match = new AntPathMatcher().match("/order/order/status/**", uri);
        if (match){
     
            return true;
        }
 
 
        MemberResponseVO attribute = (MemberResponseVO) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
        if (attribute != null){
     
            loginUser.set(attribute);
            return true;
        }else {
     
            //没登录就去登录
            request.getSession().setAttribute("msg","请先进行登录");
            response.sendRedirect("http://auth.gulimall.com/login.html");
            return false;
        }
    }
}

2.测试

Day05_谷粒商城(谷粒商城高级篇四)摘要_第78张图片

Day05_谷粒商城(谷粒商城高级篇四)摘要_第79张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第80张图片

Day05_谷粒商城(谷粒商城高级篇四)摘要_第81张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第82张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第83张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第84张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第85张图片

P298 “可靠消息+最终一致性”的实现—定时关单

Day05_谷粒商城(谷粒商城高级篇四)摘要_第86张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第87张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第88张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第89张图片
上面的图看不懂就立刻回看视频p298,视频里面用两分钟讲的清清楚楚
Day05_谷粒商城(谷粒商城高级篇四)摘要_第90张图片

1)、提交订单

添加“com.atguigu.gulimall.order.service.impl”类,代码如下:

    @Transactional(rollbackFor = Exception.class)
    // @GlobalTransactional(rollbackFor = Exception.class)
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
     

        confirmVoThreadLocal.set(vo);//设置到ThreadLocal里面,让大家共享

        SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();

        MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
        responseVo.setCode(0);

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        String orderToken = vo.getOrderToken();

        Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
                Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),
                orderToken);

        if (result == 0L) {
     
            //令牌验证失败
            responseVo.setCode(1);
            return responseVo;
        } else {
     
            //令牌验证成功
            //1、创建订单、订单项等信息
            OrderCreateTo order = createOrder();

            //2、验证价格
            BigDecimal payAmount = order.getOrder().getPayAmount();
            BigDecimal payPrice = vo.getPayPrice();

            if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
     
                //金额对比
                //TODO 3、保存订单
                saveOrder(order);

                //4、库存锁定,只要有异常,回滚订单数据
                //订单号、所有订单项信息(skuId,skuNum,skuName)
                WareSkuLockVo lockVo = new WareSkuLockVo();
                lockVo.setOrderSn(order.getOrder().getOrderSn());

                //获取出要锁定的商品数据信息
                List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map((item) -> {
     
                    OrderItemVo orderItemVo = new OrderItemVo();
                    orderItemVo.setSkuId(item.getSkuId());
                    orderItemVo.setCount(item.getSkuQuantity());
                    orderItemVo.setTitle(item.getSkuName());
                    return orderItemVo;
                }).collect(Collectors.toList());
                lockVo.setLocks(orderItemVos);

                R r = wmsFeignService.orderLockStock(lockVo);
                if (r.getCode() == 0) {
     
                    //锁定成功
                    responseVo.setOrder(order.getOrder());
                    // int i = 10/0;

			//****************************************最新代码**************************************************
                    //订单创建成功,发送消息给MQ
                    rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());
			//****************************************最新代码**************************************************
			
                    
                    redisTemplate.delete(CART_PREFIX+memberResponseVo.getId());//删除购物车里的数据
                    return responseVo;
                } else {
     
                    //锁定失败
                    String msg = (String) r.get("msg");
                    throw new NoStockException(msg);
                }

            } else {
     
                responseVo.setCode(2);
                return responseVo;
            }
        }
    }

2)、监听队列

创建订单的消息会进入延迟队列,最终发送至队列order.release.order.queue,因此我们对该队列进行监听,进行订单的关闭

@Service
@RabbitListener(queues = "order.release.order.queue")
public class OrderCloseListener {
     
 
    @Autowired
    private OrderService orderService;
 
    @RabbitHandler
    public void listener(OrderEntity entity, Channel channel, Message message) throws IOException {
     
        try {
     
            orderService.closeOrder(entity);//定时关单(后面会讲)
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
     
            // 修改失败 拒绝消息 使消息重新入队
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
        }
    }
 
}
3)、定时关单
  • 关闭订单前要查询最新的订单状态判断是否需要关单
  • 关闭订单后也需要解锁库存,因此发送消息进行库存、会员服务对应的解锁

添加“com.atguigu.gulimall.order.service.OrderService”类,代码如下:

void closeOrder(OrderEntity entity);

修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”,代码如下:

@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.CANCLED.getCode());//修改订单为“已取消”
            this.updateById(update);//跟新"oms_order"表
            
            //把订单数据封装发给RabbitMQ,然后交给库存解锁服务,让解锁库存
            OrderTo orderTo = new OrderTo();
            BeanUtils.copyProperties(orderEntity,orderTo);
            rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
        }
    }

添加“com.atguigu.common.to.mq.OrderTo”类,OrderTo其实和OrderEntity是一模一样的,只是传输数据用To,OrderTo代码如下:

@Data
public class OrderTo {
     
    private Long id;
    /**
     * member_id
     */
    private Long memberId;
    /**
     * 订单号
     */
    private String orderSn;
    /**
     * 使用的优惠券
     */
    private Long couponId;
    /**
     * create_time
     */
    private Date createTime;
    /**
     * 用户名
     */
    private String memberUsername;
    /**
     * 订单总额
     */
    private BigDecimal totalAmount;
    /**
     * 应付总额
     */
    private BigDecimal payAmount;
    /**
     * 运费金额
     */
    private BigDecimal freightAmount;
    /**
     * 促销优化金额(促销价、满减、阶梯价)
     */
    private BigDecimal promotionAmount;
    /**
     * 积分抵扣金额
     */
    private BigDecimal integrationAmount;
    /**
     * 优惠券抵扣金额
     */
    private BigDecimal couponAmount;
    /**
     * 后台调整订单使用的折扣金额
     */
    private BigDecimal discountAmount;
    /**
     * 支付方式【1->支付宝;2->微信;3->银联; 4->货到付款;】
     */
    private Integer payType;
    /**
     * 订单来源[0->PC订单;1->app订单]
     */
    private Integer sourceType;
    /**
     * 订单状态【0->待付款;1->待发货;2->已发货;3->已完成;4->已关闭;5->无效订单】
     */
    private Integer status;
    /**
     * 物流公司(配送方式)
     */
    private String deliveryCompany;
    /**
     * 物流单号
     */
    private String deliverySn;
    /**
     * 自动确认时间(天)
     */
    private Integer autoConfirmDay;
    /**
     * 可以获得的积分
     */
    private Integer integration;
    /**
     * 可以获得的成长值
     */
    private Integer growth;
    /**
     * 发票类型[0->不开发票;1->电子发票;2->纸质发票]
     */
    private Integer billType;
    /**
     * 发票抬头
     */
    private String billHeader;
    /**
     * 发票内容
     */
    private String billContent;
    /**
     * 收票人电话
     */
    private String billReceiverPhone;
    /**
     * 收票人邮箱
     */
    private String billReceiverEmail;
    /**
     * 收货人姓名
     */
    private String receiverName;
    /**
     * 收货人电话
     */
    private String receiverPhone;
    /**
     * 收货人邮编
     */
    private String receiverPostCode;
    /**
     * 省份/直辖市
     */
    private String receiverProvince;
    /**
     * 城市
     */
    private String receiverCity;
    /**
     * 区
     */
    private String receiverRegion;
    /**
     * 详细地址
     */
    private String receiverDetailAddress;
    /**
     * 订单备注
     */
    private String note;
    /**
     * 确认收货状态[0->未确认;1->已确认]
     */
    private Integer confirmStatus;
    /**
     * 删除状态【0->未删除;1->已删除】
     */
    private Integer deleteStatus;
    /**
     * 下单时使用的积分
     */
    private Integer useIntegration;
    /**
     * 支付时间
     */
    private Date paymentTime;
    /**
     * 发货时间
     */
    private Date deliveryTime;
    /**
     * 确认收货时间
     */
    private Date receiveTime;
    /**
     * 评价时间
     */
    private Date commentTime;
    /**
     * 修改时间
     */
    private Date modifyTime;
}
4)、解锁库存

修改“com.atguigu.gulimall.ware.listener.StockReleaseListener”类,代码如下:

@Slf4j
@Service
@RabbitListener(queues = "stock.release.stock.queue")
public class StockReleaseListener {
     
 
    @Autowired
    WareSkuService wareSkuService;
 
	//这是老的代码,监听来自gulimall-ware那边的MQ发来的消息
    @RabbitHandler
    public void handleStockLockedRelease(StockLockedTO to, Message message, Channel channel) throws IOException {
     
        log.info("************************收到库存解锁的消息********************************");
        try {
     
            wareSkuService.unLockStock(to);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
     
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
 
 	//这是最新的代码,监听来自gulimall-order那边的MQ发来的消息
    @RabbitHandler
    public void handleOrderCloseRelease(OrderTo to, Message message, Channel channel) throws IOException {
     
        log.info("************************订单关闭准备解锁库存********************************");
        try {
     
            wareSkuService.unLockStockForOrder(to);//解锁库存
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
     
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
}

修改“com.atguigu.gulimall.ware.service.WareSkuService”类,代码如下:

void unLockStockForOrder(OrderTo to);

修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”类,代码如下:

    @Transactional
    @Override
    public void unLockStockForOrder(OrderTo to) {
     
    
        String orderSn = to.getOrderSn();

     	/**
     	* 
     	* 思考:我们之前解锁库存前先看一下订单的状态,这里就不用,因为订单状态肯定是"已取消"才会发消息到这里来解锁库存的。
     	* 那我们需要不需要查一下库存解锁状态?需要,在解锁之前先确认lock_status=1再进行解锁(只有库存锁定状态是"已锁定"的才需要解锁)
     	* 你看下面代码中有一个eq("lock_status", 1),也就是对lock_status=1的进行解锁
     	* 
     	*/
     
        //根据订单号从"wms_ware_order_task"表拿到task_id
        WareOrderTaskEntity task = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);
        Long id = task.getId();
        
        //根据task_id查找"wms_ware_order_task_detail"表中没有解锁的库存,进行解锁
        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());
            //unLockStock()方法就是调用以前写的解锁库存的方法
        }
    }

修改“com.atguigu.gulimall.ware.service.WareOrderTaskService”类,代码如下:

WareOrderTaskEntity getOrderTaskByOrderSn(String orderSn);

修改“com.atguigu.gulimall.ware.service.impl.WareOrderTaskServiceImpl”类,代码如下:

    @Override
    public WareOrderTaskEntity getOrderTaskByOrderSn(String orderSn) {
     
        WareOrderTaskEntity orderTaskEntity = this.getOne(new QueryWrapper<WareOrderTaskEntity>().eq("order_sn", orderSn));
        return orderTaskEntity;
    }

P299 “可靠消息+最终一致性”的实现—保证消息的可靠

可靠消息+最终一致性”,我们必须保证消息的可靠性。

1.消息丢失问题:

1.使用try…catch语句

Day05_谷粒商城(谷粒商城高级篇四)摘要_第91张图片
Day05_谷粒商城(谷粒商城高级篇四)摘要_第92张图片

2.创建存放消息的数据库

Day05_谷粒商城(谷粒商城高级篇四)摘要_第93张图片
在这里插入图片描述

3.使用ACK机制
一定要开启手动ACK模式。消息的发送者、接收者都要开启手动ack

2.消息重复问题

有机会再学

3.消息积压问题

有机会再学

你可能感兴趣的:(谷粒商城,其他)