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

本文重点记录老师讲的话 和 一些配置流程,笔记中有的内容尽量少记录。

目录

九、消息队列

1、MQ简介

2、RabbitMQ概念

3、docker安装RabbitMQ

4、SpringBoot整合RabbitMQ

5、AmqpAdmin使用

6、RabbitTemplate使用

7、RabbitListener&RabbitHandler接收消息

8、可靠投递-发送端确认

9、可靠投递-消费端确认

十、订单服务

1、页面环境搭建

2、整合SpringSession

3、订单登录拦截

1)、订单流程

4、订单确认页数据获取

5、Feign远程调用丢失请求头问题

6、Feign异步调用丢失请求头问题

7、页面调整

8、订单确认页库存查询

9、订单确认页模拟运费效果

10、接口幂等性讨论

11、添加防重令牌

12、订单提交

 13、分布式事务

14、分布式事务常见解决方案

15、Seata

1)、概念

2)、Seata术语

3)、SEATA 的分布式交易解决方案

4)、seata控制分布式事务需要

5)、每个微服务数据库加上undo_log(回滚日志表)

因为linux出现问题,时间紧迫没处理,现在各个数据服务器连接不起来 ,只是跟着老师敲,没法展示

6)、环境搭建

16、RabbitMQ延时队列(实现定时任务)

17、库存自动解锁

1)、库存锁定

2)、监听队列

3)、库存解锁

18、定时关单

1)、提交订单

2)、监听队列

 3)、关闭订单

4)、解锁库存

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


 

九、消息队列

1、MQ简介

  • 异步任务
  • 应用解耦
  • 流量控制

概述:

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

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

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

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

 

2、RabbitMQ概念

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

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

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

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

3、docker安装RabbitMQ

不下载镜像,直接安装。默认会帮你下载

docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management

修改只要启动docker自动重启rabbitMQ

docker update rabbitmq --restart=always

账号:guest

密码:guest

创建一个交换机

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

创建队列

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

交换机绑定队列

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

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

删除交换机,先双击点击要删除的交换机,接着

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

4、SpringBoot整合RabbitMQ

RabbitMQ的使用

1、引入amqp;RabbitAutoConfiguration就会自动生效

2、给容器中自动配置了RabbitTemplate、AmqpAdmin、CachingConnectionFactory、RabbitMessagingTemplate

             所有的属性都是

            @ConfigurationProperties(prefix = "spring.rabbitmq")

            public class RabbitProperties

3、给配置文件中配置 spring.rabbitmq 信息

4、@EnableRabbit 开启功能

1)、导入amqp依赖

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

2)、添加配置(@ConfigurationProperties(prefix = "spring.rabbitmq"))注意配置前缀一定是spring.rabbitmq

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

3)、主启动类添加@EnableRabbit注解()

@EnableRabbit
@SpringBootApplication
public class GulimallOrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(GulimallOrderApplication.class, args);
    }

}

5、AmqpAdmin使用

@Slf4j
@SpringBootTest
class GulimallOrderApplicationTests {

    @Autowired
    AmqpAdmin amqpAdmin;

    /**
     * 1、如何创建Exchange[hello-java-exchange]、Queue、Binding
     *      1)、使用AmqpAdmin进行创建
     * 2、如何收发消息
     */
    @Test
    void contextLoads() {
        DirectExchange directExchange = new DirectExchange("hello-java-exchange",true,false);
        amqpAdmin.declareExchange(directExchange);
        log.info("Exchange[{}]创建成功","hello-java-exchange");
    }

    @Test
    public void createQueue(){
        Queue queue = new Queue("hello-java-queue",true,false,false);
        amqpAdmin.declareQueue(queue);
        log.info("Queue[{}]创建成功","hello-java-queue");
    }

    @Test
    public 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");
    }
}

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

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

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

6、RabbitTemplate使用

@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("哈哈哈");
        //1、发送消息,如果发送的消息是个对象,我们会使用序列化机制,将对象写出去。对象必须实现Serializable
        String msg = "hello word";

        //2、配置MyRabbitConfig,让发送的对象类型的消息,可以是一个json
        rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",orderReturnApplyEntity);
        log.info("消息发送完成{}",orderReturnApplyEntity);
    }
}    

注意:

配置MyRabbitConfig,让发送的对象类型的消息,可以是一个json

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

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

7、RabbitListener&RabbitHandler接收消息

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

        @RabbitListener: 类+方法上(监听哪些队列即可)

        @RabbitHandler: 标在方法上(重载区分不同的消息)

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

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

@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("哈哈哈");
                //配置MyRabbitConfig,让发送的对象类型的消息,可以是一个json
                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";
    }

}

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

package com.atguigu.gulimall.order.service.impl;

import com.atguigu.gulimall.order.entity.OrderEntity;
import com.atguigu.gulimall.order.entity.OrderReturnApplyEntity;
import com.rabbitmq.client.AMQP;
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 java.util.Map;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;

import com.atguigu.gulimall.order.dao.OrderItemDao;
import com.atguigu.gulimall.order.entity.OrderItemEntity;
import com.atguigu.gulimall.order.service.OrderItemService;

import javax.swing.*;

@RabbitListener(queues = {"hello-java-queue"})
@Service("orderItemService")
public class OrderItemServiceImpl extends ServiceImpl<OrderItemDao, OrderItemEntity> implements OrderItemService {

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<OrderItemEntity> page = this.page(
                new Query<OrderItemEntity>().getPage(params),
                new QueryWrapper<OrderItemEntity>()
        );

        return new PageUtils(page);
    }

    /**
     * queues:声明需要监听的所有队列
     *
     * org.springframework.amqp.core.Message
     * @param message
     *
     * 参数可以写以下内容
     * 1、Message message:原生消息详细信息。头+体
     * 2、T<发送的消息类型> OrderReturnApplyEntity content
     * 3、Channel channel 当前传输数据的通道
     *
     * Queue:可以很多人都来监听。只要收到消息,队列删除消息,而且只能有一个收到此消息
     * 场景:
     *       1)、订单服务启动多个;同一个消息,只能有一个客户端收到
     *       2)、只有一个消息完全处理完,方法运行结束,我们就可以接收到下一个消息
     *
     */
    //@RabbitListener(queues = {"hello-java-queue"})
    @RabbitHandler
    public void receiverMessage(Message message,OrderReturnApplyEntity content,
                                Channel channel) throws InterruptedException {
        //消息体
        byte[] body = message.getBody();
        //消息头属性信息
        MessageProperties properties = message.getMessageProperties();
        System.out.println("接收到消息...内容:" + content);
//        Thread.sleep(3000);
        System.out.println("消息处理完成=》"+content.getReturnName());
    }

    @RabbitHandler
    public void receiverMessage(OrderEntity orderEntity){
        System.out.println("接收到消息...内容:" + orderEntity);

    }
}

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

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

8、可靠投递-发送端确认

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

 定制RabbitTemplate

服务器收到消息就回调

1、开启发送端确认

        1、spring.rabbitmq.publisher-confirms=true

        2、设置确认回调

2、消息抵达队列就回调

        1、#开启发送端抵达队列确认

         spring.rabbitmq.publisher-returns=true

        #只要抵达队列,以异步发送优先回调我们这个returnConfirm

         spring.rabbitmq.template.mandatory=true

       2、设置确认回调ReturnCallback

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

修改application.properties

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

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

package com.atguigu.gulimall.order.config;

import org.springframework.amqp.core.Message;
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.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;

/**
 * @Description: MyRabbitConfig
 * @Author: WangTianShun
 * @Date: 2020/11/23 21:58
 * @Version 1.0
 */
@Configuration
public class MyRabbitConfig {
    @Autowired
    RabbitTemplate rabbitTemplate;
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }

    /**
     * 定制RabbitTemplate
     * 服务器收到消息就回调
     * 1、开启发送端确认
     *      1、spring.rabbitmq.publisher-confirms=true
     *      2、设置确认回调
     * 2、消息抵达队列就回调
     *      1、#开启发送端抵达队列确认
     *         spring.rabbitmq.publisher-returns=true
     *         #只要抵达队列,以异步发送优先回调我们这个returnConfirm
     *         spring.rabbitmq.template.mandatory=true
     *      2、设置确认回调ReturnCallback
     *  3、消费端确认(保证每个消息被正确消费,此时才可以保证broker删除这个消息)
     */
    @PostConstruct   //MyRabbitConfig对象创建完以后,执行这个方法
    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+"]");
            }
        });
    }
}

