谷粒商城-服务3

29、商品上架和ES的存储模型选择

上架概念:我们把商品存入es的过程叫上架,只有上架的商品才能被前台检索
es的数据保存位置:内存
对es的使用我们不能把所有的数据都放在进来,因为内存时很贵的,我们需要有用的信息放进来,
商品es的存储模型方案:
模型一:占用空间多

{
    
    
	skuId:1
	spuId:11
	skuName:华为xxx
	attr:[
		{
    
    
		尺寸:5
		颜色:红色
		。。。
		。。。
		}
	]
}

模型二:查询时间长

sku索引{
    
    }
spu索引{
    
    }

30、nested的是使用

es在存储数组时必须要用 nested 来指定数据类型,负责查询查询出不想查的值

PUT my-index-000002
{
    
    
  "mappings": {
    
    
    "properties": {
    
    
      "user": {
    
    
        "type": "nested" 
      }
    }
  }
}

PUT my-index-000002/_doc/1
{
    
    
  "group" : "fans",
  "user" : [ 
    {
    
    
      "first" : "John",
      "last" :  "Smith"
    },
    {
    
    
      "first" : "Alice",
      "last" :  "White"
    }
  ]
}

GET my-index-000002/_search
{
    
    
  "query": {
    
    
    "bool": {
    
    
      "must": [
        {
    
     "match": {
    
     "user.first": "Alice" }},
        {
    
     "match": {
    
     "user.last":  "Smith" }}
      ]
    }
  }
}

31、Feigh的原理和流程

 // Feign 的调用流程 重复调用?接口幂等性:重试机制
 // 1. 构造请求,经对象转为json
 // 2. 发送请求进行执行(执行成功会解码响应)
 // 3. 执行请求会有重试机制,默认充实器处于关闭状态的

测试:略

32、动静分离

动:接口,链接数据库的请求
静:静态资源,图片,html,css,等
动态的东西应该放在微服务中,静态的东西放在nginx中,这样可以减少每个微服务tomcat的并发压力
前端的模板引擎用的时thymleaf,它的缺点是可能性能不如其他的模板引擎,但是我们可以通过缓存技术来优化它。

33、thymleaf的配置

spring:
    #关闭thymeleaf缓存
  thymeleaf:
    cache: false
    # 默认前缀
    prefix: classpath:/templates/
    # 默认后缀
    suffix: .html

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

34、自动更新静态文件

第一步:

<!--        自动给更新静态文件  true 这个才是关键-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

第二步:
ctrl+f9 重新编译项目
ctrl+shift+f9 重新编译当前页面

35、nginx反向代理

36、springCache 缓存的使用

37、Elasticsearch实现搜索功能

流程:
全文检索:
skuTitle-》keyword
排序:saleCount(销量)、hotScore(热度分)、skuPrice(价格)
过滤:hasStock、skuPrice区间、brandId、catalog3Id、attrs
聚合:attrs
一下式搜索条件

前台页面URL参数

keyword=小米&
sort=saleCount_desc/asc&
hasStock=0/1&
skuPrice=400_1900&
brandId=1&
catalog3Id=1&
attrs=1_3G:4G:5G&
attrs=2_骁龙845&
attrs=4_高清屏

搜索条件封装



@Data
public class SearchParam {
    
    

    /**
     * 页面传递过来的全文匹配关键字 skutitile
     */
    private String keyword;

    /**
     * 品牌id,可以多选
     */
    private List<Long> brandId;

    /**
     * 三级分类id
     */
    private Long catalog3Id;

    /**
     * 排序条件:sort=price/salecount/hotscore_desc/asc
     */
    private String sort;

    /**
     * 是否显示有货
     */
    private Integer hasStock;

    /**
     * 价格区间查询
     */
    private String skuPrice;

    /**
     * 按照属性进行筛选
     */
    private List<String> attrs;

    /**
     * 页码
     */
    private Integer pageNum = 1;

    /**
     * 原生的所有查询条件
     */
    private String _queryString;


}


搜索结果封装

@Data
public class SearchResult {
    
    

    /**
     * 查询到的所有商品信息
     */
    private List<SkuEsModel> product;


    /**
     * 当前页码
     */
    private Integer pageNum;

    /**
     * 总记录数
     */
    private Long total;

    /**
     * 总页码
     */
    private Integer totalPages;

    private List<Integer> pageNavs;

    /**
     * 当前查询到的结果,所有涉及到的品牌
     */
    private List<BrandVo> brands;

    /**
     * 当前查询到的结果,所有涉及到的所有属性
     */
    private List<AttrVo> attrs;

    /**
     * 当前查询到的结果,所有涉及到的所有分类
     */
    private List<CatalogVo> catalogs;


    //===========================以上是返回给页面的所有信息============================//


    /* 面包屑导航数据 */
    private List<NavVo> navs;

    @Data
    public static class NavVo {
    
    
        private String navName;
        private String navValue;
        private String link;
    }


    @Data
    public static class BrandVo {
    
    

        private Long brandId;

        private String brandName;

        private String brandImg;
    }


    @Data
    public static class AttrVo {
    
    

        private Long attrId;

        private String attrName;

        private List<String> attrValue;
    }


    @Data
    public static class CatalogVo {
    
    

        private Long catalogId;

        private String catalogName;
    }
}

es索引

PUT gulimall_product
{
    
    
  "mappings": {
    
    
    "properties": {
    
    
      "skuId": {
    
    
        "type": "long"
      },
      "spuId": {
    
    
        "type": "long"
      },
      "skuTitle": {
    
    
        "type": "text",
        "analyzer": "ik_smart"
      },
      "skuPrice": {
    
    
        "type": "keyword"
      },
      "skuImg": {
    
    
        "type": "keyword"
      },
      "saleCount": {
    
    
        "type": "long"
      },
      "hosStock": {
    
    
        "type": "boolean"
      },
      "hotScore": {
    
    
        "type": "long"
      },
      "brandId": {
    
    
        "type": "long"
      },
      "catelogId": {
    
    
        "type": "long"
      },
      "brandName": {
    
    
        "type": "keyword"
      },
      "brandImg": {
    
    
        "type": "keyword"
      },
      "catalogName": {
    
    
        "type": "keyword"
      },
      "attrs": {
    
    
        "type": "nested",
        "properties": {
    
    
          "attrId": {
    
    
            "type": "long"
          },
          "attrName": {
    
    
            "type": "keyword"
          },
          "attrValue": {
    
    
            "type": "keyword"
          }
        }
      }
    }
  }
}

38、使用CompletableFuture 配合线程池 来进行商品详情的异步查询

