谷粒商城分布式高级篇(中)

谷粒商城分布式基础篇
谷粒商城分布式高级篇(上)
谷粒商城分布式高级篇(中)
谷粒商城分布式高级篇(下)

文章目录

  • 商城业务
    • 异步
      • 异步复习
      • 线程池详解
      • CompletableFuture
      • CompletableFuture-启动异步任务
      • CompletableFuture-完成回调与异常感知
      • CompletableFuture-handle最终处理
      • CompletableFuture-线程串行化
      • CompletableFuture-两任务组合-都要完成
      • CompletableFuture-两任务组合-一个完成
      • CompletableFuture-多任务组合
    • 商品详情
      • 环境搭建
      • 模型抽取
      • 规格参数
      • 销售属性组合
      • 详情页渲染
      • 销售属性渲染
      • sku组合切换
      • 异步编排优化
    • 认证服务
      • 环境搭建
      • 好玩的验证码倒计时
      • 整合短信验证码
      • 验证码防刷校验
      • 一步一坑的注册页环境
      • 异常机制
      • MD5&盐值&BCrypt
      • 注册完成
      • 账号密码登录完成
      • OAuth2.0简介
      • weibo登录测试
      • 社交登录回调
      • 社交登录完成
      • 社交登录测试成功
      • 分布式session不共享不同步问题
      • 分布式session解决方案原理
      • SpringSession整合
      • 自定义SpringSession完成子域session共享
      • SpringSession原理
      • 页面效果完成
      • 单点登录简介
      • 框架效果演示
      • 单点登录流程-1
      • 单点登录流程-2
      • 单点登录流程-3
    • 购物车
      • 环境搭建
      • 数据模型分 析
      • VO编写
      • ThreadLocal用户身份鉴别
      • 添加购物车
      • RedirectAttribute
      • 获取&合并购物车
      • 选中购物项
      • 改变购物项数量
      • 删除购物项
    • 消息队列
      • MQ简介
      • RabbitMQ简介
      • RabbitMQ工作流程
      • RabbitMQ安装
      • Exchange类型
      • Direct-Exchange 点对点
      • Fanout-Exchange 扇出类型
      • Topic-Exchange 主题类型
      • SpringBoot整合RabbitMQ
      • AmqpAdmin使用
      • RabbitTemplate使用
      • RabbitListener&RabbitHandler接收消息
      • 可靠投递-发送端确认
      • 可靠投递-消费端确认

商城业务

异步

异步复习

开启线程的四种方式
谷粒商城分布式高级篇(中)_第1张图片
以后的业务代码,将所有的多线程异步任务都交给线程池执行
谷粒商城分布式高级篇(中)_第2张图片

线程池详解

给线程池直接提交任务 创建都是返回一个ExecutorService
public static ExecutorService service = Executors.newFixedThreadPool(10);

线程池的原生创建:ThreadPoolExecutor executor = new ThreadPoolExecutor();
线程池的七大参数
谷粒商城分布式高级篇(中)_第3张图片
七大参数

  1. in corePoolSize 核心线程数;线程池创建好以后就准备就绪的线程数量,等待接受异步任务执行
  2. int maximumPoolSize 最大线程数量;控制资源
  3. long keepAliveTime 存活事件。如果当前的线程数量大于corePoolSize数量,释放空闲的线程(超出核心线程数量的部分)条件:超出核心线程数量的线程空闲时间超过long keepAliveTime就会被释放
  4. TimeUnit unit 时间单位
  5. BlockingQueue workQueue 阻塞队列。如果任务有很多,超过最大线程数量maximumPoolSize
    的部分就会放在这里,只要有空闲线程就会来这里取出任务去执行
  6. ThreadFactory threadFactory 线程的创建工厂
  7. RejectedExecutionHandler handler 如果队列满了,按照我们指定的拒绝策略拒绝执行任务

谷粒商城分布式高级篇(中)_第4张图片

CompletableFuture

谷粒商城分布式高级篇(中)_第5张图片
谷粒商城分布式高级篇(中)_第6张图片
3,4,5都需要获取1的返回结果,CompletableFuture实现了Future接口

CompletableFuture-启动异步任务

runXxx是都没有返回值的,supplyXxx 都是可以获得返回值的

public static ExecutorService executor = Executors.newFixedThreadPool(10);


public static void main(String[] args) throws ExecutionException, InterruptedException {
     
    System.out.println("main...start...");
    
    CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
     
         System.out.println("当前线程:" + Thread.currentThread().getId());
         int i = 10 / 2;
         System.out.println("运行结果:" + i);
     }, executor);
     
    CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
     

        System.out.println("当前线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        System.out.println("运行结果:" + i);
        return i;
    }, executor);
    Integer integer = future.get();
    System.out.println("main...end..."+integer );
}

CompletableFuture-完成回调与异常感知

whenComplete感知异常
exceptionally 修改返回结果
谷粒商城分布式高级篇(中)_第7张图片

CompletableFuture-handle最终处理

两个参数,返回值和异常

谷粒商城分布式高级篇(中)_第8张图片

CompletableFuture-线程串行化

谷粒商城分布式高级篇(中)_第9张图片
1、thenRun:不能获取到上一步的执行结果,无返回值
谷粒商城分布式高级篇(中)_第10张图片
2、thenAcceptAsync能接收上一步结果,但是无返回值
谷粒商城分布式高级篇(中)_第11张图片
3、thenApplyAsync能接收上一步结果,有返回值,.get()为阻塞式方法,等到俩个线程都结束并发回结果了才调用
谷粒商城分布式高级篇(中)_第12张图片