9、可靠投递-消费端确认

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

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

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

问题:

    我们收到很多消息,自动回复给服务器ack,只有一个消息处理成功,宕机了。发生消息丢失

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

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

2、

      1)、#手动确认收货(ack)

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

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

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

添加application.properties

#手动确认收货(ack)
spring.rabbitmq.listener.simple.acknowledge-mode=manual
    @RabbitHandler
    public void receiverMessage(Message message,OrderReturnApplyEntity content,
                                Channel channel) throws InterruptedException {
        //消息体
        byte[] body = message.getBody();
        //消息头属性信息
        MessageProperties properties = message.getMessageProperties();
        System.out.println("接收到消息...内容:" + content);
//        Thread.sleep(3000);
        System.out.println("消息处理完成=》"+content.getReturnName());
        //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);
                System.out.println("没有签收货物..."+deliveryTag);
            }

        }catch (Exception e){
            //网络中断
        }

    }

十、订单服务


1、页面环境搭建

1)、把静态资源放到虚拟机的nginx里,在/mydata/nginx/html/static/目录先创建order文件夹,在创建detail文件夹,并把静态资源上传到这个文件夹

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

把index.html放到gulimall-order服务,改名为detail.html

2)、在/mydata/nginx/html/static/order目录下创建list文件夹,并把静态资源上传到这个文件夹

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

把index.html放到gulimall-order服务,改名为list.html

3)、在/mydata/nginx/html/static/order目录下创建confirm文件夹,并把静态资源上传到这个文件夹

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

把index.html放到gulimall-order服务,改名为confirm.html

4)、在/mydata/nginx/html/static/order目录下创建pay文件夹,并把静态资源上传到这个文件夹

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

把index.html放到gulimall-order服务,改名为pay.html

5)、在C:\Windows\System32\drivers\etc\hosts文件里添加域名(把属性只读模式去掉,用记事本打开)

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

6)、在gulimal-gateway添加路由

        - id: gulimall_order_route
          uri: lb://gulimall-order
          predicates:
            - Host=order.gulimall.com

7)、修改每个html的资源访问路径

confirm.html

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

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

加上thymeleaf模板空间

<!DOCTYPE html>
<html  lang="en" xmlns:th="http://www.thymeleaf.org">

接下来几个html以此类推进行修改

8)、引入thymeleaf模板引擎

        <!--模板引擎 thymeleaf-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

关闭thymeleaf缓存

spring:
  thymeleaf:
    cache: false

测试

创建“com.atguigu.gulimall.order.web.HelloController”类,代码如下

@RestController
public class HelloController {

    @GetMapping("{page}.html")
    public String listPage(@PathVariable("page") String page){

        return page;
    }
}

启动gulimall-order和guliall-gateway

问题:访问http://order.gulimall.com/confirm.html访问失败,报503

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

原因:gulimall-order服务没有加入到注册中心

解决:1)、pom文件已经导入gulimall-common依赖,说明gulimall-order服务包含注册中心nacos的依赖

          2)、在主启动类添加@EnableDiscoveryClient注解

          3)、配置应用名和注册中心地址(如果不配置应用名,注册到注册中心不会成功)

spring:
  application:
    name: gulimall-order
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

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

confirm.html页面报错,搜素/*把它去掉即可

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

http://order.gulimall.com/confirm.html

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

 

2、整合SpringSession

添加依赖

        <!--属性配置的提示工具-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <!--整合SpringSession完成session共享问题-->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

        <!--引入redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>

修改application.properties

#SpringSession的存储类型
spring.session.store-type=redis
#线程池属性的配置
gulimall.thread.core= 20
gulimall.thread.max-size= 200
gulimall.thread.keep-alive-time= 10
#reidis地址
spring.redis.host=172.20.10.3

添加SpringSession的配置,添加“com.atguigu.gulimall.order.config.GulimallSessionConfig”类,代码如下

@Configuration
public class GulimallSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer(){
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName("gulimall.com");
        cookieSerializer.setCookieName("GULISESSION");
        return cookieSerializer;
    }

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
        return new GenericJackson2JsonRedisSerializer();
    }
}

添加线程池的配置,添加“com.atguigu.gulimall.order.config.MyThreadConfig”类,代码如下

@Configuration
public class MyThreadConfig {
    @Bean
    public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool){
        return new ThreadPoolExecutor(pool.getCore(),
                pool.getMaxSize(),
                pool.getKeepAliveTime(),
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(100000),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
    }
}

线程池配置需要的属性

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

@ConfigurationProperties(prefix = "gulimall.thread")
@Component
@Data
public class ThreadPoolConfigProperties {
    private Integer core;
    private Integer maxSize;
    private Integer keepAliveTime;
}

主启动类是上添加SpingSession自动启动的注解

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

页面调整

修改商城首页我的订单地链接地址

谷粒商城-个人笔记(高级篇四)获取用户信息

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

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

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

3、订单登录拦截

1)、订单流程

订单生成 -> 支付订单 -> 卖家发货 -> 确认收货 -> 交易成功

修改“去结算”的链接地址

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

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

@Controller
public class OrderWebController {

    @GetMapping("/toTrade")
    public String toTrade(){
        return "confirm";
    }
}

添加登录拦截器类“com.atguigu.gulimall.order.interceptor.LoginUserInterceptor”,代码如下:

@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 {
        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;
        }
    }
}

添加拦截器的配置“com.atguigu.gulimall.order.config.OrderWebConfiguration”类,代码如下:

@Configuration
public class OrderWebConfiguration implements WebMvcConfigurer {
    @Autowired
    LoginUserInterceptor loginUserInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
    }
}

修改gulimall-auth-server的login.html页面

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

4、订单确认页数据获取

修改“com.atguigu.gulimall.order.web.OrderWebController”类,代码如下:

@Controller
public class OrderWebController {

    @Autowired
    OrderService orderService;


    @GetMapping("/toTrade")
    public String toTrade(Model model){
        OrderConfirmVo confirmVo = orderService.confirmOrder();
        //展示订单确认的数据
        model.addAttribute("orderConfirmData",confirmVo);
        return "confirm";
    }
    
}

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

//订单确认页需要用的数据
public class OrderConfirmVo {

    //收获地址,ums_member_receive_address表
    List<MemberAddressVo> address;

    //所有选中的购物项
    List<OrderItemVo> items;

    //发票。。。

    //优惠券信息。。。
    //积分
    Integer integration;

    //订单总额
    BigDecimal total;

    //应付价格
    BigDecimal payPrice;

    //防重令牌
    String orderToken;

    //总件数
    public Integer getCount(){
        Integer i = 0;
        if (items != null){
            for (OrderItemVo item : items) {
                i += item.getCount();
            }
        }
        return i;
    }

    public List<MemberAddressVo> getAddress() {
        return address;
    }

    public void setAddress(List<MemberAddressVo> address) {
        this.address = address;
    }

    public List<OrderItemVo> getItems() {
        return items;
    }

    public void setItems(List<OrderItemVo> items) {
        this.items = items;
    }

    public Integer getIntegration() {
        return integration;
    }

    public void setIntegration(Integer integration) {
        this.integration = integration;
    }

    public BigDecimal getTotal() {
        BigDecimal total = new BigDecimal("0");
        if (items != null){
            for (OrderItemVo item : items) {
                BigDecimal multiply = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));
                total = total.add(multiply);
            }
        }

        return total;
    }

    public BigDecimal getPayPrice() {
        return getTotal();
    }

    public String getOrderToken() {
        return orderToken;
    }

    public void setOrderToken(String orderToken) {
        this.orderToken = orderToken;
    }
}

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

    /**
     * 订单确认页返回需要用到的数据
     * @return
     */
    OrderConfirmVo confirmOrder();

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

    @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;
    }

