day03_《谷粒商城》的完整流程(详细版二)

文章目录

  • 笔记链接:
  • P247—P260 RabbitMQ的知识
  • P261—P263 订单服务—环境搭建
  • P264 订单确认页—登录拦截
  • P265 订单确认页—数据获取1
  • P266 订单确认页—数据获取2
  • P267 订单确认页—Feign远程调用丢失请求头问题
      • 1.总说:
      • 2.代码:
  • P268 订单确认页—Feign异步调用丢失请求头问题
  • P269—P270 前台代码调整
  • P271 订单确认页—库存查询
  • P272 订单确认页—模拟运费效果1
  • P273 订单确认页—模拟运费效果2
  • P274 订单确认页—接口幂等性讨论
  • P275 订单确认页—添加防重令牌token
  • P275—P282 提交订单页—完成订单提交
      • 1.说明
      • 2.本节内容总说
      • 3.`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—P293 “可靠消息+最终一致性”的实现——添加RabbitMQ
      • 1.创建gulimall-order里的RabbitMQ
      • 2.创建gulimall-ware的RabbitMQ
  • P294 “可靠消息+最终一致性”的实现—锁定库存
      • 1.锁定库存
  • P295 “可靠消息+最终一致性”的实现—库存自动解锁1
  • P296 “可靠消息+最终一致性”的实现—库存自动解锁2
  • P297 “可靠消息+最终一致性”的实现—库存自动解锁3
      • 1.库存自动解锁
      • 2.测试
  • P298 “可靠消息+最终一致性”的实现—定时关单
  • P299 “可靠消息+最终一致性”的实现—保证消息的可靠
      • 1.消息丢失问题:
      • 2.消息重复问题
      • 3.消息积压问题
  • p300—p305 支付与订单—整合支付宝沙箱
      • 1)、配置支付宝沙箱环境
      • 2)、订单支付
      • 3)、整合gulimall-member
      • 4)、测试
  • P306 支付与订单—订单页面的展示
  • P307 支付与订单—支付宝异步通知1
  • P308 支付与订单—支付宝异步通知2
  • P309 支付与订单—收单
  • P310 秒杀服务—后台添加秒杀商品
  • P311 秒杀服务—环境搭建
  • P312 秒杀服务—SpringBoot整合定时任务
  • P313 秒杀服务—商品上架1
  • P314 秒杀服务—商品上架2
  • P315 秒杀服务—商品上架3
  • P316 秒杀服务—商品上架4
      • 1.三张表 + 五个实体类
      • 2.总说
      • 3.代码
  • P317 秒杀服务—商品上架5(解决幂等性问题)
  • P318 秒杀服务—查询当前可以秒杀的商品
  • P319 秒杀服务—商品详情页展示秒杀信息
  • P320 秒杀开始—理论讲解“秒杀系统设计”
  • P321 秒杀开始—登录检查
  • P322 秒杀开始—秒杀1
  • P323 秒杀开始—秒杀2
  • P324 秒杀开始—秒杀3
  • P325 Sentinel—简介
  • P326 Sentinel—Sentinel概念
  • P327 Sentinel—Sentinel流控1
  • P328 Sentinel—Sentinel流控2
  • P329 Sentinel—Sentinel流控3
  • P330 Sentinel—Sentinel流控4
      • 1.整合sentinel
      • 2.Sentinel流控演示
  • P331 Sentinel—Sentinel熔断降级
      • 1)、fegin的熔断
      • 2)、调用方指定降级策略
      • 3)、提供方指定降级策略
  • P332 Sentinel—@SentinelResource注解
  • P333 Sentinel—网关流控1
  • P334 Sentinel—网关流控2
  • P335 Sleuth—基本概念
  • P336 Sleuth的使用1
  • P337 Sleuth的使用2
      • 1.springboot整合sleuth
      • 2.Sleuth可视化界面怎么看?
  • P338 高级篇总结
  • P339 完整运行该项目

笔记链接:

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

P247—P260 RabbitMQ的知识

防止面试官问你RabbitMQ的知识,你除了把以前RabbitMQ的知识复习一遍外,你还要把雷丰阳老师在谷粒商城中的RabbitMQ也要复习一遍,二者还是有点差别的。

P261—P263 订单服务—环境搭建

除了环境搭建,还整合了SpringSession、线程池

P264 订单确认页—登录拦截

在这里插入图片描述

添加登录拦截

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

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

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

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

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

总说:
前端发来请求访问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远程调用丢失请求头问题

1.总说:

问题
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。

2.代码:

修改gulimall-order的config类

/**
 * @Description: 请求拦截器
 */
@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()");
            }
        };
    }
}

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

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.存在的问题

在这里插入图片描述

4.解决
需要在开启异步的时候将老请求的RequestContextHolder的数据设置进去

    @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—P270 前台代码调整

P271 订单确认页—库存查询

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
稍微修改gulimall-order的“OrderServiceImpl”类里面的confirmOrder()方法
在这里插入图片描述

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

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

在这里插入图片描述

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

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

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


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

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

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

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

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

这里是引用

4.幂等性的解决方法

这里是引用
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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,一定要是原子性操做。

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

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

1.说明

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

2.本节内容总说

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

当你在订单确认页点击了"提交订单",前台带着OrderSubmitVo发送请求/submitOrder方法给后台,后台submitOrder里面到底干了什么呢?

3.submitOrder()的解说

public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo)干了什么:
第一步,比较前台传过来的令牌和redis中的令牌是否一致,如果一致就继续往下执行
第二步,创建订单(创建订单时会再次从购物车里面查询用户下单的商品再次计算总价),和前端传来的价格进行比对,如果一致就继续往下执行
第三步,保存订单到数据库
第四步,锁定库存(如果用户买10件衬衣,但是没有一个仓库剩余衬衣多于10件,那就会锁定库存失败,远程调用的锁定库存方法就会回滚)
第五步,远程扣减积分

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

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. 原子性:是不可分割的最小操作单位,要么同时成功,要么同时失败。
	2. 持久性:当事务提交或回滚后,数据库会持久化的保存数据。
	3. 隔离性:多个事务之间。相互独立。
	4. 一致性:事务操作前后,数据总量不变

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

1.相关概念的解释

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

2.环境准备

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

在这里插入图片描述

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

添加“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

在这里插入图片描述

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

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

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—P293 “可靠消息+最终一致性”的实现——添加RabbitMQ

1.创建gulimall-order里的RabbitMQ

在这里插入图片描述
p是订单服务,一下单成功就给RabbitMQ发消息,经过延时队列60000ms以后到达订单的释放服务

2.创建gulimall-ware的RabbitMQ

在这里插入图片描述
图片看不懂就回看视频,视频里面讲的相当清楚。

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

1.锁定库存

1.创建三个实体类

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

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

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

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

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

在这里插入图片描述

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就不用解锁

3.测试一下

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

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

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

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

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

1.库存自动解锁

在这里插入图片描述

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

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()方法出现异常回滚,我们需要对已锁定的库存进行解锁,解锁库存调用unLockStock()方法
    • 如果订单存在,进一步查询订单状态,如果订单状态是“已取消“,说明订单过期没有支付被系统自动取消、或被用户手动取消,那么需要解锁库存,解锁库存调用unLockStock()方法。
  • 如果"wms_ware_order_task_detail"表为空,说明执行锁定库存的时候就异常回滚了,库存未锁定,自然无需解锁

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

4.解锁库存unLockStock()方法
wms_ware_sku表有个字段是stock_locked,表示已经被锁定的库存;
锁定10件库存就是把wms_ware_sku的stock_locked这个字段增加10;
解锁10件库存就是把wms_ware_sku的stock_locked这个字段减小10。

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.测试

在这里插入图片描述

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

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

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上面的图看不懂就立刻回看视频p298,视频里面用两分钟讲的清清楚楚

①修改submitOrder()方法,当它执行完锁库存、查积分等所有操作后,就给发送消息给MQ表示订单创建成功,

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

③定时关单

  • 关闭订单前要查询最新的订单状态判断是否需要关单——如果订单的状态是“新建”(说明用户没付款),那就执行关单
  • 关闭订单后也需要解锁库存,因此关闭订单后把订单数据封装发给RabbitMQ,然后交给库存解锁服务,让解锁库存

④解锁库存
* 思考:我们之前解锁库存前先看一下订单的状态,但是在这里就不用,因为订单状态肯定是"已取消"才会发消息到这里来解锁库存的。
* 那我们需要不需要查一下库存解锁状态?需要,因为万一订单系统发的解锁库存的消息晚了一步,已经被解锁了的库存就没必要再次解锁了。所以在解锁之前先确认lock_status=1再进行解锁(只有库存锁定状态是"已锁定"的才需要解锁)。

day03_《谷粒商城》的完整流程(详细版二)_第1张图片

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

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

1.消息丢失问题:

1.使用try…catch语句

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

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

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

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

2.消息重复问题

有机会再学

3.消息积压问题

有机会再学

p300—p305 支付与订单—整合支付宝沙箱

1)、配置支付宝沙箱环境

1、导入依赖

2.添加配置类:

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

@Data
public class PayVo {
     
    private String out_trade_no; // 商户订单号 必填
    private String subject; // 订单名称 必填
    private String total_amount;  // 付款金额 必填
    private String body; // 商品描述 可空
}

4.yml配置:

#支付宝相关的配置
alipay.app_id=2021000117698328
alipay.merchant_private_key=MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCmTEEsQLUp7nUo3+1oGpa+hFzDhvM1wKHBUd0aar64IXWcG4diXGfRyGmlKJYWxv8v3+P3jU0SQNXVQORQ1lmdK8ft1/EgqOAfDv0LY7zsUB/DmlljyDtnzSBgNA0sT1ZfKmjKmd5u26m5FRIBpW2Uh8Bw8mONb0eLQxFjJyQu/Yg5uJbB4rtmA9WyrPSqLwqbdYLQrOrss00SZ5AZz0dCHHMpWOaMuVOrx4b8IVoO4ShsfRGYQJ9zxV/xCJ3DgtC0cV7G5/ed5XQzJvaZrG0O7v5AVxoVvfXI7TUoZ8VMgYTqOEN7Wkv2Y+WeQbI9OkUkk/T7kp+NVrsNNi+RNTvjAgMBAAECggEAUUR0mQaqQeqZcLc10qkjv8j5eEgLtNoFcm7qKU2/FEatrfM6DxRvW/Kfxil2Z30qGiBEzKZN4ryygvuqV+LYell5476ixL4igKsXeChum+FwFGvqgTvJ5Ck3SCxHv76py+nyugfFztEkOSGV4h4Q1gQdRFT/149pHCJTbewj354WzRjoogmCMNuSNeKq69TY2OMnXv9kyWKD5hURPIjw8hoqoR4EzGiealmJgxXbf55+BEkWlJ7t2UYgO0wCSSKsBLC6DR8Z3YM2S1cmr7TyVE1e2eTp+o1+N2Oc2OoEWR855CKy3Y+xlNQs+CzjHT4oms8Cw2/2i6DzO2ic1JnI8QKBgQDXDMcQLRoEQAWSpa54Jd5ZyHe/x1/t1GBhuad/EeMO60G+0Jr47lfIYQyB7RnfSeSxQFD33keqiMbYWi7bR8+rpJy+AFshgACK5HP3SkV+0/5VLmBTfLoMJ3HoYq5GN4ywW1Vnsr6ctBYLXngbowgBe6g00N8vbpWkuWPoZ55VSwKBgQDF9utbaPjNECAlEHz1DRj/DF3pIYe5jcQyjccdMR5ZlTeAFaAXDWNWBGHdNRUIDfDSf00hM9VmIf79PRWd1T2WNBcLyl6kusLen79MeF7VSALuiZW9GRZjCpIBwF7FV2prQpX4rVrLUbN477xz+4UhsFNi+lPzX2ZVOTaNLHZMyQKBgQC5f4Ead/0QG3VzKM1VQD0LLzv0RnN+AArfYTiVCIXWcaH1iZWUEmvQIb6bOD1v+Rp2tubg2HDzLiZvq2LtrYT6JvU5g68YN4TASg2qCvvlSdICAg3/FgCZyVCdRrnTQcluumnyGCIJo+G8DtIF7NxUAyl13ZIXJQmZ3HzMlMzj/wKBgCX4/h5TnV3gWPojFoT+1SufGKhuWRV7nwW/clEkKdkvKS01eLbTR5mpT4hZ9UXNPsNxzb6vraBgpwO2Yt4amCymo0EMuWjJtjVz2QL3F+G7ZWySEZnrJQMsdONHHiamZPBcHl5MCl1zt4RcH/7zYQ8cPnJ+5/mH9B4m0lL0E2EZAoGAS9KtQxNezmhHAo0fF9K78EykF14a2jlXIfBlIxfAUr8ie18mwsdjfhxXBwkn+QyZi8W0S7kPYsZUY4au9ZpQPyAeUfHPbHjYId93Adob0bFBXbXzJqUXq2Jp1+mAHeAKUFJk1htdseHHCn2mzS37JIIJdEHEydA/ALDmB1BASz0=
alipay.alipay_public_key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8O6UgsH9DNS0cSaLScqCuacJyySGOg0XnkaPL2vxaeBhF8BHKfV3Q7uhaMbQyh0mFdHsnZr7iopMf/pNVqOrTVn1EZ5HvrgwsDSUoXP8qCC+9i53BlrD00gsxKT+Y19vlk/Hrx+H354ftRDETuU7MMShvDPVY3iodj0I6bjN1FffprhCkH8tADc/ixOhsnVP8LhblhDabgYiAd5jb1i3cWI8q+wjZXVopi9L2xEPxafY/l0B+2Muee4u7zF9TDq3o3XnSpnnUDMa7EIEUIZ4BQZnP3LPUz3dqFiw1haMxxGtyZXiXHykvAZrxr1ThLETlV73cT2Q4Ub+9xrj4ZfqTwIDAQAB
#alipay.notify_url=http://tzy.nat300.top/payed/notify
alipay.notify_url=http://member.gulimall.com/memberOrder.html
alipay.return_url=http://member.gulimall.com/memberOrder.html
alipay.sign_type=RSA2
alipay.charset=utf-8
alipay.gatewayUrl=https://openapi.alipaydev.com/gateway.do

2)、订单支付

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

@Controller
public class PayWebController {
     
    @Autowired
    AlipayTemplate alipayTemplate;
 
    @Autowired
    OrderService orderService;
 
	@ResponseBody
    @GetMapping(value = "payOrder", produces = "text/html")
    public String payOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {
     
 
        PayVo payVo = orderService.getOrderPay(orderSn);
        String pay = alipayTemplate.pay(payVo);//返回的pay是一个html页面。将此页面直接交给浏览器就行
        return pay;
    }
}

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

    /**
     * 获取当前订单地支付信息
     */
    PayVo getOrderPay(String orderSn);

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

    @Override
    public PayVo getOrderPay(String orderSn) {
     
        PayVo payVo = new PayVo();
        OrderEntity order = this.getOrderByOrderSn(orderSn);
        BigDecimal bigDecimal = order.getPayAmount().setScale(2, BigDecimal.ROUND_UP);//支付金额设置为两位小数,否则会报错
        payVo.setTotal_amount(bigDecimal.toString());
        payVo.setOut_trade_no(order.getOrderSn());
        List<OrderItemEntity> order_sn = orderItemService.list(new QueryWrapper<OrderItemEntity>().eq("order_sn", orderSn));
        OrderItemEntity entity = order_sn.get(0);
        payVo.setSubject(entity.getSkuName());//订单名称
        payVo.setBody(entity.getSkuAttrsVals());//商品描述
        return payVo;
    }

yml配置中alipay.return_url=http://member.gulimall.com/memberOrder.html就是配置了支付成功以后,我们要跳到用户的订单列表页,所以现在我们需要整合gulimall-member的用户订单列表页

3)、整合gulimall-member

动静分离、整合登陆拦截器、springsession数据共享

4)、测试

day03_《谷粒商城》的完整流程(详细版二)_第2张图片
day03_《谷粒商城》的完整流程(详细版二)_第3张图片
day03_《谷粒商城》的完整流程(详细版二)_第4张图片
day03_《谷粒商城》的完整流程(详细版二)_第5张图片

day03_《谷粒商城》的完整流程(详细版二)_第6张图片

day03_《谷粒商城》的完整流程(详细版二)_第7张图片
day03_《谷粒商城》的完整流程(详细版二)_第8张图片

day03_《谷粒商城》的完整流程(详细版二)_第9张图片

P306 支付与订单—订单页面的展示


day03_《谷粒商城》的完整流程(详细版二)_第10张图片

前台带着当前页码“pageNum”发送请求/memberOrder.html给gulimall-member,后台使用OpenFeign远程调用gulimall-order,在gulimall-order中从登陆拦截器中获取当前用户id,根据用户id查询"oms_order"表拿到OrderEntity,然后从OrderEntity中拿到订单号查询"oms_order_item"表拿到OrderItemEntity,把OrderItemEntity设置到OrderEntity中,然后采用分页工具返回OrderEntity就OK。

①OrderEntity里面有相当多的属性:

  • 订单号
  • 会员id会员名(从LoginUserInterceptor中获取到)
  • 运费 还有 收货人姓名、电话、省份、地区、街道等等,这些属性一般都是调用gulimall-ware根据addr_id远程查询
  • 订单状态、确认时长、确认状态

day03_《谷粒商城》的完整流程(详细版二)_第11张图片

②OrderItemEntity里面有超多属性:

  • spu信息
  • sku信息
  • 积分信息
  • 优惠价格:PromotionAmount、CouponAmount、IntegrationAmount
  • 实际价格:总额 - 各种优惠价格

P307 支付与订单—支付宝异步通知1

P308 支付与订单—支付宝异步通知2

day03_《谷粒商城》的完整流程(详细版二)_第12张图片
day03_《谷粒商城》的完整流程(详细版二)_第13张图片

想要修改订单状态,有两种方式:
①url上面有订单号,你可以根据订单号修改订单状态,问题是不安全,别人随便发个这种请求带上订单号你就把订单状态改了?
②当订单完成后,其实支付宝官方会把订单相关的所有信息都以RabbitMQ发给你,而且只要你不签收它就不断地给你发。

day03_《谷粒商城》的完整流程(详细版二)_第14张图片

本节课干了两件事,第一件事是配置内网穿透,这个流程看看就行了,这么做是保证你的项目可以被外网访问到,然后有用户完成订单,支付宝就会按照你的外网地址给你发送RabbitMQ消息
第二件事就重要了,你收到支付宝RabbitMQ消息(封装支付宝那边发来的消息到PayAsyncVo里),第一步先验证这个消息是支付宝发来的,“验证签名”操做的代码是官方给的;验证完签名然后就是获取支付宝发送来的消息然后提取重要的信息到“交易流水”表里面,另外更新mos_order表的“订单状态”

day03_《谷粒商城》的完整流程(详细版二)_第15张图片PaymentInfoEntity和这张交易流水表对应

day03_《谷粒商城》的完整流程(详细版二)_第16张图片

P309 支付与订单—收单

day03_《谷粒商城》的完整流程(详细版二)_第17张图片

day03_《谷粒商城》的完整流程(详细版二)_第18张图片

使用支付宝自动收单功能,一旦指定时间不支付,就不能再支付了
day03_《谷粒商城》的完整流程(详细版二)_第19张图片
做法很简单,添加个配置就行了

P310 秒杀服务—后台添加秒杀商品

1.总说

day03_《谷粒商城》的完整流程(详细版二)_第20张图片
day03_《谷粒商城》的完整流程(详细版二)_第21张图片
day03_《谷粒商城》的完整流程(详细版二)_第22张图片
day03_《谷粒商城》的完整流程(详细版二)_第23张图片

day03_《谷粒商城》的完整流程(详细版二)_第24张图片
day03_《谷粒商城》的完整流程(详细版二)_第25张图片
day03_《谷粒商城》的完整流程(详细版二)_第26张图片

2.查询关联的商品

day03_《谷粒商城》的完整流程(详细版二)_第27张图片

前台传来(分页参数page、limit)和 (场次id)
后台先判断(场次id)是否为空,如果不为空就根据场次id查询sms_seckill_sku_relation表,
然后把查询结果List分页返回

P311 秒杀服务—环境搭建

P312 秒杀服务—SpringBoot整合定时任务

用了cron表达式

P313 秒杀服务—商品上架1

P314 秒杀服务—商品上架2

P315 秒杀服务—商品上架3

P316 秒杀服务—商品上架4

为了预告用户两天后什么东西要参与秒杀,我们将要秒杀的商品的相关信息提前三天从数据库上架到缓存中,而且这个定时任务是每天晚上凌晨3点执行(趁不是高峰期时间)。

1.三张表 + 五个实体类

day03_《谷粒商城》的完整流程(详细版二)_第28张图片
day03_《谷粒商城》的完整流程(详细版二)_第29张图片

三张表:

  • ①一张表是“秒杀场次表”sms_seckill_session,它记录哪一场秒杀活动几点开始几点结束
  • ②另一张表是“秒杀场次和商品的关联表”sms_seckill_sku_relation,因为一场秒杀活动对应多个商品,而其中的一个商品肯定限制了多少钱、多少件、一个用户限购多少等等。“秒杀场次和商品的关联表”记录了某一场秒杀活动的某件商品的秒杀详情信息。
  • ③第三张表是商品详情信息表pms_sku_info。我们要前台展示这个秒杀商品的的具体详细信息。

五个实体类:

  • ①SeckillSkuRelationEntity实体类:它对应着数据库的“秒杀场次和商品的关联表”"sms_seckill_sku_relation
  • ②SeckillSessionEntity实体类:它尽管和数据库“秒杀场次表”sms_seckill_session对应,但它有一个数据库中不存在的字段(List存放着SeckillSkuRelationEntity)
  • ③SeckiSkuVo和SeckillSkuRelationEntity是一模一样的
  • ④SeckillSessionWithSkus和SeckillSessionEntity是一模一样的,它里面有个字段是List存放着SeckiSkuVo
  • ⑤SkuInfoVo和SkuInfoEntity一模一样,都对应着“商品详情信息表”pms_sku_info
  • ⑤SeckillSkuRedisTo=SeckillSkuVo+SkuInfoVo+开始时间+结束时间+随机码,也就是说它记录了“秒杀场次和商品的关联表”"sms_seckill_sku_relation+“商品详情信息表”pms_sku_info+。

2.总说

开启一个定时任务,将要秒杀的商品的相关信息提前三天从数据库上架到缓存中:

  • ①gulimall-seckill每天凌晨三点远程调用gulimall-coupon服务的getLasts3DaySession()方法获取最近三天的秒杀活动————gulimall-coupon会查询“秒杀场次表”获取到最近三天的秒杀活动,然后根据活动id查询“秒杀场次和商品的关联表”获取到每一场秒杀活动参与秒杀的所有商品。将每一场秒杀活动的所有商品封装到SeckillSessionEntity的List,然后三天的所有活动封装到List里面返回。
  • ②gulimall-seckill得到最近三天的秒杀活动后,将数据存到redis中————存到redis中的数据不仅仅是“这三天有几场秒杀活动,每个秒杀活动里面有多少商品”,还要把每件参与秒杀的商品的sku信息查询到存到redis中(需要根据skuid远程查询gulimall-product得到sku_info)。

3.代码

1.定时任务:

@Slf4j
@Service
public class SeckillSkuScheduled {
     
 
    @Autowired
    SeckillService seckillService;
 
    //定时任务
    @Scheduled(cron = "0 0 3 * * ?")
    public void uploadSeckillSkuLatest3Days(){
     
        //重复上架无需处理
        log.info("上架秒杀的信息......");
        seckillService.uploadSeckillSkuLatest3Days();
    }
}

2.service:

@Service
public class SeckillServiceImpl implements SeckillService {
     

	......
 
    private final String SESSIONS_CACHE_PREFIX = "seckill:sessions:";
 
    private final String SKUKILL_CACHE_PREFIX = "seckill:skus:";
 
    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";//+商品随机码
 
    @Override
    public void uploadSeckillSkuLatest3Days() {
     
    
        // 远程获取最近三天需要参与秒杀的活动
        R session = couponFeignService.getLasts3DaySession();
        if (session.getCode() == 0){
     
            // 上架商品
            List<SeckillSessionWithSkus> data = session.getData(new TypeReference<List<SeckillSessionWithSkus>>() {
     
            });
            // 缓存到redis
 
            // 1、缓存活动信息
            saveSessionInfos(data);
 
            // 2、缓存获得关联商品信息
            saveSessionSkuInfos(data);
        }
    }
 
   
}

3.远程获取最近三天需要参与秒杀的活动:

getLasts3DaySession()代码的逻辑就是:
gulimall-coupon会查询“秒杀场次表”获取到最近三天的秒杀活动,然后根据活动id查询“秒杀场次和商品的关联表”获取到每一场秒杀活动参与秒杀的所有商品。将每一场秒杀活动的所有商品封装到SeckillSessionEntity的List,然后三天的所有活动封装到List里面返回。所以最后返回的就是List

4.在redis中保存秒杀商品信息

day03_《谷粒商城》的完整流程(详细版二)_第30张图片
day03_《谷粒商城》的完整流程(详细版二)_第31张图片
day03_《谷粒商城》的完整流程(详细版二)_第32张图片

P317 秒杀服务—商品上架5(解决幂等性问题)

①分布式系统下,三台机器可能同时都执行这个定时任务,所以我们可以加一个分布式锁Redisson,谁拿到锁谁执行这个上架功能。

day03_《谷粒商城》的完整流程(详细版二)_第33张图片

②因为我们把提前三天的秒杀活动存到redis中这一定时任务是每天凌晨3点都要执行一遍,所以今天凌晨三天往redis中存放数据时先看看昨天凌晨三点存过了没有

day03_《谷粒商城》的完整流程(详细版二)_第34张图片
day03_《谷粒商城》的完整流程(详细版二)_第35张图片

P318 秒杀服务—查询当前可以秒杀的商品


day03_《谷粒商城》的完整流程(详细版二)_第36张图片

P319 秒杀服务—商品详情页展示秒杀信息

day03_《谷粒商城》的完整流程(详细版二)_第37张图片

商品详情页当初我们写在gulimall-product里面的,前台发送请求给gulimall-product,然后gulimall-product携带skuid远程查询gulimall-seckill来获取当前商品有没有参与秒杀活动,查询方法是写一个正则表达式"\d_4"来匹配redis中的那些keys,匹配上以后根据key获得值,封装到SeckillSkuRedisTo里面进行传输,传输之前看一下秒杀活动开始了没有,如果没有开始就把SeckillSkuRedisTo里的随机码置为空(我们的随机码只有在秒杀活动开始了才暴露)。
day03_《谷粒商城》的完整流程(详细版二)_第38张图片

P320 秒杀开始—理论讲解“秒杀系统设计”

p320是理论讲解了如何解决秒杀开始后的高并发问题,面试谈的就是这些

day03_《谷粒商城》的完整流程(详细版二)_第39张图片

P321 秒杀开始—登录检查

SpringSession的配置:

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

用户登录拦截器的配置:
(我们不能拦截所有请求,除了秒杀请求“/kill”需要判断登录外,像“/currentSeckillSkus”这些都是远程服务被别人调用的,不应该被拦截)

@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 requestURI = request.getRequestURI();
        AntPathMatcher matcher = new AntPathMatcher();
        boolean match = matcher.match("/kill", requestURI);
        // 如果是秒杀,需要判断是否登录,其他路径直接放行不需要判断
        if (match) {
     
            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;
            }
        }
        return true;
    }
}

P322 秒杀开始—秒杀1

P323 秒杀开始—秒杀2

1.总说
开始秒杀时前台携带着killIdrandomCodenum发送/kill请求给后台(假如killid=6_4那么活动的场次是6、skuid是4,randomCode是随机码,num是用户要秒杀的商品数量),后台就进行校验——首先要求根据killid能找到用户要秒杀的商品,其次要求当前时间在秒杀时间范围内,再者要求前台传来的killIdrandomCode(随机码)和后台redis中存储的能够匹配,最后要求秒杀的数量num符合数量限制,以上的条件都满足就可以去redis中给该用户占位了(占位是为了防止重复,如果redis中已经帮改用户占过位就会出现占位失败,占位的key是“用户id_场次id_商品id”),占位成功然后获取信号量,获取信号量成功就发送RabbitMQ消息给gulimall-order表示要给该用户下单啦。
gulimall-order一直监听着那边发来的消息,收到消息后就保存订单信息然后保存订单项信息(本来还应该保存收货地址、锁定库存什么的,但是我们没有做这些操做)

2.运行结果

day03_《谷粒商城》的完整流程(详细版二)_第40张图片
day03_《谷粒商城》的完整流程(详细版二)_第41张图片

P324 秒杀开始—秒杀3

这一节主要是前台代码,略微修改了后台代码

day03_《谷粒商城》的完整流程(详细版二)_第42张图片

运行结果

day03_《谷粒商城》的完整流程(详细版二)_第43张图片
day03_《谷粒商城》的完整流程(详细版二)_第44张图片

P325 Sentinel—简介

(概念)①对秒杀服务的总结;②Sentinel熔断、降级;③Hystrix和Sentinel的对比

P326 Sentinel—Sentinel概念

视频不用看,直接看笔记SpringCloudAlibaba—Sentinel

P327 Sentinel—Sentinel流控1

P328 Sentinel—Sentinel流控2

P329 Sentinel—Sentinel流控3

P330 Sentinel—Sentinel流控4

这块视频就不用看了,因为他就是简简单单地演示了下sentinel的流控,还是直接看周阳老师的笔记SpringCloudAlibaba—Sentinel

这块对应的别人的笔记也不用看了,直接看自己的就行

1.整合sentinel

整合sentinel的操做:

      1)、导入依赖 spring-cloud-starter-alibaba-sentinel

      2)、启动Sentinel的控制台

      3)、yml中配置sentinel控制台地址信息

所有微服务都进行这些操做,这样sentinel才能监控所有的微服务

1)、导入依赖

        
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>

2)、启动Sentinel的控制台

day03_《谷粒商城》的完整流程(详细版二)_第45张图片

https://github.com/alibaba/Sentinel去官网下载该版本的控制台

day03_《谷粒商城》的完整流程(详细版二)_第46张图片
day03_《谷粒商城》的完整流程(详细版二)_第47张图片
day03_《谷粒商城》的完整流程(详细版二)_第48张图片

启动Sentinel的方式:

  • 输入命令 java -jar sentinel-dashboard-1.7.1.jar 就足够了,它默认使用8080端口,
  • 如果8080已经被占用就输入java -jar sentinel-dashboard-1.7.1.jar --server.port=8333,既然你把控制台的端口改为8333了那么你在yml里面配置的时候也要记得配置为8333
  • 用户名和密码都是sentinel

3)、yml中配置

#Sentinel控制台地址(如果你把控制台的端口改为8333那么这里就是8333)
spring.cloud.sentinel.transport.dashboard=localhost:8080
#Sentinel传输端口
spring.cloud.sentinel.transport.port=8719
#设置暴露的endpoint 
management.endpoints.web.exposure.include=*

2.Sentinel流控演示

这块雷丰阳老师讲的很乱,建议直接看周阳老师的笔记SpringCloudAlibaba—Sentinel

P331 Sentinel—Sentinel熔断降级

服务熔断和降级请看文章:SpringCloud—Hystrix

浏览器直接访问/test那么我们可以对/test做个流控(限制每秒请求不能超过多少啦等等),
但是浏览器直接访问gulimall-product然后product远程调用gulimall-seckill的/test就需要用熔断降级

1)、fegin的熔断

服务熔断最常见的就是gulimall-product调用gulimall-seckill,结果gulimall-seckill宕机,以前不使用sentinel的熔断的时候直接调用会报错;现在使用sentinel的熔断保护机制,远程调用失败后gulimall-product调用自己的熔断回调方法。

1)、调用方的熔断保护开启 feign.sentinel.enabled=true

2)、调用方手动指定远程服务的降级策略。

3)、远程服务被降级处理后,触发我们的熔断回调方法

熔断的流程就是:先给对方服务进行降级,我们自己调用熔断回调方法,正常以后恢复链路(降级—>熔断—>恢复)

①在调用方(gulimall-product)的yml配置文件添加配置

#默认情况下,sentinel是不会对feign进行监控的,需要开启配置
feign.sentinel.enabled=true

②修改调用方(gulimall-product)的远程调用类

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

@FeignClient(value = "gulimall-seckill",fallback = SeckillFeignServiceFallBack.class)
public interface SeckillFeignService {
     
 
    @GetMapping("/sku/seckill/{skuId}")
    R getSkuSecKillInfo(@PathVariable("skuId") Long skuId);
}

③我们会在调用方(gulimall-product)实现远程调用类,自定义返回信息给页面。

添加“com.atguigu.gulimall.product.fallback.SeckillFeignServiceFallBack”类(注意我们修改的是gulimall-product),代码如下:

//在调用方gulimall-product实现远程调用类,自定义返回信息给页面。

@Slf4j
@Component
public class SeckillFeignServiceFallBack implements SeckillFeignService {
     
    @Override
    public R getSkuSecKillInfo(Long skuId) {
     
        log.info("熔断方法调用.....getSkuSecKillInfo");
        return R.error(BizCodeEnume.TOO_MANY_REQUESTS_EXCEPTION.getCode(),BizCodeEnume.TOO_MANY_REQUESTS_EXCEPTION.getMsg());
    }
}

2)、调用方指定降级策略

day03_《谷粒商城》的完整流程(详细版二)_第49张图片

day03_《谷粒商城》的完整流程(详细版二)_第50张图片
day03_《谷粒商城》的完整流程(详细版二)_第51张图片

  • gulimall-product调用gulimall-seckill的时候,如果请求数量大于5而且gulimall-seckill在1ms以内做不出响应,那么10秒以内我们就不调用gulimall-seckill了。
  • 我们让gulimall-seckill那边睡3秒,而且浏览器疯狂刷新使得请求数量大于5,达到降级要求就gulimall-product在10秒内不再调用gulimall-seckill而是直接走自己的fallback方法(fallback方法其实就是自己的熔断回调方法)。
  • 原理:可以在调用方手动指定自己的降级策略,如果远程服务达不到要求就被降级了,远程服务被降级了就跟被宕机了一样,我们10秒内就压根不调用它了,直接走自己的fallback方法

3)、提供方指定降级策略

我们一般都是在调用方gulimall-product指定降级策略,因为一旦提供方被降级了,调用方就不远程调用了,直接走熔断回调方法;
而如果在提供方gulimall-seckill指定降级策略,即使提供方被降级了,调用方还是会调用gulimall-seckill。

那么什么时候在提供方指定降级策略呢?
别人都来调用我,我忙不过来了,我必须牺牲一部分被调用的方法,我给自己的某些方法指定一个降级策略,当我这个方法被别人高并发访问时我这个方法就被降级了,我就不运行我这个方法里面的业务逻辑了,而是直接返回默认的降级数据。

1.指定降级策略

day03_《谷粒商城》的完整流程(详细版二)_第52张图片

2.自定义默认的降级数据
在服务提供方gulimall-seckill添加一个配置类com.atguigu.gulimall.seckill.config.SeckillSentinelConfig,让这个配置类自定义返回降级数据

/**
 * Sentinel-自定义流控响应的配置类,2.2.0以后的版本实现的是BlockExceptionHandler;以前的版本实现的是WebCallbackManager
 */
@Configuration
public class SeckillSentinelConfig implements BlockExceptionHandler {
     

    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
     
        R error = R.error(BizCodeEnume.TOO_MANY_REQUESTS_EXCEPTION.getCode(), BizCodeEnume.TOO_MANY_REQUESTS_EXCEPTION.getMsg());
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json");
        httpServletResponse.getWriter().write(JSON.toJSONString(error));
    }
}

上面的代码中R error = R.error(BizCodeEnume.TOO_MANY_REQUESTS_EXCEPTION.getCode(),BizCodeEnume.TOO_MANY_REQUESTS_EXCEPTION.getMsg());那么长你肯定没有认真看,其实他就是R error = R.error( code , message)的格式而已

3.测试

day03_《谷粒商城》的完整流程(详细版二)_第53张图片

P332 Sentinel—@SentinelResource注解

讲解了@SentinelResource注解的使用,别看视频了,也别看别人的笔记了,这块雷丰阳老师讲的很肤浅,建议直接看周阳老师的笔记SpringCloudAlibaba—Sentinel

day03_《谷粒商城》的完整流程(详细版二)_第54张图片

P333 Sentinel—网关流控1

P334 Sentinel—网关流控2

如果能在网关层就进行流控,可以避免请求流入业务,减小服务压力

gulimall-gateway引入依赖


<dependency>
    <groupId>com.alibaba.cloudgroupId>
    <artifactId>spring-cloud-alibaba-sentinel-gatewayartifactId>
    <version>2.2.0.RELEASEversion>
dependency>

注意引入的依赖要和gulimall-common的pom里的阿里巴巴版本一致

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloudgroupId>
                <artifactId>spring-cloud-alibaba-dependenciesartifactId>
                <version>2.2.0.RELEASEversion>
                <type>pomtype>
                <scope>importscope>
            dependency>
        dependencies>
    dependencyManagement>

具体内容回看视频p333和p334

P335 Sleuth—基本概念

day03_《谷粒商城》的完整流程(详细版二)_第55张图片

P336 Sleuth的使用1

P337 Sleuth的使用2

1.springboot整合sleuth

1.所有微服务导入依赖(导入zipkin就把sleuth连同导入了)


<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-zipkinartifactId>
dependency>

2.所有的微服务yml进行配置

spring:
    zipkin:
      base-url: http://192.168.56.106:9411
      sender:
        type: web
      # 取消nacos对zipkin的服务发现
      discovery-client-enabled: false
    #采样取值介于 0到1之间,1则表示全部收集
    sleuth:
      sampler:
        probability: 1

3.虚拟机运行zipkin

docker run -d -p 9411:9411 openzipkin/zipkin

day03_《谷粒商城》的完整流程(详细版二)_第56张图片

2.Sleuth可视化界面怎么看?

我非常建议你去看这块的视频p336—p337

可以结合周阳老师的课程笔记:SpringCloud—Sleuth

P338 高级篇总结

P339 完整运行该项目

1.启动虚拟机

day03_《谷粒商城》的完整流程(详细版二)_第57张图片

2.启动Nacos

day03_《谷粒商城》的完整流程(详细版二)_第58张图片

3.启动seata

day03_《谷粒商城》的完整流程(详细版二)_第59张图片

4.启动sentinel
原则上需要启动sentinel,但是由于它占用了8080端口,而且所有微服务给它配置的也是8080端口,懒得改了;8080端口和我们的renren-fast是冲突的,所以不启动它也无妨。

5.启动所有微服务

day03_《谷粒商城》的完整流程(详细版二)_第60张图片

6.启动前台renren-fast-vue

day03_《谷粒商城》的完整流程(详细版二)_第61张图片

7.操作流程

day03_《谷粒商城》的完整流程(详细版二)_第62张图片
在这里插入图片描述
day03_《谷粒商城》的完整流程(详细版二)_第63张图片
day03_《谷粒商城》的完整流程(详细版二)_第64张图片
day03_《谷粒商城》的完整流程(详细版二)_第65张图片
day03_《谷粒商城》的完整流程(详细版二)_第66张图片
在这里插入图片描述

day03_《谷粒商城》的完整流程(详细版二)_第67张图片
day03_《谷粒商城》的完整流程(详细版二)_第68张图片
day03_《谷粒商城》的完整流程(详细版二)_第69张图片
day03_《谷粒商城》的完整流程(详细版二)_第70张图片

day03_《谷粒商城》的完整流程(详细版二)_第71张图片
day03_《谷粒商城》的完整流程(详细版二)_第72张图片

你可能感兴趣的:(谷粒商城项目总结)