CompletableFuture-两任务组合-都要完成

CompletableFuture-两任务组合-一个完成

两个任务,只要有一个完成,我们就执行任务3
runAfterEitherAsync:不感知结果,自身无返回值

future01.runAfterEitherAsync(future02,()->{
     
    //不感知结果,自身无返回值
	System.out.println("任务3开始...之前的结果");
},executor);

acceptEitherAsync: 感知结果,自身无返回值

future01.acceptEitherAsync(future02,(res)->{
     
	System.out.println("任务3开始...之前的结果"+res);
},executor);

applyToEitherAsync: 感知结果,自身有返回值

CompletableFuture<String> fu = future01.applyToEitherAsync(future02, (res) -> {
     
    System.out.println("任务3开始...之前的结果"+res);
    return res + "haha";
}, executor);
 
System.out.println("main...end..."+fu.get());

CompletableFuture-多任务组合

谷粒商城分布式高级篇(中)_第13张图片

public static void main(String[] args) throws ExecutionException, InterruptedException {
     
    System.out.println("main...start...");

    CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> {
     
        System.out.println("查询图片信息");
        return "hello.jpg";
    }, executor);

    CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(() -> {
     
        System.out.println("查询商品的属性");
        return "黑色+256G";
    }, executor);

    CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(() -> {
     
        System.out.println("查询商品介绍");
        return "华为";
    }, executor);

//        CompletableFuture allOf = CompletableFuture.allOf(futureImg, futureAttr, futureDesc);
//        allOf.get();//等待所有结果完成
//        System.out.println("main...end..."+futureImg.get()+"---"+futureAttr.get()+"---"+futureDesc.get());

    CompletableFuture<Object> anyOf = CompletableFuture.anyOf(futureImg, futureAttr, futureDesc);
    System.out.println("main...end..."+anyOf.get());//获得了最先完成的结果
}

商品详情

环境搭建

  1. 本地host
    在这里插入图片描述

  2. 虚拟机nginx站点文件
    在这里插入图片描述

  3. 网关转发
    谷粒商城分布式高级篇(中)_第14张图片

  4. 导入html文件

  5. 静态分离静态文件上传nginx

  6. 改变html文件中静态文件地址

模型抽取

分析业务可知接口返回一个页面模型 SkuItemVo item = skuInfoService.item(skuId);
在模型中封装
谷粒商城分布式高级篇(中)_第15张图片

规格参数

联合查询凑齐所需要的数据,并使用自定义封装,注意自定义封装时,不能使用内部类的方式返回封装对象,所以降之前的内部类设计改为单独类,注意sql语句中返回的字段值不能起别名,否则自定义resultMap无法封装到

<resultMap id="spuItemAttrGroupVo" type="com.atguigu.gulimall.product.vo.SpuItemAttrGroupVo">
    <result property="groupName" column="attr_group_name"/>
    <collection property="attrs" ofType="com.atguigu.gulimall.product.vo.product.Attr">
        <result property="attrName" column="attr_name"/>
        <result property="attrValue" column="attr_value"/>
    collection>
resultMap>

<select id="getAttrGroupWithAttrsBySpuId"
        resultMap="spuItemAttrGroupVo">
    SELECT
        ag.attr_group_name,
        ag.attr_group_id,
        aar.attr_id,
        attr.attr_name ,
        pav.attr_value ,
        pav.spu_id attr
    FROM
        `pms_attr_group` ag
            LEFT JOIN pms_attr_attrgroup_relation aar ON aar.attr_group_id = ag.attr_group_id
            LEFT JOIN pms_attr attr ON attr.attr_id = aar.attr_id
            LEFT JOIN pms_product_attr_value pav ON attr.attr_id = pav.attr_id
    WHERE
        ag.catelog_id = #{catalogId} and pav.spu_id=#{spuId}

select>

销售属性组合

获取并封装返回到页面的商品sku信息
谷粒商城分布式高级篇(中)_第16张图片
谷粒商城分布式高级篇(中)_第17张图片
用分组查询,

SELECT ssav.attr_id                            attrId,
       ssav.attr_name                          attrName,
       GROUP_CONCAT(DISTINCT ssav.attr_value ) attrValues
FROM pms_sku_info info
         LEFT JOIN pms_sku_sale_attr_value ssav ON ssav.sku_id = info.sku_id
WHERE info.spu_id = #{spuId}
GROUP BY ssav.attr_id,
         ssav.attr_name

结果为
谷粒商城分布式高级篇(中)_第18张图片

详情页渲染