添加“com.atguigu.gulimall.order.vo.MemberAddressVo”类,代码如下

@Data
public class MemberAddressVo {
    private Long id;
    /**
     * member_id
     */
    private Long memberId;
    /**
     * 收货人姓名
     */
    private String name;
    /**
     * 电话
     */
    private String phone;
    /**
     * 邮政编码
     */
    private String postCode;
    /**
     * 省份/直辖市
     */
    private String province;
    /**
     * 城市
     */
    private String city;
    /**
     * 区
     */
    private String region;
    /**
     * 详细地址(街道)
     */
    private String detailAddress;
    /**
     * 省市区代码
     */
    private String areacode;
    /**
     * 是否默认
     */
    private Integer defaultStatus;
  
}

添加“com.atguigu.gulimall.order.vo.OrderItemVo”类,代码如下

@Data
public class OrderItemVo {
    private Long skuId;

    //标题
    private String title;

    //图片
    private String image;

    //商品套餐属性
    private List<String> skuAttr;

    //价格
    private BigDecimal price;

    //数量
    private Integer count;

    //总价
    private BigDecimal totalPrice;

    //TODO 查询库存状态
    //是否有货
    private boolean hasStock;
    
    //重量
    private BigDecimal weight;

}

远程调用要开启fegin客户端

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

//1、远程查询所有的收货地址列表

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

@FeignClient("gulimall-member")
public interface MemberFeignService {
    @GetMapping("/member/memberreceiveaddress/{memberId}/addresses")
    List<MemberAddressVo> getAddress(@PathVariable("memberId") Long memberId);
}

gulimall-member

修改“com.atguigu.gulimall.member.controller.MemberReceiveAddressController”类,代码如下

    @GetMapping("/{memberId}/addresses")
    public List<MemberReceiveAddressEntity> getAddress(@PathVariable("memberId") Long memberId){
        return memberReceiveAddressService.getAddress(memberId);
    }

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

List<MemberReceiveAddressEntity> getAddress(Long memberId);

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

    @Override
    public List<MemberReceiveAddressEntity> getAddress(Long memberId) {
        List<MemberReceiveAddressEntity> memberAddress = this.list(new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id", memberId));
        return memberAddress;
    }

//2、远程查询购物车所有选中的购物项

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

@FeignClient("gulimall-cart")
public interface CartFeignService {
    @GetMapping("/currentUserCartItems")
    List<OrderItemVo> getCurrentUserCartItems();
}

gulimall-cart

修改“com.atguigu.gulimall.cart.controller.CartController”类,代码如下

    @GetMapping("/currentUserCartItems")
    @ResponseBody
    public List<CartItem> getCurrentUserCartItems(){
        return cartService.getUserCartItems();
    }

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

    /**
     * 获取用户购物车里购物项的所有数据
     * @return
     */
    List<CartItem> getUserCartItems();

修改“com.atguigu.gulimall.cart.service.impl.CartServiceImpl”类,代码如下

    @Override
    public List<CartItem> getUserCartItems() {
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        if (userInfoTo.getUserId() == null){
            return null;
        }else {
            String cartKey = CART_PREFIX + userInfoTo.getUserId();
            List<CartItem> cartItems = getCartItems(cartKey);
            //获取所有被选中的购物项
            List<CartItem> collect = cartItems.stream().filter(item -> item.getCheck())
                    .map(item->{
                        R price = productFeignService.getPrice(item.getSkuId());
                        //更新为最新价格
                        String data = (String) price.get("data");
                        item.setPrice(new BigDecimal(data));
                        return item;})
                    .collect(Collectors.toList());
            return collect;
        }

    }

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

    @GetMapping("/product/skuinfo/{skuId}/price")
    R getPrice(@PathVariable("skuId") Long skuId);

gulimall-product

修改“com.atguigu.gulimall.product.app.SkuInfoController”类,代码如下:

    @GetMapping("/{skuId}/price")
    public R getPrice(@PathVariable("skuId") Long skuId){
        SkuInfoEntity byId = skuInfoService.getById(skuId);
        return R.ok().setData(byId.getPrice().toString());
    }

5、Feign远程调用丢失请求头问题

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

  • feign远程调用的请求头中没有含有JSESSIONIDcookie,所以也就不能得到服务端的session数据,cart认为没登录,获取不了用户信息
  • 但是在feign的调用过程中,会使用容器中的RequestInterceptorRequestTemplate进行处理,因此我们可以通过向容器中导入定制的RequestInterceptor为请求加上cookie
  • RequestContextHolder为SpingMVC*享request数据的上下文,底层由ThreadLocal实现。经过RequestInterceptor处理后的请求如下,已经加上了请求头的Cookie信息

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

package com.atguigu.gulimall.order.config;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;

/**
 * @Description: 请求拦截器
 * @Author: WangTianShun
 * @Date: 2020/11/26 9:10
 * @Version 1.0
 */
@Configuration
public class GuliFeignConfig {

    @Bean("requestInterceptor")
    public RequestInterceptor requestInterceptor(){
        return new RequestInterceptor(){
            @Override
            public void apply(RequestTemplate requestTemplate) {
                //1、RequestContextHolder拿到刚进来的请求
                ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                HttpServletRequest request = attributes.getRequest();//老请求
                //同步请求头数据。Cookie
                String cookie = request.getHeader("Cookie");
                //给新请求同步了老请求的cookie
                requestTemplate.header("Cookie",cookie);
                System.out.println("feign远程之前先执行RequestInterceptor.apply()");
            }
        };
    }
}

6、Feign异步调用丢失请求头问题

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

  • 查询购物项、库存和收货地址都要调用远程服务,串行会浪费大量时间,因此我们使用CompletableFuture进行异步编排
  • 由于RequestContextHolder使用ThreadLocal共享数据,所以在开启异步时获取不到老请求的信息,自然也就无法共享cookie了。在这种情况下,我们需要在开启异步的时候将老请求的RequestContextHolder的数据设置进去

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

    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        MemberResponseVO memberResponseVO = LoginUserInterceptor.loginUser.get();
        System.out.println("主线程..."+Thread.currentThread().getId());
        //获取之前的请求
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        //异步任务编排
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            //1、远程查询所有的收货地址列表
            System.out.println("member线程..."+Thread.currentThread().getId());
            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVO.getId());
            confirmVo.setAddress(address);
        }, executor);

        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            //2、远程查询购物车所有选中的购物项
            System.out.println("cart线程..."+Thread.currentThread().getId());
            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(items);
            //feign在远程调用之前要构造请求,调用很多拦截器RequestInterceptor interceptor: requestInterceptors
        }, executor);


        //3、查询用户积分
        Integer integration = memberResponseVO.getIntegration();
        confirmVo.setIntegration(integration);

        //4、其他数据自动计算

        //5、TODO 防重令牌想·
        CompletableFuture.allOf(getAddressFuture,cartFuture).get();
        return confirmVo;
    }