JDK1.8以后才可以使用CompletableFuture
场景:当我们请求商品详情的时候,需要请求很多信息,如

  1. sku的信息
  2. 促销信息
  3. 图片信息
  4. 属性信息
    假设每个请求都需要需要1s那么加起来也需要5s 显然是无法接收的
    这个时候我们就可以用异步请求,利用多个线程来同时请求这些数据,可能只需要1.5s就能完成。
    我们以后的业务代码中,对于比较耗时的操作,可以定义一两个线程池,每个异步任务提交到线程池里面,让它自己执行就行。
    在这里插入图片描述
    1、runXxxx 都是没有返回结果的,supplyXxx 都是可以获取返回结果的
    2、可以传入自定义的线程池,否则就用默认的线程池;
    3、方法中如果带有ASync 就表示是重新启动一个线程去执行,没有Async就表示还在原来的线程中执行。

38.1 感知异常的三种方式

whenComplete 虽然能得到异常信息,但是没法修改返回数据
exceptionally 可以感知异常,可以修改返回结果,但是拿不到
handle ★可感知异常,也可修改返回结果

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(()->{
		    System.out.println("maruis------>" + "异步请求完成。。。");
//		    int i = 10/0;
		    int i = 10/4;
		    return i;
	    },executor).whenComplete((res,e)->{
		    //虽然能得到异常信息,但是没法修改返回数据
		    System.out.println("maruis-----whenComplete->" + res);
		    System.out.println("maruis------>" + (e!=null?e.getMessage():"没问题"));
	    }).exceptionally((throwable)->{
		    //可以感知异常,同时返回默认值
		    System.out.println("maruis----exceptionally-->" + throwable.getMessage());
	    	return 404;
	    }).handle((res,throwable)->{
    
    
	    	// 可感知异常,也可修改返回结果
	    	if(res!=null){
    
    
	    		return res*2;
		    }else{
    
    
	    		return 0;
		    }
	    });
	    Integer integer = future.get();
	    System.out.println("maruis----result-->" + integer);

在这里插入图片描述

38.2 线程串行化

在这里插入图片描述
thenApply 方法:当一个线程依赖另一个线程时,获取上一个任务返回的结果,并返回当前
任务的返回值。
thenAccept 方法:消费处理结果。接收任务的处理结果,并消费处理,无返回结果。
thenRun 方法:只要上面的任务执行完成,就开始执行 thenRun,只是处理完任务后,执行
thenRun 的后续操作
带有 Async 默认是异步执行的。同之前。
以上都要前置任务成功完成。
Function<? super T,? extends U>
T:上一个任务返回结果的类型
示例

/**
	 * 串行化
	 */
	private static void thread_then() throws ExecutionException, InterruptedException {
    
    
		CompletableFuture<String> future = CompletableFuture.supplyAsync(()->{
			System.out.println("maruis------>" + "执行了第一个任务");
			//		    int i = 10/0;
			int i = 10/4;
			return i;
		},executor).whenComplete((res,e)->{
			//虽然能得到异常信息,但是没法修改返回数据
			System.out.println("maruis-----whenComplete->" + res);
			System.out.println("maruis------>" + (e!=null?e.getMessage():"没问题"));
		}).exceptionally((throwable)->{
			//可以感知异常,同时返回默认值
			System.out.println("maruis----exceptionally-->" + throwable.getMessage());
			return 404;
		}).handle((res,throwable)->{
			// 可感知异常,也可修改异常
			if(res!=null){
				return res*2;
			}else{
				return 0;
			}
		}).thenApplyAsync((res)->{
			System.out.println("maruis------>" + "执行了第二个任务");
			System.out.println("maruis------>" + "上次任务的返回结果:"+res);
			return "hello:"+res;
		},executor);
		System.out.println("maruis----result-->" + future.get());
	}

在这里插入图片描述

38.3 两任务组合 - 都要完成

在这里插入图片描述
两个任务必须都完成,触发该任务。
thenCombine:组合两个 future,获取两个 future 的返回结果,并返回当前任务的返回值
thenAcceptBoth:组合两个 future,获取两个 future 任务的返回结果,然后处理任务,没有
返回值。
runAfterBoth:组合两个 future,不需要获取 future 的结果,只需两个 future 处理完任务后,
处理该任务。

38.4 两任务组合 - 一个完成在这里插入图片描述

当两个任务中,任意一个 future 任务完成的时候,执行任务。
applyToEither:两个任务有一个执行完成,获取它的返回值,处理任务并有新的返回值。
acceptEither:两个任务有一个执行完成,获取它的返回值,处理任务,没有新的返回值。
runAfterEither:两个任务有一个执行完成,不需要获取 future 的结果,处理任务,也没有返
回值。

38.5、多任务组合

在这里插入图片描述
allOf:等待所有任务完成
anyOf:只要有一个任务完成
示例

/**
	 * 多任务组合
	 */
	private static void thread_duorenwu() throws ExecutionException, InterruptedException {
    
    
		CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(()->{
			System.out.println("maruis------>" + "查询商品图片信息");
			try {
				TimeUnit.SECONDS.sleep(2);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return "xxx.jpg";
		},executor);
		CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(()->{
			System.out.println("maruis------>" + "查询商品属性");
			try {
				TimeUnit.SECONDS.sleep(3);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return "黑色+256G";
		},executor);
		CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(()->{
			System.out.println("maruis------>" + "查询商品介绍");
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return "华为";
		},executor);
// 所有任务都执行才返回
//		 CompletableFuture.allOf(futureImg, futureDesc, futureAttr).get();
//		System.out.println("maruis----result-->" + futureImg.get()+futureAttr.get()+futureDesc.get());
// 只要有一个执行完就会返回
		Object o = CompletableFuture.anyOf(futureImg, futureDesc, futureAttr).get();
		System.out.println("maruis----result-->" + o.toString());
	}

在这里插入图片描述

38.6 最佳实战-商品详情的异步编排

第一步:设置线程池
1.1 pom文件中配置文件的代码提示

<!--        配置文件的代码提示-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

1.2 写一个线程池bean,用于公共调用

@EnableConfigurationProperties(ThreadPoolConfigProperties.class)
@Configuration
public class MyThreadConfig {
    
    


    @Bean
    public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool) {
    
    
        return new ThreadPoolExecutor(
                pool.getCoreSize(),
                pool.getMaxSize(),
                pool.getKeepAliveTime(),
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(100000),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );
    }

}

1.3 添加配置类
@ConfigurationProperties(prefix = “gulimall.thread”)
// @Component
@Data
public class ThreadPoolConfigProperties {

private Integer coreSize;

private Integer maxSize;

private Integer keepAliveTime;

};

1.4 application.properties 中配置线程参数

#配置线程池
gulimall.thread.coreSize=20
gulimall.thread.maxSize=200
gulimall.thread.keepAliveTime=10

第二步:注入线程池,编写商品详情逻辑

 /**
     * 多线程,异步请求
     * @param skuId
     * @return
     * @throws InterruptedException
     * @throws ExecutionException
     */
    @Resource
    private ThreadPoolExecutor executor ;
    private SkuItemVo getSkuItemVoThread(Long skuId) throws InterruptedException, ExecutionException {
    
    
        long start = System.currentTimeMillis();
        SkuItemVo skuItemVo = new SkuItemVo();

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


        CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
            //3、获取spu的销售属性组合
            List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrBySpuId(res.getSpuId());
            skuItemVo.setSaleAttr(saleAttrVos);
        }, executor);


        CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((res) -> {
            //4、获取spu的介绍    pms_spu_info_desc
            SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
            skuItemVo.setDesc(spuInfoDescEntity);
        }, executor);


        CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> {
            //5、获取spu的规格参数信息
            List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
            skuItemVo.setGroupAttrs(attrGroupVos);
        }, executor);


        // Long spuId = info.getSpuId();
        // Long catalogId = info.getCatalogId();

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