有用到 thymelef 函数 ${#strings.listSplit(item.desc.decript,',')} 分隔函数

销售属性渲染

sku的聚合sql查询

SELECT
	ssav.attr_id attrId,
	ssav.attr_name attrName,
	ssav.attr_value,
	GROUP_CONCAT(DISTINCT info.sku_id) sku_ids
FROM
	pms_sku_info info
	LEFT JOIN pms_sku_sale_attr_value ssav ON ssav.sku_id = info.sku_id 
WHERE
	info.spu_id = 5 
	GROUP BY ssav.attr_id,ssav.attr_name,ssav.attr_value

谷粒商城分布式高级篇(中)_第19张图片
attr_value的交集sku_ids 为一个sku
js交集判断跳转
谷粒商城分布式高级篇(中)_第20张图片

sku组合切换

主要通过jq拼接实现
thymeleaf 添加 skus 属性
谷粒商城分布式高级篇(中)_第21张图片
jq 得到属性的值,并用fiter 函数得到 数组的交集

异步编排优化

  1. 添加一个线程池,创建线程池配置文件
    谷粒商城分布式高级篇(中)_第22张图片
  2. 做成可配置的线程池文件
    谷粒商城分布式高级篇(中)_第23张图片
    谷粒商城分布式高级篇(中)_第24张图片

线程池配置完毕

异步编排开始

  1. 注入线程池
    在这里插入图片描述
@Override
public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
     
    SkuItemVo skuItemVo = new SkuItemVo();

    CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
     
        //1、sku基本信息获取 pms_sku_info
        SkuInfoEntity info = getById(skuId);
        Long spuId = info.getSpuId();
        Long catalogId = info.getCatalogId();
        skuItemVo.setInfo(info);
        return info;
    }, executor);

    CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
     

        //3、spu的销售属性组合。
        List<SkuItemSaleAttrsVo> saleAttrsVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
        skuItemVo.setSaleAttrsVos(saleAttrsVos);
    }, executor);

    CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((res) -> {
     

        //4、获取spu的介绍(商品介绍)
        SpuInfoDescEntity desc = spuInfoDescService.getById(res.getSpuId());
        skuItemVo.setDesc(desc);
    }, executor);

    CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> {
     

        //5、获取spu的规格参数信息(规格与包装)
        List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
        skuItemVo.setGroupAttrs(attrGroupVos);
    }, executor);


    CompletableFuture<Void> imgFuture = CompletableFuture.runAsync(() -> {
     
        //2、sku的图片信息 pms_sku_images
        List<SkuImagesEntity> images = imagesService.getImagesBySkuId(skuId);
        skuItemVo.setImages(images);
    }, executor);

    //阻塞等到所有任务都完成
    CompletableFuture.allOf(infoFuture,saleAttrFuture,
            descFuture,baseAttrFuture,imgFuture).get();

    return skuItemVo;
}

认证服务

环境搭建

创建认证服务模块
谷粒商城分布式高级篇(中)_第25张图片
常规nacos 注册流程

导入 登录页和注册页的html文件,并nginx动静分离,nginx站点文件,网关路由配置

好玩的验证码倒计时

点击倒计时

$(function () {
     
    $("#sendCode").click(function () {
     
        //1、给指定手机号发送验证码
        //2、倒计时
        if ($(this).hasClass("disabled")) {
     

        } else {
     
            timeoutChangeStyle();
        }

    })
})
var num = 3

function timeoutChangeStyle() {
     
    $("#sendCode").attr("class", "disabled")
    if (num == 0) {
     
        $("#sendCode").text("发送验证码")
        $("#sendCode").attr("class", "")
    } else {
     
        var str = num + "s 后再次发送"
        $("#sendCode").text(str)
        setTimeout("timeoutChangeStyle()", 1000)
    }
    num--;
}

SpringMvc viewController 直接映射请求到页面

@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
     
    /**
     * 视图映射
     * @param registry
     */

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
     

        /**
         *     @GetMapping("/login.html")
         *     public String loginPage(){
         *         return "login";
         *     }
         * @param registry
         */
        registry.addViewController("/login.html").setViewName("login");
        registry.addViewController("/reg.html").setViewName("reg");
    }
}

整合短信验证码

将验证码发送代码作为组件整体封装在第三方服务模块中
gulimall-auth-server 模块远程调用 gulimall-third-party短信服务

创建短信发送控制器,能返回json数据,基准路径为 /sms

验证码防刷校验

发送验证码时,将手机作为键值存入redis,并设置过期时间,和存入时的系统时间,发送发送验证码时先在redis中查找这个值,判断是否过期

一步一坑的注册页环境

Request method 'POST' not supported POST 不支持,
原因: 用户注册----》 /register[post] --》return "forward:/reg.html";做了路径映射 路径映射默认是get方式访问

解决办法直接渲染,不做转发 return "reg.html";

为防止刷新表单重复提交,不要转发至页面,应该使用重定向,但是重定向会获取不到请求域中的数据,将Model 改为 RedirectAttributes 解决问题

重定向携带数据,利用session原理,将数据放在session中,只要跳到下一个页面去除这个数据就会删掉

异常机制

验证码验证完成后,调用远程接口注册,gulimall-member 服务模块注册,微服务之间都是 http 加 json 进行调用,所以远服务都返回和接收json,则 接口 都会加上 @RequestBody注解,SpringMvc 会自动将请求体里的对象转为json

远程接口检验并处理数据,接口要给调用者返回各种数据的校验异常结果,所以应该使用异常机制来返回 检验结果,

//检查用户名 和 手机号是否唯一 为了让controller感知异常,异常机制
checkPhoneUnique(vo.getPhone());

@Override
public void checkPhoneUnique(String phone) throws PhoneExistException{
     
    Integer count = this.baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
    if (count > 0) {
     
        //说明数据库有这个手机号
        throw new PhoneExistException();
    }
    //否则什么都不做 检查通过 业务继续进行注册
}

MD5&盐值&BCrypt

谷粒商城分布式高级篇(中)_第26张图片
Spring 内置盐值加秘

BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
//生成加密结果
String encode = encoder.encode("123456");
//比对
boolean matches = encoder.matches("123456", "$2a$10$l.3r9fHIrJFvgbxp7nw2LuFACxMYnEOnXdAEYDv4q.Gx/4iGIp0T.");

System.out.println(encode + "=>" + matches);

注册完成