7、页面调整

		<!--主体部分-->
		<p class="p1">填写并核对订单信息</p>
		<div class="section">
			<!--收货人信息-->
			<div class="top-2">
				<span>收货人信息</span>
				<span>新增收货地址</span>
			</div>

			<!--地址-->
			<div class="top-3" th:each="addr:${orderConfirmData.address}">
				<p>[[${addr.name}]]</p><span>[[${addr.name}]] [[${addr.province}]]  [[${addr.city}]] [[${addr.detailAddress}]] [[${addr.phone}]]</span>
			</div>
			<p class="p2">更多地址︾</p>
			<div class="hh1"/></div>
		<!--********************************************************************************************-->
		<!--谷粒学院自提-->
		<div class="top-4">
			<p>谷粒学院自提</p>
			<p>省运费·无续重·随时取</p>
			<p class="xiang">详情</p>
		</div>

		<!--地址-->


		<!--支付方式-->
		<h4 class="h4">支付方式</h4>

		<div class="top-6">
			<p>货到付款</p>
			<p><span>惠</span>在线支付</p>
		</div>
		<div class="hh1"></div>
		<!--送货清单-->
		<h4 class="h4" style="margin-top: 5px;">送货清单</h4>
		<div class="top_1">
			<div class="to_left">
				<h5><span class="peisong">配送方式</span><span class="dui"><img src="/static/order/confirm/img/i_03.png"/> 对应商品</span></h5>
				<div class="box">
					谷粒学院快递
				</div>
				<p class="biao">
					<span class="til">标 准 达 :</span>
					<span class="con">预计 12月16日[今天] 15:00-19:00 送达</span>
					<a href="/static/order/confirm/#">修改</a>
				</p>
				<div class="updata-1">
					<img src="/static/order/confirm/img/im_06.png" />
					<span>京准达 标准达</span>
					<span style="color: black;"> 配送服务全面升级</span>
				</div>
				<div class="hh1"></div>
				<p class="tui">
					<span class="til">退换无忧:</span>
					<span class="con">
							<input type="checkbox" />
							自签收后7天内退货,15天内换<span style="font-size: 12px;margin-left: 5px"> ¥ 0.50</span><br />
						<span class="nul">货,</span>可享1次上门取件服务 ﹀
						</span>

				<div class="updata-2">
					<img src="/static/order/confirm/img/im_11.png" />
					<span>京准达运费大促(限自营中小件)</span>
				</div>

				</p>
				<p class="kg" style="color:#666666;margin-top: 13px;font-size: 12px">总重量 :<span style="color:#999999;font-size: 12px">0.095kg</span></p>
			</div>
			<div class="to_right">
				<h5>商家:谷粒学院自营</h5>
				<div><button>换购</button><span>已购满20.00元,再加49.90元,可返回购物车领取赠品</span></div>
				<!--图片-->
				<div class="yun1" th:each="item:${orderConfirmData.items}">
					<img style="width: 150px;height: 100px;" th:src="${item.image}" class="yun"/>
					<div class="mi">
						<p>[[${item.title}]] <span style="color: red;"> ¥[[${#numbers.formatDecimal(item.price, 1, 2)}]]</span> <span> x[[${item.count}]]</span> <span>[[${item.hasStock?"有货":"无货"}]]</span></p>
						<p><span>0.095kg</span></p>
						<p class="tui-1"><img src="/static/order/confirm/img/i_07.png" />支持7天无理由退货</p>
					</div>
				</div>

				<div class="hh1"></div>
				<p>退换无忧 <span class="money">¥ 0.00</span></p>
			</div>
		</div>
		<div class="bto">
			<div class="hh2"></div>
			<h4 class="float">发票信息</h4>
			<div class="note float"><img src="/static/order/confirm/img/i_11.png" /> <span>开企业抬头发票须填写纳税人识别号,以免影响报销</span></div>
			<ul style="clear: both;">
				<li>电子普通发票 <img src="/static/order/confirm/img/i_14.png" /></li>
				<li>个人</li>
				<li>商品明细</li>
				<li>
					<a href="/static/order/confirm/">修改</a>
				</li>
			</ul>
			<div class="hh3"></div>
			<h4 class="clear">使用优惠/礼品卡/抵用 ^</h4>
			<ul>
				<li class="red">优惠卡</li>
				<li>礼品卡</li>
				<li>京豆</li>
				<li>余额</li>
				<li>领奖码</li>
			</ul>
			<div class="tuijian clear">
				<input type="checkbox" />
				<span>优惠组合推荐</span>
			</div>
		</div>
		<div class="xia">
			<div class="qian">
				<p class="qian_y">
					<span>[[${orderConfirmData.count}]]</span>
					<span>件商品,总商品金额:</span>
					<span class="rmb">¥[[${#numbers.formatDecimal(orderConfirmData.total, 1, 2)}]]</span>
				</p>
				<p class="qian_y">
					<span>返现:</span>
					<span class="rmb">  -¥0.00</span>
				</p>
				<p class="qian_y">
					<span>运费: </span>
					<span class="rmb"> &nbsp ¥0.00</span>
				</p>
				<p class="qian_y">
					<span>服务费: </span>
					<span class="rmb"> &nbsp ¥0.00</span>
				</p>
				<p class="qian_y">
					<span>退换无忧: </span>
					<span class="rmb"> &nbsp ¥0.00</span>
				</p>

			</div>

			<div class="yfze">
				<p class="yfze_a"><span class="z">应付总额:</span><span class="hq">¥[[${#numbers.formatDecimal(orderConfirmData.payPrice, 1, 2)}]]</span></p>
				<p class="yfze_b">寄送至: 北京 朝阳区 三环到四环之间 朝阳北路复兴国际大厦23层麦田房产 IT-中心研发二部 收货人:赵存权 188****5052</p>
			</div>
			<button class="tijiao">提交订单</button>
		</div>
		</div>

http://order.gulimall.com/toTrade

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

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

8、订单确认页库存查询

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

 @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        MemberResponseVO memberResponseVO = LoginUserInterceptor.loginUser.get();
        System.out.println("主线程..."+Thread.currentThread().getId());
        //获取之前的请求
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        //异步任务编排
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            //1、远程查询所有的收货地址列表
            System.out.println("member线程..."+Thread.currentThread().getId());
            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVO.getId());
            confirmVo.setAddress(address);
        }, executor);

        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            //2、远程查询购物车所有选中的购物项
            System.out.println("cart线程..."+Thread.currentThread().getId());
            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(items);
            //feign在远程调用之前要构造请求,调用很多拦截器RequestInterceptor interceptor: requestInterceptors
        }, executor).thenRunAsync(()->{
            //查询库存信息
            List<OrderItemVo> items = confirmVo.getItems();
            List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());

            R hasStock = wmsFeignService.getSkusHasStock(collect);
            List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() {
            });
            if (data != null){
                Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                confirmVo.setStocks(map);
            }

        },executor);

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

@Data
public class SkuStockVo {
    private Long skuId;
    private Boolean hasStock;
}

修改“com.atguigu.gulimall.order.vo.OrderConfirmVo”类,添加stocks参数,并添加getter,setter方法

谷粒商城-个人笔记(高级篇四) 修改“com.atguigu.gulimall.order.vo.OrderItemVo”类,去掉hasStock属性

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

//远程调用库存,查询是否有库存gulimall-ware

添加“com.atguigu.gulimall.order.feign.WmsFeignService”类,代码如下

@FeignClient("gulimall-ware")
public interface WmsFeignService {

    //查询sku是否有库存
    @PostMapping("/ware/waresku/hasStock")
    public R getSkusHasStock(@RequestBody List<Long> skuIds);
}

修改confirm.html页面

<!--图片-->
				<div class="yun1" th:each="item:${orderConfirmData.items}">
					<img style="width: 150px;height: 100px;" th:src="${item.image}" class="yun"/>
					<div class="mi">
						<p>[[${item.title}]] <span style="color: red;"> ¥[[${#numbers.formatDecimal(item.price, 1, 2)}]]</span> <span> x[[${item.count}]]</span> <span>[[${orderConfirmData.stocks[item.skuId]?"有货":"无货"}]]</span></p>
						<p><span>0.095kg</span></p>
						<p class="tui-1"><img src="/static/order/confirm/img/i_07.png" />支持7天无理由退货</p>
					</div>
				</div>

9、订单确认页模拟运费效果

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

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

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

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

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

添加“com.atguigu.gulimall.ware.vo.FareVo”类,代码如下

@Data
public class FareVo {
    //收货人地址信息
    private MemberAddressVo address;
    //费用
    private BigDecimal fare;
}

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

    @GetMapping("/fare")
    public R getFare(@RequestParam("addrId") Long addrId){
        FareVo fare = wareInfoService.getFare(addrId);
        return R.ok().setData(fare);
    }

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

     /**
     * 根据用户的收获地址计算运费
     * @param attrId
     * @return
     */
    FareVo getFare(Long attrId);

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

    @Override
    public FareVo getFare(Long attrId) {

        FareVo fareVo = new FareVo();
        R r = memberFeignService.addrInfo(attrId);
        MemberAddressVo data = r.getData("memberReceiveAddress",new TypeReference<MemberAddressVo>() {
        });
        if (data != null){
            //模拟计算运费
            String phone = data.getPhone();
            String substring = phone.substring(phone.length() - 1, phone.length());
            BigDecimal bigDecimal = new BigDecimal(substring);
            fareVo.setAddress(data);
            fareVo.setFare(bigDecimal);

            return fareVo;
        }
        return null;
    }

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

@Data
public class MemberAddressVo {
    private Long id;
    /**
     * member_id
     */
    private Long memberId;
    /**
     * 收货人姓名
     */
    private String name;
    /**
     * 电话
     */
    private String phone;
    /**
     * 邮政编码
     */
    private String postCode;
    /**
     * 省份/直辖市
     */
    private String province;
    /**
     * 城市
     */
    private String city;
    /**
     * 区
     */
    private String region;
    /**
     * 详细地址(街道)
     */
    private String detailAddress;
    /**
     * 省市区代码
     */
    private String areacode;
    /**
     * 是否默认
     */
    private Integer defaultStatus;
}

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

@FeignClient("gulimall-member")
public interface MemberFeignService {

    @RequestMapping("/member/memberreceiveaddress/info/{id}")
    R addrInfo(@PathVariable("id") Long id);
}

10、接口幂等性讨论

1)、什么是接口幂等性

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

2)、哪些情况需要防止

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

3)、幂等性解决方案

1、token机制

2、各种锁机制

3、各种唯一性约束

4、防重表

5、全球请求唯一id

11、添加防重令牌

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

    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        MemberResponseVO memberResponseVO = LoginUserInterceptor.loginUser.get();
        System.out.println("主线程..."+Thread.currentThread().getId());
        //获取之前的请求
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        //异步任务编排
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            //1、远程查询所有的收货地址列表
            System.out.println("member线程..."+Thread.currentThread().getId());
            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVO.getId());
            confirmVo.setAddress(address);
        }, executor);

        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            //2、远程查询购物车所有选中的购物项
            System.out.println("cart线程..."+Thread.currentThread().getId());
            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(items);
            //feign在远程调用之前要构造请求,调用很多拦截器RequestInterceptor interceptor: requestInterceptors
        }, executor).thenRunAsync(()->{
            //查询库存信息
            List<OrderItemVo> items = confirmVo.getItems();
            List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());

            R hasStock = wmsFeignService.getSkusHasStock(collect);
            List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() {
            });
            if (data != null){
                Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                confirmVo.setStocks(map);
            }

        },executor);


        //3、查询用户积分
        Integer integration = memberResponseVO.getIntegration();
        confirmVo.setIntegration(integration);

        //4、其他数据自动计算

        //5、TODO 防重令牌
        String token = UUID.randomUUID().toString().replace("-", "");
        redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX+memberResponseVO.getId(),token,30, TimeUnit.MINUTES);
        confirmVo.setOrderToken(token);
        CompletableFuture.allOf(getAddressFuture,cartFuture).get();
        return confirmVo;
    }