//        CompletableFuture<Void> seckillFuture = CompletableFuture.runAsync(() -> {
//            //3、远程调用查询当前sku是否参与秒杀优惠活动
//            R skuSeckilInfo = seckillFeignService.getSkuSeckilInfo(skuId);
//            if (skuSeckilInfo.getCode() == 0) {
//                //查询成功
//                SeckillSkuVo seckilInfoData = skuSeckilInfo.getData("data", new TypeReference<SeckillSkuVo>() {
//                });
//                skuItemVo.setSeckillSkuVo(seckilInfoData);
//
//                if (seckilInfoData != null) {
//                    long currentTime = System.currentTimeMillis();
//                    if (currentTime > seckilInfoData.getEndTime()) {
    
    
//                        skuItemVo.setSeckillSkuVo(null);
//                    }
//                }
//            }
//        }, executor);


        //等到所有任务都完成
//        CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture,seckillFuture).get();
        CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture).get();
        System.out.println("maruis----商品详情用时:-->" + (System.currentTimeMillis()-start)+"毫秒");
        return skuItemVo;
    }

39、认证中心(社交登录,OAuth2.0,单点登录)

39.1 验证码

使用的是阿里云的 短信服务
由于阿里的短信服务的签名和模板需要审核,不支持个人用户申请未上线业务,若产品未上线建议先升级企业账号,
所以我们暂不实现此功能。
验证码的总体思路,为了防止有人获取到appcode后恶意消耗我们的验证码,所以验证码应该要发送验证码应该发送给我们自己的服务
我们自己的服务中去给第三发发送验证码和进行验证。
1.接口防刷
2.验证码的再次校验

@ResponseBody
    @GetMapping(value = "/sms/sendCode")
    public R sendCode(@RequestParam("phone") String phone) {
    
    

        //1、接口防刷
        String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
        if (!StringUtils.isEmpty(redisCode)) {
    
    
            //活动存入redis的时间,用当前时间减去存入redis的时间,判断用户手机号是否在60s内发送验证码
            long currentTime = Long.parseLong(redisCode.split("_")[1]);
            if (System.currentTimeMillis() - currentTime < 60000) {
    
    
                //60s内不能再发
                return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(),BizCodeEnum.SMS_CODE_EXCEPTION.getMessage());
            }
        }

        //2、验证码的再次效验 redis.存key-phone,value-code
        int code = (int) ((Math.random() * 9 + 1) * 100000);
        String codeNum = String.valueOf(code);
        String redisStorage = codeNum + "_" + System.currentTimeMillis();

        //存入redis,防止同一个手机号在60秒内再次发送验证码
        stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone,
                redisStorage,10, TimeUnit.MINUTES);

        thirdPartFeignService.sendCode(phone, codeNum);

        return R.ok();
    }

注册功能,注册时要验证验证码

 /**
     *
     * TODO: 重定向携带数据:利用session原理,将数据放在session中。
     * TODO:只要跳转到下一个页面取出这个数据以后,session里面的数据就会删掉
     * TODO:分布下session问题
     * RedirectAttributes:重定向也可以保留数据,不会丢失
     * 用户注册
     * @return
     */
    @PostMapping(value = "/register")
//    public String register(@Valid UserRegisterVo vos, BindingResult result,
//                           RedirectAttributes attributes) {
    
    
    public String register(UserRegisterVo vos, BindingResult result,
                           RedirectAttributes attributes) {
    
    

        //如果有错误回到注册页面
        if (result.hasErrors()) {
    
    
            Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
            attributes.addFlashAttribute("errors",errors);

            //效验出错回到注册页面
            return "redirect:http://auth.gulimall.com/reg.html";
        }

        //1、效验验证码
        String code = vos.getCode();
        R register = memberFeignService.register(vos);
            //成功
            return "redirect:http://auth.gulimall.com/login.html";
//        //获取存入Redis里的验证码
//        String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vos.getPhone());
//
//        if (!StringUtils.isEmpty(redisCode)) {
    
    
//            //截取字符串
//            if (code.equals(redisCode.split("_")[0])) {
    
    
//                //删除验证码;令牌机制
//                stringRedisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX+vos.getPhone());
//                //验证码通过,真正注册,调用远程服务进行注册
//                R register = memberFeignService.register(vos);
//                if (register.getCode() == 0) {
    
    
//                    //成功
//                    return "redirect:http://auth.gulimall.com/login.html";
//                } else {
    
    
//                    //失败
//                    Map<String, String> errors = new HashMap<>();
//                    errors.put("msg", register.getData("msg",new TypeReference<String>(){
    
    }));
//                    attributes.addFlashAttribute("errors",errors);
//                    return "redirect:http://auth.gulimall.com/reg.html";
//                }
//
//
//            } else {
    
    
//                //效验出错回到注册页面
//                Map<String, String> errors = new HashMap<>();
//                errors.put("code","验证码错误");
//                attributes.addFlashAttribute("errors",errors);
//                return "redirect:http://auth.gulimall.com/reg.html";
//            }
//        } else {
    
    
//            //效验出错回到注册页面
//            Map<String, String> errors = new HashMap<>();
//            errors.put("code","验证码错误");
//            attributes.addFlashAttribute("errors",errors);
//            return "redirect:http://auth.gulimall.com/reg.html";
//        }
    }

注册原理

第一步:发送验证码给服务器,服务器生成验证码,保存在redis中有过期时间为10分钟并发送给阿里云的短信服务
第二步:用户通过手机拿到验证码并填写
第三步:提交注册,服务器验证验证码的正确性,把用户出入系统

39.2 md5加密和盐值加密

md5加密时不可逆的,但是md5加密后会存在别人可以利用彩虹表进行暴力破解,比如把123,456等常用的md5密码列在一个表中进行暴力破解,为了防止这种现象,我们需要给md5加密后的密码再加上盐值。
测试:

@Test
	public void md5yanzhi(){
    
    
		String pass = "123456";
		// md5 加密只要原文一样,密文一定时一样的。
		// 利用这个特性,我们可以试下文件秒存,文件上前前我们可以获取这个文件的md5加密码,然后从数据库中找这个文件,如果找到了就不用上传了,变相实现了文件秒传
		String s = DigestUtils.md5Hex(pass);
		System.out.println("maruis------>" + s);
		// 盐值一样得到的结果就一样,想要更保险,可以把盐值设置成一个随机值,并保存在数据库中
		String s1 = Md5Crypt.md5Crypt(pass.getBytes(),"$1$qqqqqqqq");
		System.out.println("maruis------>" + s1);
		// BCryptPasswordEncoder 自动为我们实现了盐值加密,我们每次原文加密后的密文是不同的
		BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
		String p1 = bCryptPasswordEncoder.encode(pass);
		String p2 = bCryptPasswordEncoder.encode(pass);
		System.out.println("maruis----密码1-->" + p1);
		System.out.println("maruis----密码2-->" + p2);
		System.out.println("maruis----验证-->" + bCryptPasswordEncoder.matches(pass,p1));
		System.out.println("maruis----验证-->" + bCryptPasswordEncoder.matches(pass,p2));
	}

在这里插入图片描述

实例:

 @Override
    public void register(MemberUserRegisterVo vo) {
    
    

        MemberEntity memberEntity = new MemberEntity();

        //设置默认等级
        MemberLevelEntity levelEntity = memberLevelDao.getDefaultLevel();
        memberEntity.setLevelId(levelEntity.getId());

        //设置其它的默认信息
        //检查用户名和手机号是否唯一。感知异常,异常机制
        checkPhoneUnique(vo.getPhone());
        checkUserNameUnique(vo.getUserName());

        memberEntity.setNickname(vo.getUserName());
        memberEntity.setUsername(vo.getUserName());
        //密码进行MD5加密
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        String encode = bCryptPasswordEncoder.encode(vo.getPassword());
        memberEntity.setPassword(encode);
        memberEntity.setMobile(vo.getPhone());
        memberEntity.setGender(0);
        memberEntity.setCreateTime(new Date());

        //保存数据
        this.baseMapper.insert(memberEntity);
    }

在这里插入图片描述

40、OAuth2.0 (社交登录,单点登录)

40.1 社交登录原理

在这里插入图片描述

40.2 session 原理在这里插入图片描述

不同的域名,cookie是隔离的。
在这里插入图片描述

40.3 session 共享在分布式下存在的问题。

在这里插入图片描述

**在分布式下,session存在两个问题

  1. 同一个服务,复制多份,sessio不同步问题
  2. 不同服务,子域名session不能共享的问题。**

解决方案一:seesion复制(不退加)
在这里插入图片描述
方案二:客户端存储(不推荐)

在这里插入图片描述
方案三:hash一致性(可以)
在这里插入图片描述
★方案四:redis存储(推荐:解决1,2 两个问题)

在这里插入图片描述
官方文档:https://spring.io/projects/spring-session

第一步:添加依赖

        <!-- 整合springsession -->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

第二步:配置
2.1 application.properties 文件中配置

## 配置redis的连接信息
spring.redis.host=192.168.56.10
spring.redis.port=6379
## 配置springSession信息
spring.session.store-type=redis
server.servlet.session.timeout=30m

2.2 在Application 上添加
@EnableRedisHttpSession 注解

2.3 在config文件夹下添加文件配置

@Configuration
public class GulimallSessionConfig {
    
    

    @Bean
    public CookieSerializer cookieSerializer() {
    
    

        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();

        //放大作用域
        cookieSerializer.setDomainName("gulimall.com");
        // 设置cookie的名称
        cookieSerializer.setCookieName("GULISESSION");

        return cookieSerializer;
    }


	/**
	 * 用于json序列化
	 * @return
	 */
	@Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
    
    
        return new GenericJackson2JsonRedisSerializer();
    }

}

第三步:实例
1.后端保存session
在这里插入图片描述
2.前端使用

<a th:if="${session.loginUser != null}">欢迎, [[${session.loginUser.nickname}]]</a>

第四步:在其他微服务上想要拿到session,也必须实现第一步,和第二步 两个步骤。

40.4 spring-session 原理

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

41、点单登录(一处登录,处处可用)

在多个系统中,实现一处登录,处处登录
在这里插入图片描述

41.1 开源的单点登录

官网地址:https://gitee.com/xuxueli0323/xxl-sso?_from=gitee_search
在这里插入图片描述
在这里插入图片描述
打开服务器的配置
在这里插入图片描述
演示:
第一步:修改host

127.0.0.1    xxlssoserver.com
127.0.0.1    client1.com
127.0.0.1    client2.com

第二步:配置xxl-sso-server 认证中心的配置

1)改xxl-sso-server 项目中的application.properties 文件
xxl.sso.redis.address=redis://92.168.56.10:6379
2)修改xxl-sso-web-sample-springboot下的配置文件
xxl.sso.redis.address=redis://92.168.56.10:6379
xxl.sso.server=http://xxlssoserver.com:8000/xxl-sso-server

第三步:利用maven命令打包整个项目
mvn clean package -Dmanven.skip.test=true
D:\workerspace_idea_2019\xxl-sso\xxl-sso-server\target>java -jar xxl-sso-server-1.1.1-SNAPSHOT.jar --server.port=8000
D:\workerspace_idea_2019\xxl-sso\xxl-sso-samples\xxl-sso-web-sample-springboot\target>java -jar --server.port=8001

第四步:启动这个三项项目测试单点登录
D:\workerspace_idea_2019\xxl-sso\xxl-sso-samples\xxl-sso-web-sample-springboot\target>java -jar xxl-sso-web-sample-springboot-1.1.1-SNAPSHOT.jar --server.port
D:\workerspace_idea_2019\xxl-sso\xxl-sso-samples\xxl-sso-web-sample-springboot\target>java -jar xxl-sso-web-sample-springboot-1.1.1-SNAPSHOT.jar --server.port=8001
D:\workerspace_idea_2019\xxl-sso\xxl-sso-samples\xxl-sso-web-sample-springboot\target>java -jar xxl-sso-web-sample-springboot-1.1.1-SNAPSHOT.jar --server.port=8002

第五步:测试

xxlssoserver.com:8000/xxl-sso-server
client1.com:8001/xxl-sso-web-sample-springboot
client2.com:8002/xxl-sso-web-sample-springboot

在这里插入图片描述

41.2 、单点登录的核心原理

核心:三个系统及时域名不一样,想办法给三个系统同步同一个用户的票据
1)中央认证中心服务器:xxlssoserver.com
2)其他系统想要登录,去xxlssoserver.com登录,登录成功,跳转回来
3)只要有一个登录成功,其他就不用登录
4)全系统统一一个sso.sessionid,所有系统可能域名都不相同
在这里插入图片描述

在这里插入图片描述

42、购物车

42.1 购物车功能

  • 用户可以在登录状态下将商品添加到购物车
  • 用户可以在未登录状态下将商品添加到购物车
  • 用户可以使用购物车一起结算下单
  • 用户可以查询自己的购物车
  • 用户可以在购物车中修改购买商品的数量。
  • 用户可以在购物车中删除商品。
  • 在购物车中展示商品优惠信息
  • 提示购物车商品价格变化