回到gulimall-auth-server模块,验证码校验成功,调用远程接口完成注册

账号密码登录完成

简单的登录流程

  1. form表单
    提交input中的值至auth模块的 /login地址
  2. 地址中 有封装提交的值的vo,远程调用member模块的登录
    @PostMapping("/login")
    public String login(UserRegisterVo vo,RedirectAttributes redirectAttributes){
     
        //远程登录
        R r = memberFeignService.login(vo);
  1. member 服务接收 传来的json数据,自动封装成设计好的vo,处理登录请求,放回登录结果
    @PostMapping("/login")
    public R login(@RequestBody MemberLoginVo vo)
  1. auth模块得到处理结果,处理并返回到页面

OAuth2.0简介

谷粒商城分布式高级篇(中)_第27张图片
谷粒商城分布式高级篇(中)_第28张图片

weibo登录测试

谷粒商城分布式高级篇(中)_第29张图片
微博OAuth2.0文档

  1. 将引导地址放在自己的页面,用户点击可跳转至微博授权页面
  2. 如果用户同意授权,页面跳转至 YOUR_REGISTERED_REDIRECT_URI/?code=CODE
  3. 换取Access Token

code只能用一次,同一个用户的accessToken一段时间是不会变化的,即使多次获取

社交登录回调

社交登录完成

谷粒商城分布式高级篇(中)_第30张图片

社交登录测试成功

  1. 用户点击页面的授权页链接,跳转至微博授权页
<a href="https://api.weibo.com/oauth2/authorize?client_id=4074626065&response_type=code&redirect_uri=http://auth.gulimall.com/oauth2.0/weibo/success">授权页地址a>
  1. 如果用户同意授权,页面跳转至 http://auth.gulimall.com/oauth2.0/weibo/success/?code=CODE
  2. 自身的回调接口地址 接收 code 以做下一步用
@GetMapping("/oauth2.0/weibo/success")
public String weibo(@RequestParam("code") String code)
  1. 接口内封装所需数据换取Access Token
Map<String, String> map = new HashMap<>();
map.put("client_id", "4074626065");//和login.html的要保持一致
map.put("client_secret", "163a1200816d951ed4bf88735469c573");
map.put("grant_type", "authorization_code");
map.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/weibo/success");
map.put("code", code);
//1 根据 code 换取 access_token 能获取则成功
HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", new HashMap<>(), map, new HashMap<>());

获得的token数据,注意 code 只能使用一次,而access_token能用多次

{
     
    "access_token": "2.008kSAYFP4ik8Ecd802dd8f0h5eOeE",
    "remind_in": "157679999",
    "expires_in": 157679999,
    "uid": "5083131655",
    "isRealName": "true"
}
  1. 拿到token 可以根据微博已开放的接口,换取用户数据
    比如获取用户的昵称性别
//查询当前社交用户的社交账号(昵称,性别等)
Map<String, String> query = new HashMap<>();
query.put("access_token", vo.getAccess_token());
query.put("uid", vo.getUid());
HttpResponse response = HttpUtils.doGet("https://api.weibo.com/", "/2/users/show.json", "get", new HashMap<String, String>(), query);

分布式session不共享不同步问题

谷粒商城分布式高级篇(中)_第31张图片

谷粒商城分布式高级篇(中)_第32张图片

分布式session解决方案原理

谷粒商城分布式高级篇(中)_第33张图片
不推荐使用
谷粒商城分布式高级篇(中)_第34张图片
不推荐使用


谷粒商城分布式高级篇(中)_第35张图片
谷粒商城分布式高级篇(中)_第36张图片


谷粒商城分布式高级篇(中)_第37张图片
解决域名之间cookie不能共享,设置cookie时就让 cookie的 domain 为主域名,这样子域名设置的cookie主域名也能访问

SpringSession整合

  1. 引入依赖 整合Spring Session完成session共享问题 微服务自治,就不放在common里了
  2. 配置存储类型喝过期时间
spring.session.store-type=redis
server.servlet.session.timeout=30m
  1. 启动类设置启用注解
    谷粒商城分布式高级篇(中)_第38张图片
  2. 启动后走登录流程,发现报错,错误为序列化错误,因为spring-session要将对象序列化后再存储到redis,所以要存储的对象必须实现 Serializable
    在这里插入图片描述
    谷粒商城分布式高级篇(中)_第39张图片
  3. gulimall-product也如上整合spring-session,再次启动发现报错序列化异常SerializationException这一次的序列化异常是因为 gulimall-product 要取存入的MemberRespVo 而 此模块中没有这个类,所以解决办法是将这个类放入公共模块中,让两个模块都能访问到

自定义SpringSession完成子域session共享

创建配置类,解决子域共享问题

@Configuration
public class GulimallSessionConfig {
     
    /**
     * 自定义session作用域:整个网站
     * 使用一样的session配置,能保证全网站共享一样的session
     */
    @Bean
    public CookieSerializer cookieSerializer() {
     

        DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();

        defaultCookieSerializer.setDomainName("gulimall.com");
        defaultCookieSerializer.setCookieName("GULISESSION");

        return defaultCookieSerializer;
    }
    /**
     * 序列化机制
     */
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
     

        return new GenericJackson2JsonRedisSerializer();
    }

}

SpringSession原理

谷粒商城分布式高级篇(中)_第40张图片

页面效果完成

给 search模块加入session

  1. 引入 spring-session,和redis
    谷粒商城分布式高级篇(中)_第41张图片
  2. 配置redis地址和缓存类型为redis