12、订单提交

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

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

confirm.html

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

页面提交数据  添加“com.atguigu.gulimall.order.vo.OrderSubmitVo”类,代码如下:

/**
 * @Description: 封装订单提交的数据
 * @Author: WangTianShun
 * @Date: 2020/11/26 22:27
 * @Version 1.0
 */
@Data
public class OrderSubmitVo {
    // 收获地址的id
    private Long addrId;

    // 支付方式
    private Integer payType;

    //无需提交需要购买的商品,去购物车在获取一遍

    // 防重令牌
    private String orderToken;

    // 应付价格 验价
    private BigDecimal payPrice;

    // 订单备注
    private String note;

}
  • 提交订单成功,则携带返回数据转发至支付页面
  • 提交订单失败,则携带错误信息重定向至确认页

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

    /**
     * 下单功能
     * @param submitVo
     * @param model
     * @param redirectAttributes
     * @return
     */
    @PostMapping("/submitOrder")
    public String submitOrder(OrderSubmitVo submitVo, Model model, RedirectAttributes redirectAttributes) {
        // 下单 去创建订单 验证令牌 核算价格 锁定库存
        try {
            SubmitOrderResponseVo responseVo = orderService.submitOrder(submitVo);
            if (responseVo.getCode() == 0) {
                // 下单成功到选择支付方式页面
                model.addAttribute("submitOrderResp", responseVo);
                return "pay";
            } else {
                // 订单失败返回到订单确认页面
                String msg = "下订单失败: ";
                switch (responseVo.getCode()) {
                    case 1 : msg += "订单信息过期, 请刷新后再次提交."; break;
                    case 2 : msg += "订单中的商品价格发生变化, 请刷新后再次提交."; break;
                    case 3 : msg += "库存锁定失败, 商品库存不足."; break;
                }
                redirectAttributes.addFlashAttribute("msg", msg);
                return "redirect:http://order.gulimall.com/toTrade";
            }
        } catch (Exception e) {
            if (e instanceof NoStockException) {
                String message = e.getMessage();
                redirectAttributes.addFlashAttribute("msg", message);
            }
            return "redirect:http://order.gulimall.com/toTrade";
        }
    }

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

    /**
     * 下单
     * @param submitVo
     * @return
     */
    SubmitOrderResponseVo submitOrder(OrderSubmitVo submitVo);
添加“com.atguigu.gulimall.order.vo.SubmitOrderResponseVo”类,代码如下:
@Data
public class SubmitOrderResponseVo {

    private OrderEntity order;

    private Integer code;

}

 

1、验证令牌【令牌的对比和删除必须保证原子性】
2、令牌验证成功 下单 去创建订单 验证令牌 核算价格 锁定库存

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

@Transactional
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo submitVo) {
        confirmVoThreadLocal.set(submitVo);
        SubmitOrderResponseVo response = new SubmitOrderResponseVo();
        MemberResponseVO memberResponseVO = LoginUserInterceptor.loginUser.get();
        response.setCode(0);
        // 1、验证令牌【令牌的对比和删除必须保证原子性】
        // 0令牌失败 -1删除成功
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        String orderToken = submitVo.getOrderToken();
        // 原子验证令牌和删除令牌
        Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResponseVO.getId()), orderToken);
        if (result == 0L) {
            // 令牌验证失败
            response.setCode(1);
            return response;
        } else {
            // 令牌验证成功 下单 去创建订单 验证令牌 核算价格 锁定库存
            // 1、创建订单,订单项等信息
            OrderCreateTo order = createOrder();
            // 2、验价
            BigDecimal payAmount = order.getOrder().getPayAmount();
            BigDecimal payPrice = submitVo.getPayPrice();
            if (Math.abs(payAmount.subtract(payPrice).doubleValue()) <0.01){
                // 金额对比成功
                // 3、保持订单
                saveOrder(order);
                // 4、库存锁定,只要有异常回滚订单数据。订单号,订单项信息(skuId,skuName,num)
                WareSkuLockVo wareSkuLockVo = new WareSkuLockVo();
                wareSkuLockVo.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());
                wareSkuLockVo.setLocks(orderItemVos);
                // TODO 远程锁库存
                R r = wmsFeignService.orderLockStock(wareSkuLockVo);
                if (r.getCode() == 0){
                    //锁成功了
                    response.setOrder(order.getOrder());
                    return response;
                }else {
                    //锁定失败
                    throw new NoStockException((String) r.get("msg"));
                }
            }else {
                response.setCode(2);
                return response;
            }
        }
//        String redisToken = redisTemplate.opsForValue().get(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResponseVO.getId());
//        if (orderToken != null && orderToken.equals(redisToken)){
//            //令牌验证通过
//            redisTemplate.delete(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResponseVO.getId());
//        }else {
//            //不通过
//        }
    }

订单返回参数     添加“com.atguigu.gulimall.order.to.OrderCreateTo”类,代码如下:

@Data
public class OrderCreateTo {

    // 订单
    private OrderEntity order;

    // 订单项
    private List<OrderItemEntity> orderItems;

    // 订单应付的价格
    private BigDecimal payPrice;