42.2 存储方式:redis

临时数据的存储方式有很多,可以前端存储也可以后端存储,对于不重要的数据可以前端存储,对于重要的数据要后端存储。
前端存储可以放在这些里面:
在这里插入图片描述

由于购物车中的数据的频繁的写入和频繁的读取的,所以购物在存储的时候应该选用存取性能比较高的,所以我们要是redis的持久对购物车数据进行存储

42.3 购物车逻辑

ThreadLocal toThreadLocal = new ThreadLocal<>(); 同一个线程共享数据
在这里插入图片描述
拦截器的用法:https://blog.csdn.net/fen_dou_shao_nian/article/details/118641407
在这里插入图片描述
注意:如果浏览器中有cookei那么它的每次请求,浏览器自动会为我们带上这个cookie的。

加入购物车的逻辑,如果购物车中没有相同的商品,就添加,如果有就需要修改数量。

   @Override
    public CartItemVo addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
    
    

        //拿到要操作的购物车信息
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();

        //判断Redis是否有该商品的信息
        String productRedisValue = (String) cartOps.get(skuId.toString());
        //如果没有就添加数据
        if (StringUtils.isEmpty(productRedisValue)) {
    
    

            //2、添加新的商品到购物车(redis)
            CartItemVo cartItemVo = new CartItemVo();
            //开启第一个异步任务
            CompletableFuture<Void> getSkuInfoFuture = CompletableFuture.runAsync(() -> {
                //1、远程查询当前要添加商品的信息
                R productSkuInfo = productFeignService.getInfo(skuId);
                SkuInfoVo skuInfo = productSkuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {});
                //数据赋值操作
                cartItemVo.setSkuId(skuInfo.getSkuId());
                cartItemVo.setTitle(skuInfo.getSkuTitle());
                cartItemVo.setImage(skuInfo.getSkuDefaultImg());
                cartItemVo.setPrice(skuInfo.getPrice());
                cartItemVo.setCount(num);
            }, executor);

            //开启第二个异步任务
            CompletableFuture<Void> getSkuAttrValuesFuture = CompletableFuture.runAsync(() -> {
    
    
                //2、远程查询skuAttrValues组合信息
                List<String> skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId);
                cartItemVo.setSkuAttrValues(skuSaleAttrValues);
            }, executor);

            //等待所有的异步任务全部完成
            CompletableFuture.allOf(getSkuInfoFuture, getSkuAttrValuesFuture).get();

            String cartItemJson = JSON.toJSONString(cartItemVo);
            cartOps.put(skuId.toString(), cartItemJson);

            return cartItemVo;
        } else {
    
    
            //购物车有此商品,修改数量即可
            CartItemVo cartItemVo = JSON.parseObject(productRedisValue, CartItemVo.class);
            cartItemVo.setCount(cartItemVo.getCount() + num);
            //修改redis的数据
            String cartItemJson = JSON.toJSONString(cartItemVo);
            cartOps.put(skuId.toString(),cartItemJson);

            return cartItemVo;
        }
    }

批量操作redis的方法

 /**
     * 获取到我们要操作的购物车
     * @return
     */
    private BoundHashOperations<String, Object, Object> getCartOps() {
    
    
        //先得到当前用户信息
        UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();

        String cartKey = "";
        if (userInfoTo.getUserId() != null) {
    
    
            //gulimall:cart:1
            cartKey = CART_PREFIX + userInfoTo.getUserId();
        } else {
    
    
            cartKey = CART_PREFIX + userInfoTo.getUserKey();
        }

        //绑定指定的key操作Redis
        BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);

        return operations;
    }

在这里插入图片描述
订单流程图:
在这里插入图片描述

43 、订单服务

1)、需要登录 spring-session,redis
2)、线程池的配置
3)、锁库存,解锁库存
4)、幂等性
5)、解决feign调用时丢失请求头的问题。
6)、为了防止重复提交,添加token令牌,令牌获取,判断,删除时保证原子性。
7)、延时队列,保证最终一致性。

订单流程图
在这里插入图片描述
订单确认流程图
在这里插入图片描述
消息队列流程图
在这里插入图片描述

43.1 订单确认页逻辑

要点:
1)异步编排
2)Feign请求头丢失丢失,导致远程调用时处于未登录状态
3)方式订单重复提交的验证。
第一步:获取订单信息

 /**
     * 订单确认页返回需要用的数据
     * @return
     */
    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
    
    

        //构建OrderConfirmVo
        OrderConfirmVo confirmVo = new OrderConfirmVo();

        //获取当前用户登录的信息
        MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();

        //TODO :获取当前线程请求头信息(解决Feign异步调用丢失请求头问题)
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        //开启第一个异步任务
        CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {

            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);

            //1、远程查询所有的收获地址列表
            List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
            confirmVo.setMemberAddressVos(address);
        }, threadPoolExecutor);

        //开启第二个异步任务
        CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> {

            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);

            //2、远程查询购物车所有选中的购物项
            List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
            confirmVo.setItems(currentCartItems);
            //feign在远程调用之前要构造请求,调用很多的拦截器
        }, threadPoolExecutor).thenRunAsync(() -> {
            List<OrderItemVo> items = confirmVo.getItems();
            //获取全部商品的id
            List<Long> skuIds = items.stream()
                    .map((itemVo -> itemVo.getSkuId()))
                    .collect(Collectors.toList());

            //远程查询商品库存信息
            R skuHasStock = wmsFeignService.getSkuHasStock(skuIds);
            List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() {
    
    });

            if (skuStockVos != null && skuStockVos.size() > 0) {
    
    
                //将skuStockVos集合转换为map
                Map<Long, Boolean> skuHasStockMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                confirmVo.setStocks(skuHasStockMap);
            }
        },threadPoolExecutor);

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

        //4、价格数据自动计算

        //TODO 5、防重令牌(防止表单重复提交)
        //为用户设置一个token,三十分钟过期时间(存在redis)
        String token = UUID.randomUUID().toString().replace("-", "");
        redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES);
        confirmVo.setOrderToken(token);


        CompletableFuture.allOf(addressFuture,cartInfoFuture).get();

        return confirmVo;
    }

第二步:解决Feign调用丢失请求头的原因和解决办法在这里插入图片描述
解决办法:在当前微服务添加一个拦截器,当调用feign的时候就会先经过这个拦截器,在拦截器里面同步请求头
注意:如果调用feign的方法实在异步请求中,还要特别注意一下的写法才正确。
在这里插入图片描述

@Configuration
public class GuliFeignConfig {
    
    