spring:
  redis:
    host: 192.168.56.10
  session:
    store-type: redis
  1. 启动类开启redis-session @EnableRedisHttpSession
  2. 放入配置好的配置类
@Configuration
public class GulimallSessionConfig {
     
    /**
     * 自定义session作用域:整个网站
     * 使用一样的session配置,能保证全网站共享一样的session
     */
    @Bean
    public CookieSerializer cookieSerializer() {
     

        DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();

        defaultCookieSerializer.setDomainName("gulimall.com");
        defaultCookieSerializer.setCookieName("GULISESSION");

        return defaultCookieSerializer;
    }
    /**
     * 序列化机制
     */
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
     

        return new GenericJackson2JsonRedisSerializer();
    }

}

  1. 页面获取

在这里插入图片描述

单点登录简介

一处登录,处处登录
谷粒商城分布式高级篇(中)_第42张图片

框架效果演示

  1. 下载 gitee 许雪里 / xxl-sso框架

  2. 更改项目中的redis地址和配置的域名
    在这里插入图片描述

  3. 添加本机测试域名

     127.0.0.1 ssoserver.com
     127.0.0.1 client1.com
     127.0.0.1 client2.com
    
  4. 在此框架根目录先清理再打包并跳过测试
    mvn clean package -Dmaven.skip.test=true

  5. 启动服务 xxl-sso-server
    在这里插入图片描述
    访问 http://ssoserver.com:8080/xxl-sso-server/login

  6. 启动 两个web 测试模块 xxl-sso-web-sample-springboot 分别是 8081和8082端口在这里插入图片描述
    访问 http://client1.com:8081/xxl-sso-web-sample-springboot/
    可以测试一处登录处处登录了

单独打包一个模块时发生错误。无法解析xxl-sso-core 这个包
在这里插入图片描述
解决:将这个包安装到仓库
在这里插入图片描述
再打包还是没解决问题,因为还得安装所有所依赖的父项目,那就干脆在根目录全部重新打

单点登录流程-1

谷粒商城分布式高级篇(中)_第43张图片
项目中的测试
加入 web和lombok
谷粒商城分布式高级篇(中)_第44张图片
加入web Lombok 和 thyme leaf
谷粒商城分布式高级篇(中)_第45张图片

  1. 打开clien服务需要验证登录的地址,判断有无session信息,没有就跳转到server服务的登录页面,并附带当前地址的信息
    在这里插入图片描述
  2. sever服务登录页接收这个 redirect_url 值 并随页面提交一起提交

谷粒商城分布式高级篇(中)_第46张图片
谷粒商城分布式高级篇(中)_第47张图片

单点登录流程-2

  1. server服务登录成功后给重定向的地址加一个 token参数并将此参数存入redis
    谷粒商城分布式高级篇(中)_第48张图片
  2. 跳回到client的重定向地址判断有无token,有token就获取当前token的真正信息,至此,这两个交互的服务登录成功,但是要做到一处登录处处登录,还要让第三个没有参与到的服务页能直接登录成功才能算单点登录
    谷粒商城分布式高级篇(中)_第49张图片

单点登录流程-3

  1. 直接复制client服务(或者重新创建一个),作为第三个服务,更改他的名字为,端口修改为8082
    在这里插入图片描述
  2. 重复上述登录步骤,跳转至server服务的登录地址,登录成功后加入cookie,这样用户的浏览器就会带有server服务的域名cookie信息,client1登录成功
    谷粒商城分布式高级篇(中)_第50张图片
    谷粒商城分布式高级篇(中)_第51张图片
  3. clien2 如 client1 一样步骤,因为没在此服务的域名做任何操作,所以还是如client1一样直接跳转到server服务登录地址
    在这里插入图片描述
  4. 在显示登录页前,先判断之前是否有其他服务登录过,因为之前登录成功在此域名存入了cookie作为标识,所以得到之前有登录过的结果,带上cookie中的token信息重定向回来时页面
    谷粒商城分布式高级篇(中)_第52张图片
  5. 返回到原页面,判断是否拿到token值,通过token值查询到保存在redis中的用户信息,至此 单点登录完成
    谷粒商城分布式高级篇(中)_第53张图片

购物车

环境搭建

创建购物车服务

  1. 创建模块
    谷粒商城分布式高级篇(中)_第54张图片

  2. 添加host,和nginx配置
    谷粒商城分布式高级篇(中)_第55张图片
    谷粒商城分布式高级篇(中)_第56张图片

  3. 上传静态资源至nginx并修改html中的资源地址

  4. cart服务引入common模块,配置nacos地址服务名,端口号,启动类加入@EnableFeignClients 开启服务注册发现,和开启远程调用 @EnableFeignClients

     server.port=30000
     spring.application.name=gulimall-cart
     spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
    
  5. 添加网关的负载均衡,改一个index.html的页面
    谷粒商城分布式高级篇(中)_第57张图片
    谷粒商城分布式高级篇(中)_第58张图片

  6. 启动网关和cart 就可访问cart了

数据模型分 析

谷粒商城分布式高级篇(中)_第59张图片
谷粒商城分布式高级篇(中)_第60张图片
谷粒商城分布式高级篇(中)_第61张图片

VO编写

/**
 * 整个购物车
 * 需要计算的属性,都需要重写get方法
 */
public class Cart {
     
    List<CartItem> items;
    //全部sku的总数 3+3=6 商品数量
    private Integer countNum;
    //共有多少种类型
    private Integer countType;
    //所有sku总价
    private BigDecimal totalAmount;
    //优惠价格
    private BigDecimal reduce = new BigDecimal("0");
    