    //运费
    private BigDecimal fare;
}

1、生成一个订单

    /**
     * 生成一个订单
     * @return
     */
    public OrderCreateTo createOrder(){
        OrderCreateTo createTo = new OrderCreateTo();
        // 1、生成一个订单号
        String orderSn = IdWorker.getTimeId();
        // 创建订单
        OrderEntity orderEntity = buildOrder(orderSn);
        createTo.setOrder(orderEntity);
        // 2、获取所有的订单项
        List<OrderItemEntity> itemEntities = buildOrderItems(orderSn);
        createTo.setOrderItems(itemEntities);
        // 3、计算价格、积分等相关
        computePrice(orderEntity,itemEntities);
        return createTo;
    }

 1.1、创建订单

    /**
     * 创建订单
     * @param orderSn
     * @return
     */
    private OrderEntity buildOrder(String orderSn) {
        MemberResponseVO memberResponseVo = LoginUserInterceptor.loginUser.get();
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setOrderSn(orderSn);
        orderEntity.setMemberId(memberResponseVo.getId());
        OrderSubmitVo orderSubmitVo = confirmVoThreadLocal.get();
        // 获取收获地址信息
        R r = wmsFeignService.getFare(orderSubmitVo.getAddrId());
        FareVo fareResp = r.getData(new TypeReference<FareVo>() {
        });
        // 设置运费信息
        orderEntity.setFreightAmount(fareResp.getFare());
        // 设置收货人信息
        orderEntity.setReceiverCity(fareResp.getAddress().getCity());
        orderEntity.setReceiverDetailAddress(fareResp.getAddress().getDetailAddress());
        orderEntity.setReceiverName(fareResp.getAddress().getName());
        orderEntity.setReceiverPhone(fareResp.getAddress().getPhone());
        orderEntity.setReceiverPostCode(fareResp.getAddress().getPostCode());
        orderEntity.setReceiverRegion(fareResp.getAddress().getRegion());
        // 设置订单的相关状态信息
        orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
        orderEntity.setAutoConfirmDay(7);
        return orderEntity;
    }

//远程获取收获地址信息(gulimall-ware)

修改“com.atguigu.gulimall.order.feign.WmsFeignService”类,代码如下:

    @GetMapping("/ware/wareinfo/fare")
    public R getFare(@RequestParam("addrId") Long addrId);

封装远程返回的数据

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

@Data
public class FareVo {
    //收货人地址信息
    private MemberAddressVo address;
    //费用
    private BigDecimal fare;
}

 1.2、获取所有的订单项

    /**
     * 构建所有订单项数据
     * @return
     */
    private List<OrderItemEntity> buildOrderItems(String orderSn) {
        // 最后确定每个购物项的价格
        List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
        if (currentUserCartItems != null && currentUserCartItems.size()>0){
            List<OrderItemEntity> itemEntities = currentUserCartItems.stream().map(cartItem -> {
                OrderItemEntity itemEntity = buildOrderItem(cartItem);
                itemEntity.setOrderSn(orderSn);
                return itemEntity;
            }).collect(Collectors.toList());
            return itemEntities;
        }
        return null;
    }

1.2.1、构建某一个订单项

    /**
     * 构建某一个订单项
     * @param cartItem
     * @return
     */
    private OrderItemEntity buildOrderItem(OrderItemVo cartItem) {
        OrderItemEntity orderItemEntity = new OrderItemEntity();
        // 1 订单信息 订单号
        // 2 SPU信息
        Long skuId = cartItem.getSkuId();
        R r = productFeignService.getSpuInfoBySkuId(skuId);
        SpuInfoVo data = r.getData(new TypeReference<SpuInfoVo>(){});
        orderItemEntity.setSpuId(data.getId());
        orderItemEntity.setSpuBrand(data.getBrandId().toString());
        orderItemEntity.setSpuName(data.getSpuName());
        orderItemEntity.setCategoryId(data.getCatalogId());
        // 3 SKU信息
        orderItemEntity.setSkuId(cartItem.getSkuId());
        orderItemEntity.setSkuName(cartItem.getTitle());
        orderItemEntity.setSkuPic(cartItem.getImage());
        orderItemEntity.setSkuPrice(cartItem.getPrice());
        String skuAttrs = StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(), ";"); //将集合转换成字符串
        orderItemEntity.setSkuAttrsVals(skuAttrs);
        orderItemEntity.setSkuQuantity(cartItem.getCount());
        // 4 优惠信息 [不做]

        // 5 积分信息
        orderItemEntity.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
        orderItemEntity.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
        // 6 订单项的价格信息
        orderItemEntity.setPromotionAmount(new BigDecimal("0"));
        orderItemEntity.setIntegrationAmount(new BigDecimal("0"));
        orderItemEntity.setCouponAmount(new BigDecimal("0"));
        // 当前订单项的实际金额
        BigDecimal origin = orderItemEntity.getSkuPrice().multiply(new BigDecimal(orderItemEntity.getSkuQuantity().toString()));
        // 总额减去各种优惠后的价格
        BigDecimal subtract = origin.subtract(orderItemEntity.getCouponAmount()).subtract(orderItemEntity.getIntegrationAmount()).subtract(orderItemEntity.getPromotionAmount());
        orderItemEntity.setRealAmount(subtract);
        return orderItemEntity;
    }

//远程获取spu信息

封装远程spu的返回信息   添加“com.atguigu.gulimall.order.vo.SpuInfoVo”类,代码如下

@Data
public class SpuInfoVo {
    /**
     * 商品id
     */
    private Long id;
    /**
     * 商品名称
     */
    private String spuName;
    /**
     * 商品描述
     */
    private String spuDescription;
    /**
     * 所属分类id
     */
    private Long catalogId;
    /**
     * 品牌id
     */
    private Long brandId;
    /**
     *
     */
    private BigDecimal weight;
    /**
     * 上架状态[0 - 下架,1 - 上架]
     */
    private Integer publishStatus;
    /**
     *
     */
    private Date createTime;
    /**
     *
     */
    private Date updateTime;
}

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

@FeignClient("gulimall-product")
public interface ProductFeignService {
    @GetMapping("/product/spuinfo/skuId/{id}")
    R getSpuInfoBySkuId(@PathVariable("id") Long skuId);
}

gulimall-product

修改“com.atguigu.gulimall.product.app.SpuInfoController”类,代码如下:

    @GetMapping("skuId/{id}")
    public R getSpuInfoBySkuId(@PathVariable("id") Long skuId){
        SpuInfoEntity entity = spuInfoService.getSpuInfoBySkuId(skuId);
        return R.ok().setData(entity);
    }

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

 SpuInfoEntity getSpuInfoBySkuId(Long skuId);

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

    @Override
    public SpuInfoEntity getSpuInfoBySkuId(Long skuId) {
        SkuInfoEntity byId = skuInfoService.getById(skuId);
        Long spuId = byId.getSpuId();
        SpuInfoEntity spuInfoEntity = getById(spuId);

        return spuInfoEntity;
    }

1.3、计算价格、积分等相关

 

    /**
     * 计算价格相关
     * @param orderEntity
     * @param itemEntities
     */
    private void computePrice(OrderEntity orderEntity, List<OrderItemEntity> itemEntities) {
        BigDecimal total = new BigDecimal("0.0");
        BigDecimal coupon = new BigDecimal("0.0");
        BigDecimal integration = new BigDecimal("0.0");
        BigDecimal promotion = new BigDecimal("0.0");
        BigDecimal gift = new BigDecimal("0.0");
        BigDecimal growth = new BigDecimal("0.0");
        // 订单的总额,叠加每一个订单项的总额信息。
        for (OrderItemEntity entity : itemEntities) {
            coupon = coupon.add(entity.getCouponAmount());
            integration = integration.add(entity.getIntegrationAmount());
            promotion = promotion.add(entity.getPromotionAmount());
            total = total.add(entity.getRealAmount());
            gift = gift.add(new BigDecimal(entity.getGiftIntegration().toString()));
            growth = growth.add(new BigDecimal(entity.getGiftGrowth().toString()));
        }
        // 订单价格相关
        orderEntity.setTotalAmount(total);
        // 应付金额
        orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));
        orderEntity.setPromotionAmount(promotion);
        orderEntity.setIntegrationAmount(integration);
        orderEntity.setCouponAmount(coupon);
        // 设置积分信息
        orderEntity.setIntegration(gift.intValue());
        orderEntity.setGrowth(growth.intValue());
        // 设置删除状态 0未删除
        orderEntity.setDeleteStatus(0);
    }

 2、保存订单

    /**
     * 保存订单数据
     * @param order
     */
    private void saveOrder(OrderCreateTo order) {
        OrderEntity orderEntity = order.getOrder();
        orderEntity.setModifyTime(new Date());
        this.save(orderEntity);
        List<OrderItemEntity> orderItems = order.getOrderItems();
        orderItemService.saveBatch(orderItems);
    }