    @Bean("requestInterceptor")
    public RequestInterceptor requestInterceptor() {
    
    

        RequestInterceptor requestInterceptor = new RequestInterceptor() {
    
    
            @Override
            public void apply(RequestTemplate template) {
    
    
                //1、使用RequestContextHolder拿到刚进来的请求数据
                ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

                if (requestAttributes != null) {
    
    
                    //老请求
                    HttpServletRequest request = requestAttributes.getRequest();

                    if (request != null) {
    
    
                        //2、同步请求头的数据(主要是cookie)
                        //把老请求的cookie值放到新请求上来,进行一个同步
                        String cookie = request.getHeader("Cookie");
                        template.header("Cookie", cookie);
                    }
                }
            }
        };

        return requestInterceptor;
    }

}

在这里插入图片描述
第三步:订单提交时解决重复提交问题,也叫做订单的幂等性
幂等性的相关概念和使用场景:https://blog.csdn.net/fen_dou_shao_nian/article/details/118750556
订单确认流程图:
在这里插入图片描述
防重令牌(防止表单重复提交)在这里插入图片描述

43.2 下单流程

提交订单时为了防止重复提交,要保证令牌获取,判断和删除的原子性

//1、验证令牌是否合法【令牌的对比和删除必须保证原子性】
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        String orderToken = vo.getOrderToken();

        //通过lure脚本原子验证令牌和删除令牌
        Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
                Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),
                orderToken);

第一步:构建订单项

   //1、商品的spu信息
   //2、商品的sku信息 
   //3、商品的优惠信息
   //4、商品的积分信息
   //5、订单项的价格信息
   //当前订单项的实际金额.总额 - 各种优惠价格
   //原来的价格
   //原价减去优惠价得到最终的价格

锁库存流程
在这里插入图片描述
在这里插入图片描述

43.3 支付功能

我们用支付宝来做演示,使用支付宝的沙箱功能进行演示
通过雷王穿透工具:续断:www.zhexi.tech
调用支付功能时一定要保证我们的项目是utf-8 的编码,否则支持可能不成功
内网穿透的几个常用软件
1、natapp:https://natapp.cn/ 优惠码:022B93FD(9 折)[仅限第一次使用]
2、续断:www.zhexi.tech 优惠码:SBQMEA(95 折)[仅限第一次使用]
3、花生壳:https://www.oray.com/

在这里插入图片描述
续断
nginx 配置

listen	80;
server_name gulimall.com *.gulimall.com 497n86m7k7.52http.net;

#charset koi8-r;
#access_log /var/log/nginx/log/host.access.log main;  location /static/ {
    
    
root	/usr/share/nginx/html;
}
location /payed/ {
    
    
proxy_set_header Host order.gulimall.com;  proxy_pass http://gulimall;
}

在这里插入图片描述
异步回调:
在这里插入图片描述
异步回调的方法

 @PostMapping(value = "/payed/notify")
    public String handleAlipayed(PayAsyncVo asyncVo, HttpServletRequest request) throws AlipayApiException, UnsupportedEncodingException {
    
    
        // 只要收到支付宝的异步通知,返回 success 支付宝便不再通知
        // 获取支付宝POST过来反馈信息
        //TODO 需要验签
        Map<String, String> params = new HashMap<>();
        Map<String, String[]> requestParams = request.getParameterMap();
        for (String name : requestParams.keySet()) {
    
    
            String[] values = requestParams.get(name);
            String valueStr = "";
            for (int i = 0; i < values.length; i++) {
    
    
                valueStr = (i == values.length - 1) ? valueStr + values[i]
                        : valueStr + values[i] + ",";
            }
            //乱码解决,这段代码在出现乱码时使用
            // valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
            params.put(name, valueStr);
        }

        boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayTemplate.getAlipay_public_key(),
                alipayTemplate.getCharset(), alipayTemplate.getSign_type()); //调用SDK验证签名

        if (signVerified) {
    
    
            System.out.println("签名验证成功...");
            //去修改订单状态
            String result = orderService.handlePayResult(asyncVo);
            return result;
        } else {
    
    
            System.out.println("签名验证失败...");
            return "error";
        }
    }

43.4 收单

1、订单在支付页,不支付,一直刷新,订单过期了才支付,订单状态改为已支付了,但是库 存解锁了。
使用支付宝自动收单功能解决。只要一段时间不支付,就不能支付了。
2、由于时延等问题。订单解锁完成,正在解锁库存的时候,异步通知才到
订单解锁,手动调用收单
3、网络阻塞问题,订单支付成功的异步通知一直不到达
查询订单列表时,ajax获取当前未支付的订单状态,查询订单状态时,再获取一下支付宝 此订单的状态
4、其他各种问题
每天晚上闲时下载支付宝对账单,一一进行对账

44 秒杀

44.1 在高并发系统里,要注意的问题。

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

1)单一职责,
秒杀服务即使自己扛不住压力, 挂掉。 不要影响别人
2)秒杀连接加密
防止恶意攻击, 模拟秒杀请求, 1000 次/ s攻击。
防止链接暴露, 自己工作人员, 提前秒杀商品
3)库存预热,快速扣减
秒杀读多写少。无需每次实时校验库存。我 们库存预热, 放到redis(或集群中)中。信号量控制进 来秒杀的请求
4)动静分离
nginx做好动静分离。保证秒杀和商品详情 页的动态请求才打到后端的服务集群。
使用CDN网络, 分担本集群压力(例如使用阿里云存储静态资源)
5)恶意请求的拦截
识别非法攻击请求并进行拦截, 网关层去拦截
6)流量错峰
使用各种手段, 将流量分担到更大宽度的时 间点。比如验证码, 加入购物车,由于有了这些操作,每个人的操作时间就会不一样,这样就可以实现流量的错峰。
7)限流&熔断&降级
前端限流(比如:只允许1s点击一次按钮)+ 后端限流(区分那些是用户的正常行为,哪些是恶意操作进行过滤)
限制次数, 限制总量, 快速失败降级运行, 熔断隔离防止雪崩
8)队列削峰-杀手锏
1 万个商品, 每个1000 件秒杀。双11
所有秒杀成功的请求, 进入队列, 慢慢创建 订单, 扣减库存即可。

44.2 上架流程

在这里插入图片描述

@Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {
    
    

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private CouponFeignService couponFeignService;

    @Autowired
    private ProductFeignService productFeignService;

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    private final String SESSION__CACHE_PREFIX = "seckill:sessions:";

    private final String SECKILL_CHARE_PREFIX = "seckill:skus";

    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";    //+商品随机码

    /**
     * 上架
     * 上架是扫描出最近3天需要上架的商品
     * 把场次信息 和  商品信息分别放在redis的两个key中
     * 把库存放在redis的信号量中,这个信号量类似与之前juc错线程中的人走了关门那个线程工具类
     */
    @Override
    public void uploadSeckillSkuLatest3Days() {
    
    

        //1、扫描最近三天的商品需要参加秒杀的活动
        R lates3DaySession = couponFeignService.getLates3DaySession();
        if (lates3DaySession.getCode() == 0) {
    
    
            //上架商品
            List<SeckillSessionWithSkusVo> sessionData = lates3DaySession.getData("data", new TypeReference<List<SeckillSessionWithSkusVo>>() {
    
    
            });
            //缓存到Redis
            //1、缓存活动信息
            saveSessionInfos(sessionData);

            //2、缓存活动的关联商品信息
            saveSessionSkuInfo(sessionData);
        }

    }

    /**
     * 缓存秒杀活动信息
     * @param sessions
     */
    private void saveSessionInfos(List<SeckillSessionWithSkusVo> sessions) {
    
    

        sessions.stream().forEach(session -> {
    
    

            //获取当前活动的开始和结束时间的时间戳
            long startTime = session.getStartTime().getTime();
            long endTime = session.getEndTime().getTime();

            //存入到Redis中的key
            String key = SESSION__CACHE_PREFIX + startTime + "_" + endTime;

            //判断Redis中是否有该信息,如果没有才进行添加
            Boolean hasKey = redisTemplate.hasKey(key);
            //缓存活动信息
            if (!hasKey) {
    
    
                //获取到活动中所有商品的skuId
                List<String> skuIds = session.getRelationSkus().stream()
                        .map(item -> item.getPromotionSessionId() + "-" + item.getSkuId().toString()).collect(Collectors.toList());
                redisTemplate.opsForList().leftPushAll(key,skuIds);
            }
        });

    }

    /**
     * 缓存秒杀活动所关联的商品信息
     * @param sessions
     */
    private void saveSessionSkuInfo(List<SeckillSessionWithSkusVo> sessions) {
    
    

        sessions.stream().forEach(session -> {
    
    
            //准备hash操作,绑定hash
            BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
            session.getRelationSkus().stream().forEach(seckillSkuVo -> {
    
    
                //生成随机码
                String token = UUID.randomUUID().toString().replace("-", "");
                String redisKey = seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString();
                if (!operations.hasKey(redisKey)) {
    
    

                    //缓存我们商品信息
                    SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
                    Long skuId = seckillSkuVo.getSkuId();
                    //1、先查询sku的基本信息,调用远程服务
                    R info = productFeignService.getSkuInfo(skuId);
                    if (info.getCode() == 0) {
    
    
                        SkuInfoVo skuInfo = info.getData("skuInfo",new TypeReference<SkuInfoVo>(){
    
    });
                        redisTo.setSkuInfo(skuInfo);
                    }

                    //2、sku的秒杀信息
                    BeanUtils.copyProperties(seckillSkuVo,redisTo);

                    //3、设置当前商品的秒杀时间信息
                    redisTo.setStartTime(session.getStartTime().getTime());
                    redisTo.setEndTime(session.getEndTime().getTime());

                    //4、设置商品的随机码(防止恶意攻击)
                    redisTo.setRandomCode(token);

                    //序列化json格式存入Redis中
                    String seckillValue = JSON.toJSONString(redisTo);
                    operations.put(seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(),seckillValue);

                    //如果当前这个场次的商品库存信息已经上架就不需要上架
                    //5、使用库存作为分布式Redisson信号量(限流)
                    // 使用库存作为分布式信号量
                    RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                    // 商品可以秒杀的数量作为信号量
                    semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
                }
            });
        });
    }


    /**
     * 获取到当前可以参加秒杀商品的信息
     * @return
     */
    @SentinelResource(value = "getCurrentSeckillSkusResource",blockHandler = "blockHandler")
    @Override
    public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
    
    

        try (Entry entry = SphU.entry("seckillSkus")) {
    
    
            //1、确定当前属于哪个秒杀场次
            long currentTime = System.currentTimeMillis();

            //从Redis中查询到所有key以seckill:sessions开头的所有数据
            Set<String> keys = redisTemplate.keys(SESSION__CACHE_PREFIX + "*");
            for (String key : keys) {
    
    
                //seckill:sessions:1594396764000_1594453242000
                String replace = key.replace(SESSION__CACHE_PREFIX, "");
                String[] s = replace.split("_");
                //获取存入Redis商品的开始时间
                long startTime = Long.parseLong(s[0]);
                //获取存入Redis商品的结束时间
                long endTime = Long.parseLong(s[1]);

                //判断是否是当前秒杀场次
                if (currentTime >= startTime && currentTime <= endTime) {
    
    
                    //2、获取这个秒杀场次需要的所有商品信息
                    List<String> range = redisTemplate.opsForList().range(key, -100, 100);
                    BoundHashOperations<String, String, String> hasOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
                    assert range != null;
                    List<String> listValue = hasOps.multiGet(range);
                    if (listValue != null && listValue.size() >= 0) {
    
    

                        List<SeckillSkuRedisTo> collect = listValue.stream().map(item -> {
    
    
                            String items = (String) item;
                            SeckillSkuRedisTo redisTo = JSON.parseObject(items, SeckillSkuRedisTo.class);
                            // redisTo.setRandomCode(null);当前秒杀开始需要随机码
                            return redisTo;
                        }).collect(Collectors.toList());
                        return collect;
                    }
                    break;
                }
            }
        } catch (BlockException e) {
    
    
            log.error("资源被限流{}",e.getMessage());
        }

        return null;
    }

    public List<SeckillSkuRedisTo> blockHandler(BlockException e) {
    
    

        log.error("getCurrentSeckillSkusResource被限流了,{}",e.getMessage());
        return null;
    }

    /**
     * 根据skuId查询商品是否参加秒杀活动
     * @param skuId
     * @return
     */
    @Override
    public SeckillSkuRedisTo getSkuSeckilInfo(Long skuId) {
    
    

        //1、找到所有需要秒杀的商品的key信息---seckill:skus
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);

        //拿到所有的key
        Set<String> keys = hashOps.keys();
        if (keys != null && keys.size() > 0) {
    
    
            //4-45 正则表达式进行匹配
            String reg = "\\d-" + skuId;
            for (String key : keys) {
    
    
                //如果匹配上了
                if (Pattern.matches(reg,key)) {
    
    
                    //从Redis中取出数据来
                    String redisValue = hashOps.get(key);
                    //进行序列化
                    SeckillSkuRedisTo redisTo = JSON.parseObject(redisValue, SeckillSkuRedisTo.class);

                    //随机码
                    Long currentTime = System.currentTimeMillis();
                    Long startTime = redisTo.getStartTime();
                    Long endTime = redisTo.getEndTime();
                    //如果当前时间大于等于秒杀活动开始时间并且要小于活动结束时间
                    if (currentTime >= startTime && currentTime <= endTime) {
    
    
                        return redisTo;
                    }
                    redisTo.setRandomCode(null);
                    return redisTo;
                }
            }
        }
        return null;
    }


    /**
     * 当前商品进行秒杀(秒杀开始)
     * @param killId
     * @param key
     * @param num
     * @return
     */
    @Override
    public String kill(String killId, String key, Integer num) throws InterruptedException {
    
    

        long s1 = System.currentTimeMillis();
        //获取当前用户的信息
        MemberResponseVo user = LoginUserInterceptor.loginUser.get();

        //1、获取当前秒杀商品的详细信息从Redis中获取
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
        String skuInfoValue = hashOps.get(killId);
        if (StringUtils.isEmpty(skuInfoValue)) {
    
    
            return null;
        }
        //(合法性效验)
        SeckillSkuRedisTo redisTo = JSON.parseObject(skuInfoValue, SeckillSkuRedisTo.class);
        Long startTime = redisTo.getStartTime();
        Long endTime = redisTo.getEndTime();
        long currentTime = System.currentTimeMillis();
        //判断当前这个秒杀请求是否在活动时间区间内(效验时间的合法性)
        if (currentTime >= startTime && currentTime <= endTime) {
    
    

            //2、效验随机码和商品id
            String randomCode = redisTo.getRandomCode();
            String skuId = redisTo.getPromotionSessionId() + "-" +redisTo.getSkuId();
            if (randomCode.equals(key) && killId.equals(skuId)) {
    
    
                //3、验证购物数量是否合理和库存量是否充足
                Integer seckillLimit = redisTo.getSeckillLimit();

                //获取信号量
                String seckillCount = redisTemplate.opsForValue().get(SKU_STOCK_SEMAPHORE + randomCode);
                Integer count = Integer.valueOf(seckillCount);
                //判断信号量是否大于0,并且买的数量不能超过库存
                if (count > 0 && num <= seckillLimit && count > num ) {
    
    
                    //4、验证这个人是否已经买过了(幂等性处理),如果秒杀成功,就去占位。userId-sessionId-skuId
                    //SETNX 原子性处理
                    String redisKey = user.getId() + "-" + skuId;
                    //设置自动过期(活动结束时间-当前时间)
                    Long ttl = endTime - currentTime;
                    Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                    if (aBoolean) {
    
    
                        //占位成功说明从来没有买过,分布式锁(获取信号量-1)
                        RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                        //TODO 秒杀成功,快速下单
                        boolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
                        //保证Redis中还有商品库存
                        if (semaphoreCount) {
    
    
                            //创建订单号和订单信息发送给MQ
                            // 秒杀成功 快速下单 发送消息到 MQ 整个操作时间在 10ms 左右
                            String timeId = IdWorker.getTimeId();
                            SeckillOrderTo orderTo = new SeckillOrderTo();
                            orderTo.setOrderSn(timeId);
                            orderTo.setMemberId(user.getId());
                            orderTo.setNum(num);
                            orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
                            orderTo.setSkuId(redisTo.getSkuId());
                            orderTo.setSeckillPrice(redisTo.getSeckillPrice());
                            rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo);
                            long s2 = System.currentTimeMillis();
                            log.info("耗时..." + (s2 - s1));
                            return timeId;
                        }
                    }
                }
            }
        }
        long s3 = System.currentTimeMillis();
        log.info("耗时..." + (s3 - s1));
        return null;
    }

}