    public Integer getCountNum() {
     
        int count = 0;
        if (items != null && items.size() > 0) {
     
            for (CartItem item : items) {
     
                count += item.getCount();
            }
        }
        return count;
    }


    public Integer getCountType() {
     
        int countType = 0;
        if (items != null && items.size() > 0) {
     
            for (CartItem item : items) {
     
                countType += 1;
            }
        }
        return countType;
    }

    public BigDecimal getTotalAmount() {
     
        BigDecimal amount = new BigDecimal("0");
        //计算购物项总价
        if (items != null && items.size() > 0) {
     
            for (CartItem item : items) {
     
                BigDecimal totalPrice = item.getTotalPrice();
                amount = amount.add(totalPrice);
            }
        }
        //减去优惠总价
        BigDecimal subtract = amount.subtract(getReduce());
        return subtract;
    }
}

ThreadLocal用户身份鉴别

还是整合spring-session 加入依赖和配置类,判断是否登录只用从session获取当前用户

 * 浏览器有一个cookie,user-key用来标识临时用户信息,一个月过期
 * 如果第一次使用jd的购物车功能,都会给一个临时的用户身份
 * 浏览器保存以后,每次访问都会带上

京东的判断登录逻辑
登录:session有
没登录:安照cookie李米娜带来的user-key来做
第一次:如果没有临时用户,帮忙创建一个临时用户
登录了有id,没登录有user-key

编写一个拦截器,在执行目标方法之前,判断用户的登录状态,并封装传递给controller目标请求
编写的方法,创建一个类,实现 spring-mvcHandlerInterceptor , 关键 的方法实现preHandle

public class CartInterceptor implements HandlerInterceptor {
     
    /**
     * 目标方法执行之前
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
     
        HttpSession session = request.getSession();
        MemberRespVo member = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
        if (member != null){
     
            //用户登录
        }else {
     
            //用户没登录
        }
        return false;
    }

注册这个拦截器需要创建一个配置类 并添加需要拦截的路径地址,这里拦截所有请求

@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
     
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
     
        registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");
    }
}

同一个线程共享数据
谷粒商城分布式高级篇(中)_第62张图片
ThreadLocal快速获取用户信息
在拦截器中注入一个 ThreadLocal 的线程,里面存的 UserInfo
谷粒商城分布式高级篇(中)_第63张图片
目标方法返回之前,将信息存入 ThreadLocal
谷粒商城分布式高级篇(中)_第64张图片
Controller 或其他任何方法的获取,因为是静态的

    //1、快速得到用户信息。登录了有id,没登录有user-key
    UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();

添加购物车

前端页面拼接地址,携带catId和数量
location.href = "http://cart.gulimall.com/addToCart?skuId="+skuId+"&num="+val;

controller封装了一个redis的查询操作,返回当前用户存储在reids 的购物车数据

/**
 * 获取我们要操作的购物车
 * @return
 */
private BoundHashOperations<String, Object, Object> getCartOps() {
     
    UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
    //1、
    String cartKey = "";
    if (userInfoTo.getUserId() != null) {
     
        //gulimall:cart:1
        cartKey = CART_PREFIX + userInfoTo.getUserId();
    } else {
     
        cartKey = CART_PREFIX + userInfoTo.getUserKey();
    }

    return redisTemplate.boundHashOps(cartKey);

}

远程查询商品信息和商品属性的组合信息
谷粒商城分布式高级篇(中)_第65张图片并做好这两个查询的异步编排
导入了两个线程池的配置
谷粒商城分布式高级篇(中)_第66张图片

加入购物车时报错数字转化异常
在这里插入图片描述
debug发现购物车对象完全为空,原因为异步问题,异步还没执行完就运行结束了所以对象为空,要等到异步的查询完成后再装入对象

谷粒商城分布式高级篇(中)_第67张图片
等到getSkuSaleAttrValuesgetSkuInfoTask这两个异步任务都完成后再往redis添加数据
谷粒商城分布式高级篇(中)_第68张图片

RedirectAttribute

避免刷新导致重复添加,添加购物车,和添加成功分为两个页面操作

谷粒商城分布式高级篇(中)_第69张图片

获取&合并购物车

点击我的购物车后,主要做判断,登录和未登录,未登录就查询临时购物车数据,登录了,就需要合并临时和用户购物车,合并后清除临时购物车

选中购物项

点击勾选后跳转至处理请求,处理完成后重定向回购物车列表

@GetMapping("/checkItem")
public String checkItem(@RequestParam("skuId") Long skuId,@RequestParam("check") Integer check){
     
    cartService.checkItem(skuId,check);
    return "redirect:http://cart.gulimall.com/cart.html";
}

改变购物项数量

同上

删除购物项

同上

消息队列

MQ简介

谷粒商城分布式高级篇(中)_第70张图片
谷粒商城分布式高级篇(中)_第71张图片

RabbitMQ简介

谷粒商城分布式高级篇(中)_第72张图片

谷粒商城分布式高级篇(中)_第73张图片
谷粒商城分布式高级篇(中)_第74张图片
谷粒商城分布式高级篇(中)_第75张图片
谷粒商城分布式高级篇(中)_第76张图片

RabbitMQ工作流程

谷粒商城分布式高级篇(中)_第77张图片
谷粒商城分布式高级篇(中)_第78张图片
谷粒商城分布式高级篇(中)_第79张图片

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 update rabbitmq --restart=always
谷粒商城分布式高级篇(中)_第80张图片
访问虚拟机的15672端口就可打开rabbitmq的web端 http://192.168.56.10:15672/默认账号密码 guest

Exchange类型

谷粒商城分布式高级篇(中)_第81张图片
谷粒商城分布式高级篇(中)_第82张图片
谷粒商城分布式高级篇(中)_第83张图片
创建一个交换机
谷粒商城分布式高级篇(中)_第84张图片
创建一个队列
谷粒商城分布式高级篇(中)_第85张图片
交换机中绑定刚才创建的队列
谷粒商城分布式高级篇(中)_第86张图片

Direct-Exchange 点对点

点对点 直接类型交换机
创建4个实验队列
谷粒商城分布式高级篇(中)_第87张图片
创建一个direct交换机
谷粒商城分布式高级篇(中)_第88张图片
依次绑定刚创建的队列
谷粒商城分布式高级篇(中)_第89张图片
发送一则消息
谷粒商城分布式高级篇(中)_第90张图片
获取消息,第一种获取后不消失,第二种获取后消失
谷粒商城分布式高级篇(中)_第91张图片

Fanout-Exchange 扇出类型

扇出类型交换机
谷粒商城分布式高级篇(中)_第92张图片
绑定四个测试队列
谷粒商城分布式高级篇(中)_第93张图片
发送一个路由键为 emps的,结果四个队列都收到了
谷粒商城分布式高级篇(中)_第94张图片
谷粒商城分布式高级篇(中)_第95张图片
而且不写路由键,绑定的路由键也都能收到

Topic-Exchange 主题类型

主题类型交换机
谷粒商城分布式高级篇(中)_第96张图片
实现这种绑定效果,分别是 atguigu.#*.news
谷粒商城分布式高级篇(中)_第97张图片
谷粒商城分布式高级篇(中)_第98张图片谷粒商城分布式高级篇(中)_第99张图片
谷粒商城分布式高级篇(中)_第100张图片
发送一个 atguigu.news 路由键的消息,四个队列都接收到了,因为 atguigu.news 满足了两种绑定效果
谷粒商城分布式高级篇(中)_第101张图片
谷粒商城分布式高级篇(中)_第102张图片
若发送 hello.news 路由键的消息,就只有 一个满足了调教的接收到了
谷粒商城分布式高级篇(中)_第103张图片

SpringBoot整合RabbitMQ

谷粒商城分布式高级篇(中)_第104张图片
谷粒商城分布式高级篇(中)_第105张图片
配置文件中添加配置
在这里插入图片描述
启动类开启注解
谷粒商城分布式高级篇(中)_第106张图片

AmqpAdmin使用