3、库存锁定,只要有异常回滚订单数据。订单号,订单项信息(skuId,skuName,num)

  • 找出所有库存大于商品数的仓库
  • 遍历所有满足条件的仓库,逐个尝试锁库存,若锁库存成功则退出遍历

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

修改“com.atguigu.gulimall.order.feign.WmsFeignService”类,代码如下:

    @PostMapping("/ware/waresku/lock/order")
    public R orderLockStock(@RequestBody WareSkuLockVo vo);

gulimall-ware

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

    @PostMapping("/lock/order")
    public R orderLockStock(@RequestBody WareSkuLockVo vo){
        try{
            Boolean stock = wareSkuService.orderLockStock(vo);
            return R.ok();
        }catch (NoStockException e){
            return R.error(BizCodeEnume.NO_STOCK_EXCEPTION.getCode(),BizCodeEnume.NO_STOCK_EXCEPTION.getMsg());
        }
    }

 添加“com.atguigu.common.exception.BizCodeEnume”类,代码如下:

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

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

Boolean orderLockStock(WareSkuLockVo vo);

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

    /**
     * 为某个订单锁定库存
     * (rollbackFor = NoStockException.class)
     * 默认只要都是运行异常都会回滚
     * @param vo
     * @return
     */
    @Transactional
    @Override
    public Boolean orderLockStock(WareSkuLockVo vo) {
        // 1、按照下单的收货地址,找到一个就近仓库,锁定库存

        // 1、找到每个商品在哪个仓库都有库存
        List<OrderItemVo> locks = vo.getLocks();
        List<SkuWareHasStock> collect = locks.stream().map(item -> {
            SkuWareHasStock stock = new SkuWareHasStock();
            Long skuId = item.getSkuId();
            stock.setSkuId(skuId);
            stock.setNum(item.getCount());
            //查询这个商品在哪个仓库有库存
            List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId);
            stock.setWareId(wareIds);
            return stock;
        }).collect(Collectors.toList());
        // 2、锁定库存
        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);
            }
            for (Long wareId : wareIds) {
                //成功返回1;否则就是0
                Long count = wareSkuDao.lockSkuStock(skuId,wareId,hasStock.getNum());
                if (count == 1){
                    skuStocked = true;
                    break;
                    //当仓库锁失败,重试下一个仓库
                }
            }
            if (skuStocked == false){
                //当前商品所有仓库都没有锁住
                throw new NoStockException(skuId);
            }
        }
        // 3、肯定全部都是锁定成功的
        return true;
    }

    @Data
    class SkuWareHasStock{

        private Long skuId;

        private Integer num;

        private List<Long> wareId;
    }

}

这里通过异常机制控制事务回滚,如果在锁定库存失败则抛出NoStockExceptions,订单服务和库存服务都会回滚。 

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

    List<Long> listWareIdHasSkuStock(@Param("skuId") Long skuId);

    Long lockSkuStock(@Param("skuId") Long skuId, @Param("wareId") Long wareId, @Param("num") Integer num);
    <select id="listWareIdHasSkuStock" resultType="java.lang.Long">
        select ware_id from wms_ware_sku where sku_id =#{skuId} and stock - stock_locked > 0
    </select>

    <update id="lockSkuStock">
        update wms_ware_sku set stock_locked = stock_locked + #{num}
        where sku_id = #{skuId} and ware_id = #{wareId} and stock - stock_locked >= #{num}
    </update>

 13、分布式事务

本地事务

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

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

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

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

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

分布式事务

分布式情况下,可能出现一些服务事务不一致的情况

  • 远程服务假失败
  • 远程服务执行完成后,下面其他方法出现异常

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

分布式CAP&BASE理论

1)、cap定理

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

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

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

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

14、分布式事务常见解决方案

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

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

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

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

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

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

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

 

15、Seata

1)、概念

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

2)、Seata术语

  • TC (Transaction Coordinator) - 事务协调者

      维护全局和分支事务的状态,驱动全局事务提交或回滚。

  • TM (Transaction Manager) - 事务管理器

       定义全局事务的范围:开始全局事务、提交或回滚全局事务。

  • RM (Resource Manager) - 资源管理器

      管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

3)、SEATA 的分布式交易解决方案

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

我们只需要使用一个 @GlobalTransactional 注解在业务方法上:


    @GlobalTransactional
    public void purchase(String userId, String commodityCode, int orderCount) {
        ......
    }

4)、seata控制分布式事务需要

* Seata控制分布式事务
* 1)、每一个微服务先必须创建undo_logo;
* 2)、安装事务协调器;seata-server: https://github.com/seata/seata/releases
* 3)、整合
*      1、导入依赖  spring-cloud-starter-alibaba-seata  seata-all-1.0.0.jar
*      2、解压并启动seata-server
*          registry.conf注册中心相关的配置,修改registry type=nacos
*          file.conf
*      3、所有想要用到分布式事务的微服务使用seata DatasourceProxy代理自己的数据源
*      4、每个微服务,都必须导入registry.cof
*      file.conf  vgroup_mapping.{application.name}-fescar-service-group = "default"
*      5、启动测试
*      6、给分布式大事务的路口标注@GlobalTransactional
*      7、每一个远程的小事务用 @Transactional

5)、每个微服务数据库加上undo_log(回滚日志表)

CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

因为linux出现问题,时间紧迫没处理,现在各个数据服务器连接不起来 ,只是跟着老师敲,没法展示

6)、环境搭建

https://github.com/seata/seata/releases,下载服务器软件包,将其解压缩。

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

下载senta-server-1.0.0并修改register.conf,使用nacos作为注册中心(这里根据自己maven版本下载)

修改registry.conf,把nacos作为seata的注册中

添加“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);
    }
}

gulimall-ware

修改“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

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

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

 @GlobalTransactional
    @Transactional
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo submitVo) 

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

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

16、RabbitMQ延时队列(实现定时任务)

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

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

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

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

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

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

@Configuration
public class MyRabbitConfig {
    @Autowired
    RabbitTemplate rabbitTemplate;

    /**
     * 使用JSON序列化机制,进行消息转换
     * @return
     */
    @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() {
        // String name, boolean durable, boolean exclusive, boolean autoDelete,
        //			@Nullable Map<String, Object> arguments
        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);
        return new Queue("stock.delay.queue", true, false, false, arguments);
    }

    @Bean
    public Binding stockReleaseStockBinding() {
        return new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.release.#",
                new HashMap<>());
    }

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

修改“com.atguigu.gulimall.order.web.HelloController”类代码如下:

    @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";
    }

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

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

17、库存自动解锁

gulimall-ware  服务添加RabbitMQ

导入依赖

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

添加配置

spring.rabbitmq.host=172.20.10.9
spring.rabbitmq.virtual-host=/

主启动类添加注解

@EnableRabbit
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallWareApplication {

    public static void main(String[] args) {
        SpringApplication.run(GulimallWareApplication.class, args);
    }

}

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

@Configuration
public class MyRabbitConfig {
    @Autowired
    RabbitTemplate rabbitTemplate;