44.3 秒杀流程

方式一:
优点:逻辑与之前的业务统一
缺点:调用了其他微服务,对其他微服务造成压力。
在这里插入图片描述
在这里插入图片描述

方式二:我们使用方式二
优点:速度快,隔离性好,高并发
缺点:做一套独立的业务。
在这里插入图片描述
随机码:用于防止恶意攻击,只有到了秒杀时间才会给客户返回这个随机码
幂等性:通过redis的占坑去判断这个人是否已经秒杀过了。

Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
if (aBoolean) {
    
    

秒杀代码:

/**
     * 当前商品进行秒杀(秒杀开始)
     * @param killId
     * @param key
     * @param num
     * @return
     */
    @Override
    public String kill(String killId, String key, Integer num) throws InterruptedException {
    
    

        long s1 = System.currentTimeMillis();
        //获取当前用户的信息
        MemberResponseVo user = LoginUserInterceptor.loginUser.get();

        //1、获取当前秒杀商品的详细信息从Redis中获取
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
        String skuInfoValue = hashOps.get(killId);
        if (StringUtils.isEmpty(skuInfoValue)) {
    
    
            return null;
        }
        //(合法性效验)
        SeckillSkuRedisTo redisTo = JSON.parseObject(skuInfoValue, SeckillSkuRedisTo.class);
        Long startTime = redisTo.getStartTime();
        Long endTime = redisTo.getEndTime();
        long currentTime = System.currentTimeMillis();
        //判断当前这个秒杀请求是否在活动时间区间内(效验时间的合法性)
        if (currentTime >= startTime && currentTime <= endTime) {
    
    

            //2、效验随机码和商品id
            String randomCode = redisTo.getRandomCode();
            String skuId = redisTo.getPromotionSessionId() + "-" +redisTo.getSkuId();
            if (randomCode.equals(key) && killId.equals(skuId)) {
    
    
                //3、验证购物数量是否合理和库存量是否充足
                Integer seckillLimit = redisTo.getSeckillLimit();

                //获取信号量
                String seckillCount = redisTemplate.opsForValue().get(SKU_STOCK_SEMAPHORE + randomCode);
                Integer count = Integer.valueOf(seckillCount);
                //判断信号量是否大于0,并且买的数量不能超过库存
                if (count > 0 && num <= seckillLimit && count > num ) {
    
    
                    //4、验证这个人是否已经买过了(幂等性处理),如果秒杀成功,就去占位。userId-sessionId-skuId
                    //SETNX 原子性处理
                    String redisKey = user.getId() + "-" + skuId;
                    //设置自动过期(活动结束时间-当前时间)
                    Long ttl = endTime - currentTime;
                    Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                    if (aBoolean) {
    
    
                        //占位成功说明从来没有买过,分布式锁(获取信号量-1)
                        RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                        //TODO 秒杀成功,快速下单
                        boolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
                        //保证Redis中还有商品库存
                        if (semaphoreCount) {
    
    
                            //创建订单号和订单信息发送给MQ
                            // 秒杀成功 快速下单 发送消息到 MQ 整个操作时间在 10ms 左右
                            String timeId = IdWorker.getTimeId();
                            SeckillOrderTo orderTo = new SeckillOrderTo();
                            orderTo.setOrderSn(timeId);
                            orderTo.setMemberId(user.getId());
                            orderTo.setNum(num);
                            orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
                            orderTo.setSkuId(redisTo.getSkuId());
                            orderTo.setSeckillPrice(redisTo.getSeckillPrice());
                            rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo);
                            long s2 = System.currentTimeMillis();
                            log.info("耗时..." + (s2 - s1));
                            return timeId;
                        }
                    }
                }
            }
        }
        long s3 = System.currentTimeMillis();
        log.info("耗时..." + (s3 - s1));
        return null;
    }

猜你喜欢

转载自blog.csdn.net/fen_dou_shao_nian/article/details/117932002
今日推荐