  1. 创建交换机
    AmqpAdmin 创建交换机,declareExchange方法声明一个交换机,传入一个 Exchange 接口类型的类,Exchange接口的具体实现有这几种,就是之前之前客户创建时可选的那几个类型,创建一个 简单的 DirectExchange 类型 ,他有三种构造函数,全参的就是之前客户端创建时的几种选项,String name, boolean durable, boolean autoDelete, Map arguments 分别是 名字,是否持久化(关闭RebbitMq后再打开还存在),是否自动删除,指定的参数
    在这里插入图片描述
    谷粒商城分布式高级篇(中)_第107张图片
    谷粒商城分布式高级篇(中)_第108张图片
@Autowired
AmqpAdmin amqpAdmin;
@Test
public void createExchange() {
     
    //String name, boolean durable, boolean autoDelete, Map arguments
    DirectExchange directExchange = new DirectExchange("hello-java-exchange",true,false);
    amqpAdmin.declareExchange(directExchange);
    System.out.println("Exchange[hello-java-exchange]创建成功");
}
  1. 创建队列

全参构造器,String name, boolean durable, boolean exclusive, boolean autoDelete, Map arguments
名字,持久化,是否排他,自动删除,运行成功后 客户端可查看 创建成功
谷粒商城分布式高级篇(中)_第109张图片
3. 创建绑定
Binding 类只有一个构造函数
谷粒商城分布式高级篇(中)_第110张图片
客户端查看绑定成功
谷粒商城分布式高级篇(中)_第111张图片

RabbitTemplate使用

谷粒商城分布式高级篇(中)_第112张图片
客户端可看到
谷粒商城分布式高级篇(中)_第113张图片
谷粒商城分布式高级篇(中)_第114张图片
还可以发送对象,但是对象必须实现序列化接口
谷粒商城分布式高级篇(中)_第115张图片
谷粒商城分布式高级篇(中)_第116张图片
可以将对象转换成json发送,需要一个转化配置类,配置好转化配置类则自动转化为json了

谷粒商城分布式高级篇(中)_第117张图片
谷粒商城分布式高级篇(中)_第118张图片

RabbitListener&RabbitHandler接收消息

监听消息:使用@RabbitListener 必须有 @EnableRabbit
@RabbitListener标注在业务逻辑组件上,且组件在容器中
在任意一个 service实现中 编写一个监听器,用Object 接收这个消息获取他的类型,发现类型为class org.springframework.amqp.core.Message
谷粒商城分布式高级篇(中)_第119张图片
在这里插入图片描述
就可以将接收 Message 类型的数据,还可以直接将其中的对象直接写在参数上,spring回自动转换
谷粒商城分布式高级篇(中)_第120张图片
还可以传入当前通道
谷粒商城分布式高级篇(中)_第121张图片
测试多个服务监听这个队列,启动多个相同的服务,发送多个消息
谷粒商城分布式高级篇(中)_第122张图片
谷粒商城分布式高级篇(中)_第123张图片
发现一个消息只能被一个客户端接收
谷粒商城分布式高级篇(中)_第124张图片
接收消息时休眠此线程,测试是否能正常接收,发现只有一个消息完全处理完,方法运行结束才可以接收下一个消息
谷粒商城分布式高级篇(中)_第125张图片
@RabbitListener能标注在类和方法上(监听哪些队列)
@RabbitHandler能标注在方法上 (重载区分不同类型的消息)
直接重载了
谷粒商城分布式高级篇(中)_第126张图片
谷粒商城分布式高级篇(中)_第127张图片

谷粒商城分布式高级篇(中)_第128张图片
谷粒商城分布式高级篇(中)_第129张图片

可靠投递-发送端确认

谷粒商城分布式高级篇(中)_第130张图片
谷粒商城分布式高级篇(中)_第131张图片
设置发送端消息抵达Broker的确认
在这里插入图片描述
在配置类中定制RabbitTemplate设置ConfirmCallback


@Configuration
public class MyRabbitConfig {
     