    /**
     * 使用JSON序列化机制,进行消息转换
     * @return
     */
    @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() {
        // String name, boolean durable, boolean exclusive, boolean autoDelete,
        //			@Nullable Map<String, Object> arguments
        Map<String, Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange", "stock-event-exchange");
        arguments.put("x-dead-letter-routing-key", "order.release");
        arguments.put("x-message-ttl", 120000);
        return new Queue("stock.delay.queue", true, false, false, arguments);
    }

    @Bean
    public Binding stockReleaseStockBinding() {
        return new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.release.#",
                new HashMap<>());
    }

    @Bean
    public Binding orderLockedBinding() {
        return new Binding("stock.delay.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.locked",
                new HashMap<>());
    }
}

修改表wms_ware_order_task_detail

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

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

@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;
}

修改WareOrderTaskDetailDao.xml

<resultMap type="com.atguigu.gulimall.ware.entity.WareOrderTaskDetailEntity" id="wareOrderTaskDetailMap">
        <result property="id" column="id"/>
        <result property="skuId" column="sku_id"/>
        <result property="skuName" column="sku_name"/>
        <result property="skuNum" column="sku_num"/>
        <result property="taskId" column="task_id"/>
        <result property="wareId" column="ware_id"/>
        <result property="lockStatus" column="lock_status"/>
    </resultMap>

 

1)、库存锁定

在库存锁定是添加以下逻辑

  • 由于可能订单回滚的情况,所以为了能够得到库存锁定的信息,在锁定时需要记录库存工作单,其中包括订单信息和锁定库存时的信息(仓库id,商品id,锁了几件...)
  • 在锁定成功后,向延迟队列发消息,带上库存锁定的相关信息

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

   @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 {
                    //锁定成功,保存工作单详情
                    WareOrderTaskDetailEntity detailEntity = WareOrderTaskDetailEntity.builder()
                            .skuId(skuId)
                            .skuName("")
                            .skuNum(lockVo.getNum())
                            .taskId(taskEntity.getId())
                            .wareId(wareId)
                            .lockStatus(1).build();
                    wareOrderTaskDetailService.save(detailEntity);
                    //发送库存锁定消息至延迟队列
                    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);

                    lock = true;
                    break;
                }
            }
        }
        if (!lock) throw new NoStockException(skuId);
    }
    return true;
}

 

2)、监听队列

  • 延迟队列会将过期的消息路由至"stock.release.stock.queue",通过监听该队列实现库存的解锁
  • 为保证消息的可靠到达,我们使用手动确认消息的模式,在解锁成功后确认消息,若出现异常则重新归队 
@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);
        }
    }
}

3)、库存解锁

  • 如果工作单详情不为空,说明该库存锁定成功
    • 查询最新的订单状态,如果订单不存在,说明订单提交出现异常回滚,或者订单处于已取消的状态,我们都对已锁定的库存进行解锁
  • 如果工作单详情为空,说明库存未锁定,自然无需解锁
  • 为保证幂等性,我们分别对订单的状态和工作单的状态都进行了判断,只有当订单过期且工作单显示当前库存处于锁定的状态时,才进行库存的解锁
@Override
    public void unlock(StockLockedTo stockLockedTo) {
        StockDetailTo detailTo = stockLockedTo.getDetailTo();
        WareOrderTaskDetailEntity detailEntity = wareOrderTaskDetailService.getById(detailTo.getId());
        //1.如果工作单详情不为空,说明该库存锁定成功
        if (detailEntity != null) {
            WareOrderTaskEntity taskEntity = wareOrderTaskService.getById(stockLockedTo.getId());
            R r = orderFeignService.infoByOrderSn(taskEntity.getOrderSn());
            if (r.getCode() == 0) {
                OrderTo order = r.getData("order", new TypeReference<OrderTo>() {
                });
                //没有这个订单||订单状态已经取消 解锁库存
                if (order == null||order.getStatus()== OrderStatusEnum.CANCLED.getCode()) {
                    //为保证幂等性,只有当工作单详情处于被锁定的情况下才进行解锁
                    if (detailEntity.getLockStatus()== WareTaskStatusEnum.Locked.getCode()){
                        unlockStock(detailTo.getSkuId(), detailTo.getSkuNum(), detailTo.getWareId(), detailEntity.getId());
                    }
                }
            }else {
                throw new RuntimeException("远程调用订单服务失败");
            }
        }else {
            //无需解锁
        }
    }

远程查询订单状态  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.OrderService”类,代码如下:

   OrderEntity getOrderByOrderSn(String orderSn);

修改“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;
    }

修改“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>

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

@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 {
        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;
        }
    }
}

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

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

18、定时关单

1)、提交订单

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

/**
     * 下单功能
     * @param submitVo
     * @param model
     * @param redirectAttributes
     * @return
     */
    @PostMapping("/submitOrder")
    public String submitOrder(OrderSubmitVo submitVo, Model model, RedirectAttributes redirectAttributes) {
        // 下单 去创建订单 验证令牌 核算价格 锁定库存
        try {
            SubmitOrderResponseVo responseVo = orderService.submitOrder(submitVo);
            System.out.println("============================="+responseVo.getCode());
            if (responseVo.getCode() == 0) {
                // 下单成功到选择支付方式页面
                model.addAttribute("submitOrderResp", responseVo);
                return "pay";
            } else {
                // 订单失败返回到订单确认页面
                String msg = "下订单失败: ";
                switch (responseVo.getCode()) {
                    case 1 : msg += "订单信息过期, 请刷新后再次提交."; break;
                    case 2 : msg += "订单中的商品价格发生变化, 请刷新后再次提交."; break;
                    case 3 : msg += "库存锁定失败, 商品库存不足."; break;
                }
                redirectAttributes.addFlashAttribute("msg", msg);
                return "redirect:http://order.gulimall.com/toTrade";
            }
        } catch (Exception e) {
            if (e instanceof NoStockException) {
                String message = e.getMessage();
                redirectAttributes.addFlashAttribute("msg", message);
            }
            return "redirect:http://order.gulimall.com/toTrade";
        }

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);
            OrderTo orderTo = new OrderTo();
            BeanUtils.copyProperties(orderEntity,orderTo);
            //发给MQ一个
            rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
        }
    }

添加“com.atguigu.common.to.mq.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;

    /**
     * 1、库存自动解锁
     * 库存解锁的场景
     *     1)、下订单成功,库存锁定成功,接下来的业务调用失效,导致订单回滚。之前锁定的库存就要自动回滚
     *     2)、订单失败
     *     锁库存失败
     *
     * 只要解锁库存的消息失败。一定澳告诉服务解锁失败。
     *
     */
    @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);
        }
    }

    @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”类,代码如下:

     /**
     * 防止订单服务卡顿,导致订单状态一直改变不了,库存消息优先到期,查订单状态新建状态,什么都不做就走了
     * 导致卡顿的订单,永远不能解锁库存
     * @param to
     */
    @Transactional
    @Override
    public void unLockStockForOrder(OrderTo to) {
        String orderSn = to.getOrderSn();
        //查一下最新的库存解锁状态,防止重复解锁库存
        R r = orderFeignService.getOrderStatus(orderSn);
        WareOrderTaskEntity task = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);
        Long id = task.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());
            
        }
    }

修改“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;
    }

总结:

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

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

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

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

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

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

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

@Configuration
public class MyRabbitConfig {
    @Autowired
    RabbitTemplate rabbitTemplate;

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

    
    @PostConstruct   //MyRabbitConfig对象创建完以后,执行这个方法
    public void initRabbitTemplate(){
        //设置确认回调
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             * 只要消息抵达Broker就b = true
             * @param correlationData 当前消息的唯一关联数据(这个消息的唯一id)
             * @param ack  消息是否成功收到
             * @param s 失败的原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String s) {
                /**
                 *1、做好消息确认机制(publisher,consumer【手动ack】)
                 * 2、每一个发送的消息都在数据库做好记录。定期将失效的消息再次发送
                 */
                //服务器收到了
                System.out.println("confirm...correlationData["+correlationData+"]==>ack["+ack+"]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+"]");
            }
        });
    }
}

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

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

 

 

 

 

 

上一篇:注意:6月PMP考试缴费今天开始!


下一篇:POJ 3254 Corn Fields(状压DP)