    @Autowired
    RabbitTemplate rabbitTemplate;
    /**
     * 定制 RabbitTemplate
     * 1、服务器收到消息就回调
     *      1、spring.rabbitmq.publisher-confirms=true
     *      2、设置确认回调 ConfirmCallback
     * 2、消息正确抵达队列
     *      1、spring.rabbitmq.publisher-returns=true
     *         spring.rabbitmq.template.mandatory=true
     *      2、设置确认回调 ReturnCallback
     *
     * @PostConstruct MyRabbitConfig对象创建完成后执行这个方法
     */
    @PostConstruct
    public void initRabbitTemplate() {
     
        //设置消息抵达Broker的确认回调
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
     
            /**
             * 1、只要消息抵达Broker ack就为true 不管是否有消费者都会回调
             * @param correlationData 当前消息的唯一关联数据(这个是消息的唯一id)
             * @param ack 消息是否成功收到
             * @param cause 失败的原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
     
                System.out.println("confirm...correlationData[" + correlationData + "],=>b[" + ack + "],=>s[" + cause + "]");

            }
        });

correlationData 消息的唯一id如何获取。在消息发送时可以携带这个类,类中的值指定一个字符串为唯一id
谷粒商城分布式高级篇(中)_第132张图片

//1、发送消息
rabbitTemplate.convertAndSend("hello-java-exchange", "hello.java", res, new CorrelationData(UUID.randomUUID().toString()));

设置发送端抵达队列的确认
谷粒商城分布式高级篇(中)_第133张图片
在配置类中定制RabbitTemplate设置ReturnCallback

//设置消息抵达队列的确认回调
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
     
    /**
     * 只有消息没有投递给指定的队列,才会触发这个失败回调
     * @param message    投递失败的消息详细信息
     * @param replyCode  恢复的状态码
     * @param replyText  恢复的文本内容
     * @param exchange   当时发给哪个交换机
     * @param routingKey 当时用的路由键
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
     
        System.out.println("Fail message[" + message + "] replyCode[" + replyCode + "] replyText[" + replyText + "] exchange[" + exchange + "] routingKey[" + routingKey + "]");
    }
});

谷粒商城分布式高级篇(中)_第134张图片
测试失败投递,最简单的失败在投递消息时错误指定路由键,这种错误信息回复码为312,NOT_ROUTE
在这里插入图片描述

可靠投递-消费端确认

谷粒商城分布式高级篇(中)_第135张图片
ack是自动的,默认是自动确认的,只要消息被接收到,客户端会自动确认,服务端就会移除这个消息
这种自动确认的问题:debug启动,发送5个消息,在接收这个消息的地方断点,模拟宕机断点运行到这里并处理完一个消息后就结束这个服务,发现消息5个消息都被自动回复了,发生了消息丢失

解决方法设置开启手动确认模式,只要没有明确告诉MQ消息被签收。没有ACK,消息就一直是unacked状态。即使Consumer 消费端宕机。消息也不会丢失,重置为Ready状态,下一次有新的Consumer连接进来就发给他

#手动ack消息
spring.rabbitmq.listener.simple.acknowledge-mode=manual

如何签收这个手动模式的消息
Channel 通道中有一个方法 channel.basicAck(); 确认签收方法,此方法传入一个 long deliveryTag 和 布尔值,deliveryTag则在 messagemessage.getMessageProperties().getDeliveryTag(); 获取,每一个传入的消息都有一个deliveryTag 是自增的 ,布尔值则表示是否批量签收
谷粒商城分布式高级篇(中)_第136张图片
重新设计了签收逻辑,保证部分消息没有签收到
谷粒商城分布式高级篇(中)_第137张图片
就发现,队列中没有被签收的消息还在Unacked状态等待签收
谷粒商城分布式高级篇(中)_第138张图片
关闭服务模拟宕机后消息回到 Ready 准备状态 没有消失
谷粒商城分布式高级篇(中)_第139张图片
也能调用channel.basicNack(deliveryTag,false,false); 手动拒绝签收消息,第三个参数为是否重新入队,重新入队后会回到队列中当即消费,若不重新入队则直接丢弃,服务器也不会再等待接收
谷粒商城分布式高级篇(中)_第140张图片
总结
业务成功完成就应该签收 channel.basicAck(deliveryTag,false);
业务失败就拒签channel.basicNack(deliveryTag,false,false);

你可能感兴趣的:(java基础,学习笔记,技巧,java)