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

文章目录

  • P136—P138 首页展示一级、二级、三级目录
  • P139 nginx搭建域名访问环境1
  • P140 nginx搭建域名访问环境1
  • P141—P150 JMeter压测+JvisualVM监测+性能优化
  • P148 性能优化—nginx动静分离
  • p151—P155 Redis
  • p155 缓存击穿、穿透、雪崩
  • p156—p157 加锁解决缓存击穿(本地锁)
  • p158 加锁解决缓存击穿(分布式锁)
  • P159—P165 Redisson基本介绍
  • P166 将Redisson集成到项目里
  • P167—P172 SpringCache
      • 1.和缓存有关的注解:
      • 2.@Cacheable注解的使用,同时它存在的问题:
      • 3.解决@Cacheable注解存在的问题
      • 4.解决@Cacheable注解存在的问题
      • 5.`@CacheEvict`、`@Cacheput`、`@chaching`注解的演示
      • 6.在配置文件中,还可以指定一些缓存的自定义配置
      • 7.SpringCache的不足
  • 阶段总结:
  • p173—P190 ES检索查询
      • 1.前台功能
      • 2.前台传回来的检索条件
      • 3.后台返回给前台的数据
      • 4.DSL分析
      • 5.构建查询代码
      • 6.构建查询结果数据
      • 7.Controller语句
      • 8.总体逻辑:
  • p190—p192 ES检索之—面包屑功能
  • P193—P202 多线程
  • P203—P210 商品详情页
  • P211—P214 注册页面—验证码功能
  • P215—P216 注册页面—注册功能1
      • 1.总说:
      • 2.注册功能的实现:
  • P217 注册页面—注册功能3
  • P218 注册页面—注册功能4
  • P219 登陆页面—用户名密码登录
  • P220—P224 登陆页面—完成微博登录
  • P225 SpringSession—session不共享、不跨域问题
      • 1.session不能跨域问题
      • 2.分布式下session共享问题
      • 3.session共享问题的解决方案
      • 4.总说:
      • 5.修改微博登陆的代码
      • 6.修改账号密码登录的代码
  • P231—P235 单点登录
          • 1.为什么要单点登录?
          • 2.单点登录的原理?
          • 3.单点登录代码的实现
  • P236 购物车—环境的搭建
  • P237 购物车—数据模型的分析1
  • P238 购物车—数据模型的分析2
  • P239 购物车—拦截器鉴别用户
      • 1.总说:
      • 2.代码
  • P240 页面调整
  • P241 添加商品到购物车1
  • P242 添加商品到购物车2
  • P243 添加商品到购物车3
      • 1.总说:
      • 2.代码
      • 3.测试:
  • P244 获取购物车
      • 1.总说:
      • 2.代码:
      • 3.测试
  • P245 选中购物车项
  • P246 修改购物项数量
  • P247 删除购物项

P136—P138 首页展示一级、二级、三级目录

这个太重要了,后期P140—P172的什么性能压测、缓存优化都是基于这块进行的

day02_《谷粒商城》的完整流程(详细版一)_第1张图片
day02_《谷粒商城》的完整流程(详细版一)_第2张图片
day02_《谷粒商城》的完整流程(详细版一)_第3张图片
在这里插入图片描述

将"pms_category"表中所有数据封装到List0
找到List0中parent_cid为0的List1,这些就是1级分类;
遍历一级分类集合List1里面的所有CategoryEntity,记住它们的id,在List0里面找parent_id为一级分类id的就是二级分类;
遍历二级分类的集合List2里面的所有CategoryEntity,记住它们的id,在List0里面找parent_id为二级分类id的就是三级分类;

总之全程只查询了"pms_category"表,没有涉及到其他表。找到三级分类后将它们封装返回就是了。

P139 nginx搭建域名访问环境1

day02_《谷粒商城》的完整流程(详细版一)_第4张图片

P140 nginx搭建域名访问环境1

访问gulimall.com,本地根据在windows的hosts文件配置里找到gulimall.com映射的是192.168.56.106,于是转发到虚拟机,虚拟机会交给nginx,nginx有一处配置专门监听gulimall.com,监听到以后根据配置转发到88端口的gulimall-gateway,网关根据断言转发到gulimall-product

P141—P150 JMeter压测+JvisualVM监测+性能优化

1.中间件的影响
前台请求先经过nginx,然后nginx交给gulimall-Gateway,然后才能到达具体的服务,中间两个中间件nginx、Gateway会不会影响性能?

2.做了哪些压测?
先压测没有Gateway的情况下的吞吐量、响应时间;
然后压测有了Gateway的情况下的吞吐量、响应时间;
然后压测有了nginx,有了Gateway的情况下的吞吐量、响应时间;

访问首页gulimall.com,首页会访问数据库查询数据库的三级目录,模板引擎需要将查询到的数据转交给thymeleaf然后渲染到页面,你的业务代码都会导致响应会慢很多。比如你查询三级分类时查询数据库要尽量一次拿到pms_category的所有数据,而不是一级分类查一下数据库、二级分类查一下数据库、三级分类再查一下数据库。

先压测localhost:10000,因为它没有使用中间件直接访问到首页,所以响应也挺快的;
然后压测gulimall.com,因为它有了nginx,有了Gateway,查看它的响应时间;

3.优化的方向:
1.优化中间件:①让中间件每秒的吞吐量先上去,②然后让中间件之间的传输效率提高(买更好的网线、买更好的网卡、使用更高效率的传输协议等)
2.测试时JvisualVM发现伊甸园区内存只有32M,超小,所以垃圾回收次数非常多,所以如果伊甸园区调整的大一些就gc的时间就减少很多,那么吞吐量也就上去了。而且老年代也很小,导致几乎就要爆满了。给gulimall-product设置一Xmx1024m -xms1024m 一Xmn512m(内存最大占用1024M,初值也时1024M,相当于内存大小固定好了就是1024M,Xmn就是伊甸园区,给伊甸园区调大到512M)

3.业务代码也很影响性能。①查询数据库次数问题。②静态资源问题——因为我们把静态资源放到IDEA中的微服务中,所以首页需要的css、js样式也得找tomcat要,所以tomcat还得处理这些静态资源请求,导致吞吐量变少。③模板的缓存问题,你开发时经常在yml中有个配置就是thymeleaf.cache: false关闭thymeleaf缓存便于调试,到了实际上线后一定要开启缓存。④优化日志级别,以前是debug,现在改为error,也就是只打印错误日志。⑤优化数据库,在查询三级目录时经常查询parent_cid,由于parent_cid不是主键没有索引导致查询起来其实很慢,你如果查询id那种主键、有索引的就会很快很快,所以给parent_cid加上索引(索引类型就是普通索引,不用选成主键索引),那么查询速度就会快很多
4.使用缓存(见p151—p172)

P148 性能优化—nginx动静分离

以前我们是动态请求、静态请求都是先找nginx,然后找Gateway,然后找具体的微服务。
所以我们可以把静态资源上传到nginx上面,这样静态请求只需要找到nginx就看拿到对应的资源了。


分割线

p151—P155 Redis

在Redis这里别觉得代码多,代码一点都不多,最多的代码就是早就被你烂熟于心了的从数据查询三级目录的那片代码。
所以别被吓到,我担保p151—p172这块没有任何一个代码会让你看查过30秒都看不懂

1.哪些数据适合放入锾存?

  • 即时性、数据一致性要求不高的
  • 访间量大且更新频率不高的数据(读多,写少)

2.本地缓存与分布式缓存对比

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

3.使用redis优化三级目录的代码

    public Map<String, List<Catelog2Vo>> getCatalogJson2() {
     

        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        String catalogJson = ops.get("catalogJson");
        if (StringUtils.isEmpty(catalogJson)) {
     
            System.out.println("缓存不命中...查询数据库...");

            Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();

            return catalogJsonFromDb;
        }

        System.out.println("缓存命中...直接返回...");

        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJson,new TypeReference<Map<String, List<Catelog2Vo>>>(){
     });//转为指定的对象

        return result;
    }
	

    //从数据库查询数据(查询数据库之前再查一遍redis,防止在执行getDataFromDB()的前0.0001秒时正好有人把三级目录数据存入redis)
   private Map<String, List<Catelog2Vo>> getDataFromDB() {
     
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        if (!StringUtils.isEmpty(catalogJSON)) {
     
            //如果缓存不为null直接缓存
            Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
     
            });
            return result;
        }
        
        System.out.println("Redis中还是没有数据,查询了数据库。。。。。");
 
        List<CategoryEntity> selectList = baseMapper.selectList(null);
        //查出所有一级分类
        List<CategoryEntity> level1Category = getParent_cid(selectList, 0L);
 
        //2、封装数据
        Map<String, List<Catelog2Vo>> parent_cid = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
     
            //1、每一个的一级分类,查到这个一级分类的二级分类
            List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());
            //2、封装上面的结果
            List<Catelog2Vo> catelog2Vos = null;
            if (categoryEntities != null) {
     
                catelog2Vos = categoryEntities.stream().map(l2 -> {
     
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                    //1、找当前二级分类的三级分类封装vo
                    List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId());
                    if (level3Catelog != null) {
     
                        List<Catelog2Vo.Catalog3Vo> collect = level3Catelog.stream().map(l3 -> {
     
                            //2、封装成指定格式
                            Catelog2Vo.Catalog3Vo catelog3Vo = new Catelog2Vo.Catalog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(collect);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2Vos;
        }));
        //3、将查到的数据再放入缓存,将对象转为JSON在缓存中
        String jsonString = JSON.toJSONString(parent_cid);
        redisTemplate.opsForValue().set("catalogJSON", jsonString, 1, TimeUnit.DAYS);
        return parent_cid;
    }

4.整合redis后压力测试出内存泄漏问题

就在首页查询三级目录整合了Redis后,使用JMeter大并发测试时,出现堆外内存溢出异常OutOfDirectMemoryError

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

解决办法就是修改“gulimall-product”的“pom.xml”文件,更换为Jedis

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-redisartifactId>
        <exclusions>
            <exclusion>
                <groupId>io.lettucegroupId>
                <artifactId>lettuce-coreartifactId>
            exclusion>
        exclusions>
    dependency>
    <dependency>
        <groupId>redis.clientsgroupId>
        <artifactId>jedisartifactId>
    dependency>

p155 缓存击穿、穿透、雪崩

前面我们将查询三级分类数据的查询进行了优化,将查询结果放入到Redis中,当再次获取到相同数据的时候,直接从缓存中读取,没有则到数据库中查询,并将查询结果放入到Redis缓存中

1.缓存穿透:

  • 指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都是缓存是不命中然后查询数据库然后数据库也没有。
  • 解决办法就是:从数据库查询到null以后写入缓存,以后凡是请求这个资源的让缓存直接返回null,别再查询数据了。(当然缓存中存放的这个null肯定得有过期时间)

2.缓存雪崩:

  • 缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
  • 解决:原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

3.缓存击穿

  • 对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。如果这个key在100万请求同时进来前一秒正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿(还没来得及写入缓存数据库就被查了100来万遍)。
  • 解决:加锁。大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db

简单来说:缓存穿透是指查询一个永不存在的数据;缓存雪崩是值大面积key同时失效问题;缓存击穿是指高频key失效问题;

p156—p157 加锁解决缓存击穿(本地锁)

1.锁时序问题:

  • 之前的逻辑是查缓存没有,然后查数据库,释放锁后才把缓存结果写入数据库,这就到了当你释放锁还没有把数据库结果写入缓存就被别人又去查库。现在就是先加锁,然后从数据库中查询三级分类数据,查到以后写入缓存,最后释放锁。

2.加锁代码

 //从数据库查询并封装分类数据
    public Map<String, List<Catalog2Vo>> getCatalogJsonFromDb() {
     


        //为什么是this,看视频
        synchronized (this) {
     

            //得到锁以后应该再去缓存中确定一次,如果没有才需要继续查询
            String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
            if (StringUtils.isNotEmpty(catalogJSON)) {
     
                //缓存不为null,直接返回,
                Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
     
                });
                return result;
            }

			System.out.println("查询了数据库");
			
            List<CategoryEntity> selectList = baseMapper.selectList(null);

            //1、查出所有一级分类,
            List<CategoryEntity> category = getParent_cid(selectList, 0L);

            //2、封装数据
            Map<String, List<Catalog2Vo>> parent_cid = category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
     
                //1、查到这个一级分类下的所有二级分类
                List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());

                //2、封装上面的结果
                List<Catalog2Vo> catalog2Vos = new ArrayList<>();
                if (categoryEntities != null && categoryEntities.size() != 0) {
     
                    catalog2Vos = categoryEntities.stream().map(l2 -> {
     
                        Catalog2Vo catalog2Vo = new Catalog2Vo(
                                l2.getParentCid().toString(), null, l2.getCatId().toString(), l2.getName());
                        //1、找当前二级分类的三级分类封装成vo
                        List<CategoryEntity> level3Catalog = getParent_cid(selectList, l2.getCatId());
                        List<Catalog2Vo.Catalog3Vo> collectlevel3 = new ArrayList<>();
                        if (level3Catalog != null && !level3Catalog.isEmpty()) {
     
                            //2、封装成指定格式
                            collectlevel3 = level3Catalog.stream().map(l3 -> {
     
                                Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(l3.getParentCid().toString(),
                                        l3.getCatId().toString(), l3.getName());
                                return catalog3Vo;
                            }).collect(Collectors.toList());
                        }
                        catalog2Vo.setCatalog3List(collectlevel3);
                        return catalog2Vo;
                    }).collect(Collectors.toList());
                }
                return catalog2Vos;
            }));

            //3、将查到的数据再放入缓存,将对象转为JSON在缓存中
            String jsonString = JSON.toJSONString(parent_cid);
            redisTemplate.opsForValue().set("catalogJSON",jsonString,1, TimeUnit.DAYS);
            return parent_cid;
        }
    }

3.本地锁在分布式情况下存在的问题
把gulimall-product复制四份,然后让JMeter大并发去访问gulimall.com,我们发现在分布式下的四个服务分别存在着四个缓存未命中的情况,也就意味着会有四次查询数据库的操作,显然我们的synchronize锁未能实现限制其他服务实例进入临界区,也就印证了在分布式情况下,本地锁只能针对于当前的服务生效。

p158 加锁解决缓存击穿(分布式锁)

在Redisson出现之前我们使用的就是去Redis中占坑的方式去获得分布式锁,我们占坑的方法lock=setIfAbsent(key,value)就是如果这个key不存在的话就设置key-value,而且返回true;如果存在了就设置不了key-value,返回false

加了分布式锁解决缓存击穿,但是分布式锁存在很多需要考虑的因素:

  • 万一某个线程拿到分布式锁后执行业务逻辑时抛出异常,此时会被该线程独占这把锁——解决办法:给锁要设置过期时间
  • 加锁和给锁设置过期时间这两行代码存在时间差,万一在这个时间差内出现断电什么的也会被某个线程拿到没有被设置过期时间的锁——解决办法:加锁和设置过程时间弄成原子性操做,要么同时成功要么同时失败。
  • 删锁时发现你的锁已经过期了,你的锁已经被别的线程拿到了,别的线程就会进来,你此时如果删锁那么删掉的是别人的锁——解决办法:给锁设置uuid,每个人都不一样,你就删不了别人的锁了。
  • 上一步说到删锁时先比对uuid再删锁,期间存在时间差,万一在你比对完发现这把锁就是你的锁,正要删锁时你的锁过期了,被别的线程拿到了,你删掉的就是别人的锁。——解决办法:比对uuid和删锁必须是原子操作。
  • 一句话:加锁保证原子性,解锁保证原子性。
  • 可以看到:手写一个分布式锁很麻烦,所以我们用Redisson,Redisson就是分布式锁
public Map<String, List<Catalog2Vo>> getCatalogJson() {
     

        /**
         * 1、空结果缓存,解决缓存穿透
         * 2、设置过期时间(加随机值),解决缓存雪崩
         * 3、加锁,解决缓存击穿
         */

        String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
        if (StringUtils.isEmpty(catalogJSON)) {
     
            //缓存中没有,查询数据库
            System.out.println("缓存不命中。。。。将要查询数据库。。。。");
            Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDbWithRedisLock();
            return catalogJsonFromDb;
        }
        
        System.out.println("缓存命中。。。。直接返回。。。。");
        Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
     });//转为我们指定的对象
        return result;

    }

    public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {
     

        String token = UUID.randomUUID().toString();

        //1、加锁和设置过程时间弄成原子性操做,这条有四个参数的setIfAbsent()方法就是原子的操做
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", token, 100, TimeUnit.SECONDS);

        if (lock) {
     
        	//lock=true说明加锁成功,那就从数据库中获取数据
        	
            System.out.println("获取分布式锁成功");
            
            Map<String, List<Catalog2Vo>> dataFromDB;
            try {
     
                
                dataFromDB = getDataFromDB();
            } finally {
     

//			  先比对uuid再删锁(这不是原子性操做,所以被注释掉了)
//            String lock1 = stringRedisTemplate.opsForValue().get("lock");
//            if (token.equals(lock1)) {
     
//                stringRedisTemplate.delete("lock");
//            }


                String lua = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
                RedisScript<Long> luaScript = RedisScript.of(lua, Long.class);
                Long lock1 = stringRedisTemplate.execute(luaScript, Arrays.asList("lock"), token);
            }

            return dataFromDB;
        } else {
     
            System.out.println("获取分布式锁失败,等待重试");
            //加锁失败。。。重试 。休眠300ms重试
            try {
     
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            return getCatalogJsonFromDbWithRedisLock();
        }

    }

private Map<String, List<Catalog2Vo>> getDataFromDB() {
     
        //得到锁以后应该再去缓存中确定一次,如果没有才需要继续查询
        String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
        if (StringUtils.isNotEmpty(catalogJSON)) {
     
            //缓存不为null,直接返回,
            Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
      });
            
            return result;
        }
        System.out.println(Thread.currentThread().getName() + "查询了数据库");
        List<CategoryEntity> selectList = baseMapper.selectList(null);

        List<CategoryEntity> category = getParent_cid(selectList, 0L);

        //2、封装数据
        Map<String, List<Catalog2Vo>> parent_cid = category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
     
            //1、查到这个一级分类下的所有二级分类
            List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());

            //2、封装上面的结果
            List<Catalog2Vo> catalog2Vos = new ArrayList<>();
            if (categoryEntities != null && categoryEntities.size() != 0) {
     
                catalog2Vos = categoryEntities.stream().map(l2 -> {
     
                    Catalog2Vo catalog2Vo = new Catalog2Vo(
                            l2.getParentCid().toString(), null, l2.getCatId().toString(), l2.getName());
                    //1、找当前二级分类的三级分类封装成vo
                    List<CategoryEntity> level3Catalog = getParent_cid(selectList, l2.getCatId());
                    List<Catalog2Vo.Catalog3Vo> collectlevel3 = new ArrayList<>();
                    if (level3Catalog != null && !level3Catalog.isEmpty()) {
     
                        //2、封装成指定格式
                        collectlevel3 = level3Catalog.stream().map(l3 -> {
     
                            Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(l3.getParentCid().toString(),
                                    l3.getCatId().toString(), l3.getName());
                            return catalog3Vo;
                        }).collect(Collectors.toList());
                    }
                    catalog2Vo.setCatalog3List(collectlevel3);
                    return catalog2Vo;
                }).collect(Collectors.toList());
            }
            return catalog2Vos;
        }));

        //3、将查到的数据放到缓存,将对象转为json放到缓存中
        String s = JSON.toJSONString(parent_cid);
        stringRedisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);

        return parent_cid;
    }

P159—P165 Redisson基本介绍

1.SpringBoot整合Redisson
①导入pom
②写一个配置类,配置类中指明Redis的地址,返回Redisson

@Configuration
 
public class MyRedisConfig {
     
 
    @Bean(destroyMethod="shutdown")
 
        public RedissonClient redisson() throws IOException {
     
 
        Config config = new Config();
 
        config.useSingleServer().setAddress("redis://192.168.137.14:6379");
 
        RedissonClient redisson = Redisson.create(config);
 
        return redisson;
    }
}

③直接使用

RLock lock = redisson.getLock("my-lock");
lock.lock();
lock.unlock();

2.Redisson的特性
设想一种情况,一个请求线程在执行业务方法的时候,突然发生了中断,此时没有来得及执行释放锁操作,那么同时等待的另外一个线程是否会发生死锁。

在A服务在获取锁后,突然中断它的运行;等待的B服务会很快就拿到锁,不会因为A没有释放锁而被卡死。通这是因为在Redisson中会为每个锁加上“leaseTime”,默认是30秒,如果A服务宕机,到了时间就会自动释放锁。如果A服务没有宕机,而且30秒不够用,Redisson会自动给它续期。当然,人家默认的自动解锁时间是30秒,如果你改为10秒,那么10秒后立刻释放锁,不会给锁续期,但是这种自定义解锁场景也很常用,你可以自定义300秒,如果一个业务300秒都没有执行完肯定就有问题,而且我们还可以拿它评估一下业务的最大执行用时。


小结:redisson的lock具有如下特点

  • (1)阻塞式等待。默认的锁的时间是30s。
  • (2)锁定的制动续期,如果业务超长,运行期间会自动给锁续上新的30s,无需担心业务时间长,锁自动被删除的问题。
  • (3)加锁的业务只要能够运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除。
  • (4)可以自定义解锁时间,时间到了不会续期,但它可以评估一下业务的最大执行用时

3.Redisson的读写锁
写+读:要等写完才能读
写+写:等前一个写完后一个才能写
读+读:相当于无锁,大家都能读
读+写:有读锁,写必须等待
读写锁适合经常读、很少写的情况,因为读的时候相当于无锁。

4.Redisson的闭锁
走完五个人就锁门,这就是闭锁

5.Redisson的信号量
车库停车,3个停车位,获取到信号量才能进去停车。

以上演示的Redisson的读写锁、闭锁、信号量都是分布式下也适用的情况。

P166 将Redisson集成到项目里

1.如果没有使用Redisson

    public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {
     

        String token = UUID.randomUUID().toString();

        //1、占分布式锁。去redis占坑,
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", token, 100, TimeUnit.SECONDS);

        if (lock) {
     
        	//lock=true说明加锁成功,那就从数据库中获取数据
        	
            System.out.println("获取分布式锁成功");
            
            Map<String, List<Catalog2Vo>> dataFromDB;
            try {
     
                
                dataFromDB = getDataFromDB();
            } finally {
     

                String lua = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
                RedisScript<Long> luaScript = RedisScript.of(lua, Long.class);
                Long lock1 = stringRedisTemplate.execute(luaScript, Arrays.asList("lock"), token);
            }

            return dataFromDB;
        } else {
     
            System.out.println("获取分布式锁失败,等待重试");
            //加锁失败。。。重试 。休眠300ms重试
            try {
     
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            return getCatalogJsonFromDbWithRedisLock();
        }

    }

2.如果使用Redisson

    public Map<String, List<Catalogs2Vo>> getCatalogJsonFromDbWithRedissonLock() {
     

		//注意锁的粒度问题
        RReadWriteLock lock = redissonClient.getReadWriteLock("catalogJson-lock");

        lock.lock();

        Map<String, List<Catalogs2Vo>> dataFromDb = null;
        try {
     
            dataFromDb = getCatalogJsonFromDB();
        } finally {
     
            lock.unlock();
        }
        return dataFromDb;
    }

3.使用Redsson时应该注意锁的粒度问题
     给锁起名字要注意,不能都起一样的名字,一样的名字代表同一把锁,获取三级分类数据、获取品牌、获取属性锁到同一把锁里面,那就导致粒度很粗。假如访问三级分类是高并发的请求,访问品牌是低并发的,他俩如果同一把锁那么高并发的锁住导致低并发的也访问不到。

4.使用了Redsson还存在问题

  • 如何保证缓存和数据库中的数据一致?
         ①双写模式(改了数据库顺带着改了缓存)
         ②失效模式(改了数据库顺带删了缓存)
         ③双写模式/失效模式+读写锁
         ④使用Canal(MySQL中一有什么变化就会同步到缓存中来)

  • 各自的弊端:
    ①双写模式:A要把a改为1,然后B要把a改为2,最后数据库a应该是2。但是A把a改为1本来顺带改一下缓存结果卡顿了,导致B把a改为2顺带先改了缓存,然后卡顿的A改了缓存导致缓存中a是1但数据库中的a是2。高并发下缓存不一致出现了,这就又又又得加锁解决。
    ②失效模式:(A和B是写操作,改了数据库就要删缓存;C是读操作,如果缓存中读不到就得去数据库读然后写到缓存中)现在有这么一个场景:A要把a改为1,然后B要把a改为2,然后C要读取a,本来C读到的a应该是2。但是A要把a改为1然顺带删了缓存;然后B要把a改为2结果B卡顿住了;C进来读取缓存发现缓存没有数据就读数据库读到了a=1,因为缓存中没有数据所以C要把读到的a写到缓存上,但是C写缓存之前也卡顿了一下;结果现在B变流畅了,它把数据库a改为2,顺带要删缓存,结果发现缓存中还没有数据所以就不删了;现在C开始了,它把读到的a=1写到缓存中。又要加锁。
    ③双写模式/失效模式+读写锁:这个没什么问题,但是代码太复杂了吧。
    ④使用Canal:Canal是第三方的,使用起来非常方便,而且也没什么问题,但是又加了一个中间件,还得自定义一些功能,我们这个小项目就不用了。


我们系统的一致性解决方案:
实时性、一致性要求高的那就去数据库中查;
实时性、一致性要求不高的那就放到缓存中,如果害怕出现脏数据,那就给缓存加上过期时间,然后使用双写模式/失效模式+读写锁,代码很复杂


基于以上分析,我们发现用了Redisson依旧要考虑复杂的系统的一致性问题,所以SpringCache应用而生

P167—P172 SpringCache

1.和缓存有关的注解:

@Cacheable:触发将数据保存到缓存的操作
@CacheEvict:触发将数据从缓存中删除的操作
@CachePut:在不影响方法执行的情况下更新缓存。
@Caching:组合以上多个操作
@CacheConfig: 在类级别上共享一些公共的与缓存相关的设置。

2.@Cacheable注解的使用,同时它存在的问题:

  • 默认的过期时间是无限时间
  • 默认的数据保存方式不是json的
  • 默认的key不好,我们要自定义存到缓存里面的key
//这是以前编写的前台访问/index时获取一级目录的方法,我们只需要在上面添加@Cacheable注解就表示如果缓存中有就不用执行下面的方法,缓存中没有就执行下面的方法查出数据并且放入缓存
@Cacheable({
     "catagory"})
@Override
public List<CategoryEntity> getLevel1Categorys() {
     
    System.out.println("getLevel1Categorys......");
    long l = System.currentTimeMillis();
    return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
}

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

3.解决@Cacheable注解存在的问题

@Cacheable注解里面有些默认配置不合理,我们要自定义

  • 自定义过期时间
  • 自定义存到缓存里面的key
#在yml中指定过期时间
spring.cache.redis.time-to-live=3600000
//因为spel动态取值,所有需要额外加''表示字符串
@Cacheable(value = {
     "catagory"},key = "'Level1Categorys'")
@Override
public List<CategoryEntity> getLevel1Categorys() {
     
    System.out.println("getLevel1Categorys......");
    long l = System.currentTimeMillis();
    return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
}

在这里插入图片描述

@Cacheable(value = {
     "catagory"},key = "#root.method.name") //用方法名做key
@Override
public List<CategoryEntity> getLevel1Categorys() {
     
    System.out.println("getLevel1Categorys......");
    long l = System.currentTimeMillis();
    return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
}

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

4.解决@Cacheable注解存在的问题

@Cacheable注解里面有些默认配置不合理,我们要自定义

  • 自定义数据保存方式是json,写个配置类就行了

5.@CacheEvict@Cacheput@chaching注解的演示

说明:
getLevel1Categorys()是从数据库中读取一级分类数据,getCatalogJson()是从数据库中读取三级分类数据,;
updateCascade()是更新数据库中的三级分类数据。一旦数据库中三级分类数据被更新,那么那么一级目录的数据和三级目录的数据都变了,所以需要清除getLevel1Categorys()和getCatalogJson()里面的缓存数据。
1.清除缓存的方法一

@Caching(evict = {
     
       @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
       @CacheEvict(value = "category",key = "'getCatalogJson'")
})
@Transactional(rollbackFor = Exception.class)
@Override
public void updateCascade(CategoryEntity category) {
     
    this.updateById(category);
    categoryBrandRelationDao.updateCategory(category.getCatId(), category.getName());
    //同时修改缓存中的数据,
}
    @Cacheable(value = {
     "category"},key = "#root.method.name")
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
     
        System.out.println("getLevel1Categorys........");
        long l = System.currentTimeMillis();
        List<CategoryEntity> categoryEntities = this.baseMapper.selectList(
                new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        System.out.println("消耗时间:"+ (System.currentTimeMillis() - l));
        return categoryEntities;
    }
@Cacheable(value = {
     "catagory"},key = "#root.method.name")
@Override
public Map<String, List<Catalog2Vo>> getCatalogJson() {
     

    System.out.println(Thread.currentThread().getName() + "查询了数据库");
    List<CategoryEntity> selectList = baseMapper.selectList(null);

    List<CategoryEntity> category = getParent_cid(selectList, 0L);

    //2、封装数据
    Map<String, List<Catalog2Vo>> parent_cid = category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
     
        //1、查到这个一级分类下的所有二级分类
        List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());

        //2、封装上面的结果
        List<Catalog2Vo> catalog2Vos = new ArrayList<>();
        if (categoryEntities != null && categoryEntities.size() != 0) {
     
            catalog2Vos = categoryEntities.stream().map(l2 -> {
     
                Catalog2Vo catalog2Vo = new Catalog2Vo(
                        l2.getParentCid().toString(), null, l2.getCatId().toString(), l2.getName());
                //1、找当前二级分类的三级分类封装成vo
                List<CategoryEntity> level3Catalog = getParent_cid(selectList, l2.getCatId());
                List<Catalog2Vo.Catalog3Vo> collectlevel3 = new ArrayList<>();
                if (level3Catalog != null && !level3Catalog.isEmpty()) {
     
                    //2、封装成指定格式
                    collectlevel3 = level3Catalog.stream().map(l3 -> {
     
                        Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(l3.getParentCid().toString(),
                                l3.getCatId().toString(), l3.getName());
                        return catalog3Vo;
                    }).collect(Collectors.toList());
                }
                catalog2Vo.setCatalog3List(collectlevel3);
                return catalog2Vo;
            }).collect(Collectors.toList());
        }
        return catalog2Vos;
    }));

    return parent_cid;

}

1.清除缓存的方法二

@CacheEvict(value = "category",allEntries = true)       //删除某个分区下的所有数据
@Transactional(rollbackFor = Exception.class)
@Override
public void updateCascade(CategoryEntity category) {
     
    this.updateById(category);
    categoryBrandRelationDao.updateCategory(category.getCatId(), category.getName());
    //同时修改缓存中的数据,
}

6.在配置文件中,还可以指定一些缓存的自定义配置

spring.cache.type=redis
 
#设置超时时间,默认是毫秒
 
spring.cache.redis.time-to-live=3600000
 
#设置Key的前缀,如果指定了前缀,则使用我们定义的前缀,否则使用缓存的名字作为前缀
 
spring.cache.redis.key-prefix=CACHE_
 
spring.cache.redis.use-key-prefix=true
 
#是否缓存空值,防止缓存穿透
 
spring.cache.redis.cache-null-values=true

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

7.SpringCache的不足

p155 缓存击穿、穿透、雪崩问题能用SpringCache解决掉吗?

(总说)先明确什么是读模式什么是写模式:

  • getLevel1Categorys()是从数据库中读取一级分类数据,getCatalogJson()是从数据库中读取三级分类数据,它们都是读模式,读模式就是从数据中读取数据,然后使用@Cacheable将数据放入缓存。
  • updateCascade()是更新数据库中的三级分类数据,他就是写模式,写模式就是更新数据库数据,数据库数据一旦被更新就需要使用@CacheEvict()注解清除缓存中的旧数据。

1)、读模式

  • 缓存穿透:并发查询一个null数据就会产生缓存穿透。SpringCache可以解决。解决方案:缓存空数据,可通过yml中的spring.cache.redis.cache-null-values=true配置来实现
  • 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:需要加锁,使用@Cacheable(sync = true)来解决击穿问题。
  • 缓存雪崩:大量的key同时过期。解决:加随机时间。
  • 也就是说在读模式中SpringCache能够解决掉所有问题。

2)、写模式:(缓存与数据库一致)

  • 读写加锁。
  • 引入Canal,感知到MySQL的更新去更新Redis
  • 读多写多,直接去数据库查询就行

3)、总结:

常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):

写模式(只要缓存的数据有过期时间就足够了,过期了让它自己更新就可以了)

特殊数据:你还想加缓存,还想保证数据库和缓存的一致性,那就需要结合Redisson来使用

阶段总结:

p136到p138是搭建了首页;
p139和p140是让我们借助nginx来通过域名访问这个首页(假如nginx肯定会使访问路线更加曲折,从而影响性能);
p141-p147是通过Jmeter、JvisualVM来分析加入Gateway、nginx这些中间件带来的性能损失,
p148-p150是进行性能优化—动静分离、JVM内存优化、代码优化
p151--p154是进行性能优化—使用Redis
p155—p158是要解决缓存击穿就需要加锁,而加本地锁不行,只能加分布式锁,但是加分布式锁又要考虑一堆分布式并发问题,于是就有了Redisson;
p159—p166是给你介绍了Redisson分布式锁的用法,但是用上Redisson后还要考虑缓存和数据库一致性问题,于是SpringCache应用而生。
p167—p172有了SpringCache,常规数据的缓存你可以不用Redisson,因为SpringCache已经考虑到缓存雪崩、击穿、穿透问题了,它里面可以加锁,可以设置过期时间等等。

从P173开始,完整的笔记全部参考这位博主写的笔记:谷粒商城-个人笔记(高级篇二)


分割线

p173—P190 ES检索查询

1.前台功能


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

day02_《谷粒商城》的完整流程(详细版一)_第12张图片
day02_《谷粒商城》的完整流程(详细版一)_第13张图片
day02_《谷粒商城》的完整流程(详细版一)_第14张图片
day02_《谷粒商城》的完整流程(详细版一)_第15张图片

2.前台传回来的检索条件

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

@Data
public class SearchParam {
     

    private String keyword;//页面传递过来的全文匹配关键字
 
     //sort=saleCount_asc/desc销量
     //sort=skuPrice_asc/desc价格
     //sort=hotScore_asc/desc热度分
    private String sort;//排序条件
 
    //hasStock=0/1
    private Integer hasStock;//是否只显示有货
 
 	//skuPrice=1_500
    private String skuPrice;//价格区间查询
 
 	//brandId=2&brandId=3
    private List<Long> brandId;//按照品牌进行查询,可以多选
  
  	//catelog3Id=1
    private Long catalog3Id;//三级分类id
    
    //attr=1_3G:4G:5G;attrs=2_骁龙
    private List<String> attrs;//按照属性进行筛选
 
    private Integer pageNum = 1;//页码
}

3.后台返回给前台的数据

@Data
public class SearchResult {
     
 	
 	//查询到的商品信息
    private List<SkuEsModel> products;
 
 	//分页信息
    private Integer pageNum;//当前页码
    private Long total;//总记录数
    private Integer totalPages;//总页码
 
 	//所有涉及到的品牌
    private List<BrandVo> brands;
 
 	//所有涉及到的分类
    private List<CatalogVo> catalogs;
 
 	//所有涉及到的属性
    private List<AttrVo> attrs;
 
    //=========================================================================
 
    @Data
    public static class BrandVo{
     
        private Long brandId;
 
        private String brandName;
 
        private String brandImg;
    }
 
    @Data
    public static class CatalogVo{
     
        private Long catalogId;
 
        private String catalogName;
 
        private String brandImg;
    }
 
    @Data
    public static class AttrVo{
     
        private Long attrId;
 
        private String attrName;
 
        private List<String> attrValue;
    }
}
//注意:因为Attrs这个类在SkuEsModel这个类里面,属于嵌入式的,所以后期在DSL语句中查询Attrs里面的东西要用nested

@Data
public class SkuEsModel {
     

    private Long skuId; //(SkuInfoEntity)中有

    private Long spuId; //(SkuInfoEntity)中有

    private String skuTitle; //(SkuInfoEntity)中有

    private BigDecimal skuPrice; //(SkuInfoEntity)中有,但是名字不一样

    private String skuImg; //(SkuInfoEntity)中有,但是名字不一样

    private Long saleCount; //(SkuInfoEntity)中有

    private Boolean hasStock;

    private Long hotScore;//热度评分设置为0即可

    private Long brandId; //(SkuInfoEntity)中有

    private Long catalogId; //(SkuInfoEntity)中有

    private String brandName; //{BrandEntity}中有

    private String brandImg; //{BrandEntity}中有

    private String catalogName;//【CategoryEntity】中有

    private List<Attrs> attrs;//{
     {ProductAttrValueEntity}}中有

    @Data  
    public static class Attrs {
      //{
     {ProductAttrValueEntity}}中有

        private Long attrId;

        private String attrName;

        private String attrValue;

    }
}

4.DSL分析

完整查询参数 keyword=小米&catalog3Id=1&brandId=1&hasStock=0/1&skuPrice=400_1900&at trs=1_3G:4G:5G&attrs=2_骁龙845&attrs=4_高清屏&sort=saleCount_desc/asc

GET gulimall_product/_search
{
     
  "query": {
     
    "bool": {
     
      "must": [
        {
     
          "match": {
     
            "skuTitle": "华为"
          }
        }
      ],
      "filter": [
        {
     
            "term": {
     
              "catalogId": "225"
            }
        },
        {
     
            "terms": {
     
            "brandId": [
              "2"
            ]
          }
        },
        {
     
          "term": {
     
            "hasStock": "false"
          }
        },
        {
     
          "range": {
     
            "skuPrice": {
     
              "gte": 1000,
              "lte": 7000
            }
          }
        },
        {
     
          "nested": {
     
            "path": "attrs",
            "query": {
     
              "bool": {
     
                "must": [
                  {
     
                    "term": {
     
                      "attrs.attrId": {
     
                        "value": "6"
                      }
                    }
                  }
                ]
              }
            }
          }
        }
      ]
    }
  },
  "sort": [
    {
     
      "skuPrice": {
     
        "order": "desc"
      }
    }
  ],
  "from": 0,
  "size": 5,
  "highlight": {
     
    "fields": {
     "skuTitle": {
     }},  //用户搜索"华为",那么前台要高亮显示
    "pre_tags": "", 
    "post_tags": ""
  },
  "aggs": {
     
    "brandAgg": {
     
      "terms": {
     
        "field": "brandId",
        "size": 10
      },
      "aggs": {
     
        "brandNameAgg": {
     
          "terms": {
     
            "field": "brandName",
            "size": 10
          }
        },
        "brandImgAgg": {
     
          "terms": {
     
            "field": "brandImg",
            "size": 10
          }
        }
        
      }
    },
    "catalogAgg":{
     
      "terms": {
     
        "field": "catalogId",
        "size": 10
      },
      "aggs": {
     
        "catalogNameAgg": {
     
          "terms": {
     
            "field": "catalogName",
            "size": 10
          }
        }
      }
    },
    "attrs":{
     
      "nested": {
     
        "path": "attrs"
      },
      "aggs": {
     
        "attrIdAgg": {
     
          "terms": {
     
            "field": "attrs.attrId",
            "size": 10
          },
          "aggs": {
     
            "attrNameAgg": {
     
              "terms": {
     
                "field": "attrs.attrName",
                "size": 10
              }
            }
          }
        }
      }
    }
  }
}

5.构建查询代码

代码看不懂太正常了,回看Day03_谷粒商城(谷粒商城高级篇二)摘要


@Service
public class MallSearchServiceImpl implements MallSearchService {
     
 
    @Autowired
    RestHighLevelClient restHighLevelClient;
 
    //去es进行检索
    @Override
    public SearchResult search(SearchParam param) {
     
        //动态构建出查询需要的DSL语句
        SearchResult result = null;
        //1、准备检索请求
        SearchRequest searchRequest = buildSearchRequest(param);
        try {
     
            //2、执行检索请求
            SearchResponse response = restHighLevelClient.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
            //分析响应数据封装我们需要的格式
            result = buildSearchResult(response,param);
        } catch (IOException e) {
     
            e.printStackTrace();
        }
        return result;
    }
 
    /**
     * 准备检索请求
     * 模糊匹配、过滤(按照属性、分类、品牌、价格区间、库存),排序,分页,高亮,聚合分析
     * @return
     */
    private SearchRequest buildSearchRequest(SearchParam param) {
     
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();//构建DSL语句的
        /**
         * 过滤(按照属性、分类、品牌、价格区间、库存)
         */
        //1、构建bool-query
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();

        //1.1 must-模糊匹配、
        if (!StringUtils.isEmpty(param.getKeyword())){
     
            boolQuery.must(QueryBuilders.matchQuery("skuTitle",param.getKeyword()));
        }
        //1.2.1 filter-按照三级分类id查询
        if (null != param.getCatalog3Id()){
     
            boolQuery.filter(QueryBuilders.termQuery("catalogId",param.getCatalog3Id()));
        }
        //1.2.2 filter-按照品牌id查询
        if (null != param.getBrandId() && param.getBrandId().size()>0) {
     
            boolQuery.filter(QueryBuilders.termsQuery("brandId",param.getBrandId()));
        }
        //1.2.3 filter-按照是否有库存进行查询
        if (null != param.getHasStock() ) {
     
            boolQuery.filter(QueryBuilders.termQuery("hasStock", param.getHasStock() == 1));
        }
        //1.2.4 filter-按照区间进行查询  1_500/_500/500_
        RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
        if (!StringUtils.isEmpty(param.getSkuPrice())) {
     
            String[] prices = param.getSkuPrice().split("_");
            if (prices.length == 1) {
     
                if (param.getSkuPrice().startsWith("_")) {
     
                    rangeQueryBuilder.lte(Integer.parseInt(prices[0]));
                }else {
     
                    rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
                }
            } else if (prices.length == 2) {
     
                //_6000会截取成["","6000"]
                if (!prices[0].isEmpty()) {
     
                    rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
                }
                rangeQueryBuilder.lte(Integer.parseInt(prices[1]));
            }
            boolQuery.filter(rangeQueryBuilder);
        }
        //1.2.5 filter-按照属性进行查询
        List<String> attrs = param.getAttrs();
        if (null != attrs && attrs.size() > 0) {
     
            //attrs=1_5寸:8寸&2_16G:8G
            attrs.forEach(attr->{
     
                BoolQueryBuilder queryBuilder = new BoolQueryBuilder();
                String[] attrSplit = attr.split("_");
                queryBuilder.must(QueryBuilders.termQuery("attrs.attrId", attrSplit[0]));//检索的属性的id
                String[] attrValues = attrSplit[1].split(":");
                queryBuilder.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));//检索的属性的值
                //每一个必须都得生成一个nested查询
                NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("attrs", queryBuilder, ScoreMode.None);
                boolQuery.filter(nestedQueryBuilder);
            });
        }
        //把以前所有的条件都拿来进行封装
        sourceBuilder.query(boolQuery);
        /**
         * 排序,分页,高亮,
         */
        //2.1 排序  eg:sort=saleCount_desc/asc
        if (!StringUtils.isEmpty(param.getSort())) {
     
            String[] sortSplit = param.getSort().split("_");
            sourceBuilder.sort(sortSplit[0], sortSplit[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC);
        }
 
        //2.2、分页
        sourceBuilder.from((param.getPageNum() - 1) * EsConstant.PRODUCT_PAGESIZE);
        sourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);
 
        //2.3 高亮highlight
        if (!StringUtils.isEmpty(param.getKeyword())) {
     
            HighlightBuilder highlightBuilder = new HighlightBuilder();
            highlightBuilder.field("skuTitle");
            highlightBuilder.preTags("");
            highlightBuilder.postTags("");
            sourceBuilder.highlighter(highlightBuilder);
        }
 
        /**
         * 聚合分析
         */
        //5. 聚合
        //5.1 按照品牌聚合
        TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg").field("brandId").size(50);
        //品牌聚合的子聚合
        TermsAggregationBuilder brand_name_agg = AggregationBuilders.terms("brand_name_agg").field("brandName").size(1);
        TermsAggregationBuilder brand_img_agg = AggregationBuilders.terms("brand_img_agg").field("brandImg");
        brand_agg.subAggregation(brand_name_agg);
        brand_agg.subAggregation(brand_img_agg);
        sourceBuilder.aggregation(brand_agg);
 
        //5.2 按照catalog聚合
        TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg").field("catalogId").size(20);
        TermsAggregationBuilder catalog_name_agg = AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1);
        catalog_agg.subAggregation(catalog_name_agg);
        sourceBuilder.aggregation(catalog_agg);
 
        //5.3 按照attrs聚合
        NestedAggregationBuilder nestedAggregationBuilder = new NestedAggregationBuilder("attr_agg", "attrs");
        //按照attrId聚合
        TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId");
        //按照attrId聚合之后再按照attrName和attrValue聚合
        TermsAggregationBuilder attr_name_agg = AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1);
        TermsAggregationBuilder attr_value_agg = AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50);
        attr_id_agg.subAggregation(attr_name_agg);
        attr_id_agg.subAggregation(attr_value_agg);
        nestedAggregationBuilder.subAggregation(attr_id_agg);
        sourceBuilder.aggregation(nestedAggregationBuilder);
 
        String s = sourceBuilder.toString();
        System.out.println("构建的DSL"+s);
 
        SearchRequest request = new SearchRequest(new String[]{
     EsConstant.PRODUCT_INDEX}, sourceBuilder);
        return request;
 
    }

6.构建查询结果数据

代码看不懂太正常了,回看Day03_谷粒商城(谷粒商城高级篇二)摘要

day02_《谷粒商城》的完整流程(详细版一)_第17张图片
day02_《谷粒商城》的完整流程(详细版一)_第18张图片
day02_《谷粒商城》的完整流程(详细版一)_第19张图片

        //5、分页信息-页码
        result.setPageNum(param.getPageNum());
        //5、1分页信息、总记录数
        long total = hits.getTotalHits().value;
        result.setTotal(total);

        //5、2分页信息-总页码-计算
        int totalPages = (int)total % EsConstant.PRODUCT_PAGESIZE == 0 ?
                (int)total / EsConstant.PRODUCT_PAGESIZE : ((int)total / EsConstant.PRODUCT_PAGESIZE + 1);
        result.setTotalPages(totalPages);

        List<Integer> pageNavs = new ArrayList<>();
        for (int i = 1; i <= totalPages; i++) {
     
            pageNavs.add(i);
        }
        result.setPageNavs(pageNavs);

day02_《谷粒商城》的完整流程(详细版一)_第20张图片

        SearchResult result = new SearchResult();

        //1、返回的所有查询到的商品
        SearchHits hits = response.getHits();

        List<SkuEsModel> esModels = new ArrayList<>();
        //遍历所有商品信息
        if (hits.getHits() != null && hits.getHits().length > 0) {
     
            for (SearchHit hit : hits.getHits()) {
     
                String sourceAsString = hit.getSourceAsString();
                SkuEsModel esModel = JSON.parseObject(sourceAsString, SkuEsModel.class);

                //判断是否按关键字检索,若是就显示高亮,否则不显示
                if (!StringUtils.isEmpty(param.getKeyword())) {
     
                    //拿到高亮信息显示标题
                    HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
                    String skuTitleValue = skuTitle.getFragments()[0].string();
                    esModel.setSkuTitle(skuTitleValue);
                }
                esModels.add(esModel);
            }
        }
        result.setProduct(esModels);

day02_《谷粒商城》的完整流程(详细版一)_第21张图片
day02_《谷粒商城》的完整流程(详细版一)_第22张图片

        //4、当前商品涉及到的所有分类信息
        //获取到分类的聚合
        List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
        ParsedLongTerms catalogAgg = response.getAggregations().get("catalog_agg");
        for (Terms.Bucket bucket : catalogAgg.getBuckets()) {
     
            SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
            //得到分类id
            String keyAsString = bucket.getKeyAsString();
            catalogVo.setCatalogId(Long.parseLong(keyAsString));

            //得到分类名
            ParsedStringTerms catalogNameAgg = bucket.getAggregations().get("catalog_name_agg");
            String catalogName = catalogNameAgg.getBuckets().get(0).getKeyAsString();
            catalogVo.setCatalogName(catalogName);
            catalogVos.add(catalogVo);
        }

        result.setCatalogs(catalogVos);

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

        //2、当前商品涉及到的所有属性信息
        List<SearchResult.AttrVo> attrVos = new ArrayList<>();
        //获取属性信息的聚合
        ParsedNested attrsAgg = response.getAggregations().get("attr_agg");
        ParsedLongTerms attrIdAgg = attrsAgg.getAggregations().get("attr_id_agg");
        for (Terms.Bucket bucket : attrIdAgg.getBuckets()) {
     
            SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
            //1、得到属性的id
            long attrId = bucket.getKeyAsNumber().longValue();
            attrVo.setAttrId(attrId);

            //2、得到属性的名字
            ParsedStringTerms attrNameAgg = bucket.getAggregations().get("attr_name_agg");
            String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString();
            attrVo.setAttrName(attrName);

            //3、得到属性的所有值(可能有多个值)
            ParsedStringTerms attrValueAgg = bucket.getAggregations().get("attr_value_agg");
            List<String> attrValues = attrValueAgg.getBuckets().stream().map(item -> item.getKeyAsString()).collect(Collectors.toList());
            attrVo.setAttrValue(attrValues);

            attrVos.add(attrVo);
        }

        result.setAttrs(attrVos);

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

        //3、当前商品涉及到的所有品牌信息
        List<SearchResult.BrandVo> brandVos = new ArrayList<>();
        //获取到品牌的聚合
        ParsedLongTerms brandAgg = response.getAggregations().get("brand_agg");
        for (Terms.Bucket bucket : brandAgg.getBuckets()) {
     
            SearchResult.BrandVo brandVo = new SearchResult.BrandVo();

            //1、得到品牌的id
            long brandId = bucket.getKeyAsNumber().longValue();
            brandVo.setBrandId(brandId);

            //2、得到品牌的名字
            ParsedStringTerms brandNameAgg = bucket.getAggregations().get("brand_name_agg");
            String brandName = brandNameAgg.getBuckets().get(0).getKeyAsString(); //品牌名只有一种情况,所以get(0)就可以
            brandVo.setBrandName(brandName);

            //3、得到品牌的图片
            ParsedStringTerms brandImgAgg = bucket.getAggregations().get("brand_img_agg");
            String brandImg = brandImgAgg.getBuckets().get(0).getKeyAsString();//品牌的默认图片只有一种情况,所以get(0)就可以
            brandVo.setBrandImg(brandImg);

            brandVos.add(brandVo);
        }
        result.setBrands(brandVos);


完整代码如下:


    private SearchResult buildSearchResult(SearchResponse response,SearchParam param) {
     
        SearchResult result = new SearchResult();
        //1、返回的所有查询到的商品
        SearchHits hits = response.getHits();
        List<SkuEsModel> esModels = new ArrayList<>();
        if (null != hits.getHits() && hits.getHits().length>0){
     
            for (SearchHit hit : hits.getHits()) {
     
                String sourceAsString = hit.getSourceAsString();
                SkuEsModel esModel = JSON.parseObject(sourceAsString, SkuEsModel.class);
                if (!StringUtils.isEmpty(param.getKeyword())) {
     
                    HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
                    esModel.setSkuTitle(skuTitle.fragments()[0].string());
                }
                esModels.add(esModel);
            }
        }
        result.setProducts(esModels);
        //2、当前所有商品涉及到的所有属性
        List<SearchResult.AttrVo> attrVos = new ArrayList<>();
        ParsedNested attr_agg = response.getAggregations().get("attr_agg");
        ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg");
        for (Terms.Bucket bucket : attr_id_agg.getBuckets()) {
     
            SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
            //1、得到属性的id;
            long attrId = bucket.getKeyAsNumber().longValue();
            //2、得到属性的名字
            String attrName = ((ParsedStringTerms) bucket.getAggregations().get("attr_name_agg")).getBuckets().get(0).getKeyAsString();
            //3、得到属性的所有值
            List<String> attrValues = ((ParsedStringTerms) bucket.getAggregations().get("attr_value_agg")).getBuckets().stream().map(item -> {
     
                String keyAsString = item.getKeyAsString();
                return keyAsString;
            }).collect(Collectors.toList());
            attrVo.setAttrId(attrId);
            attrVo.setAttrName(attrName);
            attrVo.setAttrValue(attrValues);
            attrVos.add(attrVo);
        }
 
        result.setAttrs(attrVos);
        //3、当前所有品牌涉及到的所有属性
        List<SearchResult.BrandVo> brandVos = new ArrayList<>();
        ParsedLongTerms brand_agg = response.getAggregations().get("brand_agg");
        for (Terms.Bucket bucket : brand_agg.getBuckets()) {
     
            SearchResult.BrandVo brandVo = new SearchResult.BrandVo();
            //1、得到品牌的id
            long brandId = bucket.getKeyAsNumber().longValue();
            //2、得到品牌的名
            String brandName = ((ParsedStringTerms) bucket.getAggregations().get("brand_name_agg")).getBuckets().get(0).getKeyAsString();
            //3、得到品牌的图片
            String brandImg = ((ParsedStringTerms) bucket.getAggregations().get("brand_img_agg")).getBuckets().get(0).getKeyAsString();
            brandVo.setBrandId(brandId);
            brandVo.setBrandName(brandName);
            brandVo.setBrandImg(brandImg);
            brandVos.add(brandVo);
        }
        result.setBrands(brandVos);
        //4、当前商品所涉及的分类信息
        ParsedLongTerms catalog_agg = response.getAggregations().get("catalog_agg");
 
        List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
        List<? extends Terms.Bucket> buckets = catalog_agg.getBuckets();
        for (Terms.Bucket bucket : buckets) {
     
            SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
            //得到分类id
            String keyAsString = bucket.getKeyAsString();
            catalogVo.setCatalogId(Long.parseLong(keyAsString));
 
            //得到分类名
            ParsedStringTerms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg");
            String catalog_name = catalog_name_agg.getBuckets().get(0).getKeyAsString();
            catalogVo.setCatalogName(catalog_name);
            catalogVos.add(catalogVo);
        }
        result.setCatalogs(catalogVos);
        //===========以上从聚合信息获取到=============
        //5、分页信息-页码
        result.setPageNum(param.getPageNum());
        //6、分页信息-总记录数
        long total = hits.getTotalHits().value;
        result.setTotal(total);
        //7、分页信息-总页码-计算
        int totalPages = total%EsConstant.PRODUCT_PAGESIZE == 0 ?(int) total/EsConstant.PRODUCT_PAGESIZE:((int)total/EsConstant.PRODUCT_PAGESIZE+1);
        result.setTotalPages(totalPages);
        return result;
    }

7.Controller语句

    @GetMapping("/list.html")
    public String listPage(SearchParam searchParam, Model model) {
     
        SearchResult result = mallSearchService.search(searchParam);
        System.out.println("===================="+result);
        model.addAttribute("result", result);
        return "list";
    }

8.总体逻辑:

前台把查询的条件封装到SearchParam里面,后台根据SearchParam查询ElastiSearch,后台写的Java代码事实上就是动态的DSL语句,用DSL语句查询ElasticSearch,把查询到的结果从DSL语句中提取出来,封装到SearchResult里面返回给前台。

p190—p192 ES检索之—面包屑功能

day02_《谷粒商城》的完整流程(详细版一)_第29张图片
day02_《谷粒商城》的完整流程(详细版一)_第30张图片

做法很简单,我们之前前台给后台传回去的SearchParam不变,但是后台返回给前台的SearchResult里面再添加一个新的字段List navs,在NavVo里面有navName,navValue,link这三个字段;

然后还是一如既往,后台根据SearchParam查询ElastiSearch,查询结果封装到SearchResult里面返回给前台,唯一变了的就是在封装结果时,要对List navs也要进一步封装。

假如你点击了一个属性是“高清屏”,那么前台传给后台就有attrs=4_高清屏,对于NavVo里的navValue其实就是"高清屏";对于NavVo里的navName其实就是根据attrId调用gulimall-product查询属性表得到attr_name,attrId不就是4嘛;对于NavVo里的link其实就是没点面包屑之前的url,点了面包屑不就是在原先url基础上多拼装了一个attrs=4_高清屏嘛,所以你从前端拿到现在的url(也就是点了面包屑以后的url)然后切割一下就行了。



需要注意的三个点:
①因为远程调用gulimall-product所以可以在被调用的gulimall-product的那个方法上添加缓存@cacheable(value = "attr",key = " 'attrInfo'+#root.args[0]")
②如何从前端拿到现在的url?

day02_《谷粒商城》的完整流程(详细版一)_第31张图片

③通过以上方法拿到的前端的url是被URL编码的结果&attrs=%257B%2522request%255Fid%2522%253A%25,不是你想要的url,所以你需要先解码。


分割线

P193—P202 多线程

P193—P202是对多线程知识的讲解,和项目独立

P203—P210 商品详情页

前台传回来的只有skuid,然后查询对应的表得到对应的封装信息

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

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


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


day02_《谷粒商城》的完整流程(详细版一)_第37张图片
day02_《谷粒商城》的完整流程(详细版一)_第38张图片
day02_《谷粒商城》的完整流程(详细版一)_第39张图片
day02_《谷粒商城》的完整流程(详细版一)_第40张图片


在这里插入图片描述

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


day02_《谷粒商城》的完整流程(详细版一)_第42张图片
spu是华为手机,sku是华为手机中具体的那个亮黑色128G的手机 ,spu和sku肯定在同一个三级分类下,它们的三级分类都是手机。
day02_《谷粒商城》的完整流程(详细版一)_第43张图片
day02_《谷粒商城》的完整流程(详细版一)_第44张图片
day02_《谷粒商城》的完整流程(详细版一)_第45张图片
day02_《谷粒商城》的完整流程(详细版一)_第46张图片
day02_《谷粒商城》的完整流程(详细版一)_第47张图片
day02_《谷粒商城》的完整流程(详细版一)_第48张图片
根据spuid和catalogid找到当前sku下的所有spu属性及其对应的值


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

返回给前台的Vo

@Data
public class SkuItemVo {
     
 
    //1、sku基本信息获取    pms_sku_info
    SkuInfoEntity info;
 
    //2、sku的图片信息      pms_sku_images
    List<SkuImagesEntity> images;
 
    //3、获取spu的销售属性组合
    List<SkuItemSaleAttrVo> saleAttr;
 
    //4、获取spu的介绍
    SpuInfoDescEntity desc;
 
    //5、获取spu的规格参数信息
    List<SpuItemAttrGroupVo> groupAttrs;
 
}


@Data
public class SkuItemSaleAttrVo {
     
 
    private Long attrId;
 
    private String attrName;
 
    private String attrValues;
}



@ToString
@Data
public class SpuItemAttrGroupVo {
     
 
    private String groupName;
 
    private List<Attr> attrs;
}

分割线

P211—P214 注册页面—验证码功能

总说:

完成用户在注册页面的发送验证码的操做:前台发送/sms/sendcode的请求给后台的gulimall-auth-server,然后gulimall-auth-server会先验证一下验证码是否在60秒前发送过(接口防刷),如果没有就使用OpenFeign远程调用gulimall-thrid-party的sendCode方法完成第三方服务的发送验证码功能。

关于接口防刷

gulimall-auth-server如何校验验证码是否在60秒前发送过?当前台带着手机号发送/sms/sendcode的请求给后台,后台先到redis中根据key为(“sms:code:”+phone)尝试获取这段redis信息,如果获取不到,后台会在redis中存储(key为"sms:code:"+phone,value为"验证码_当前时间",过期时间是10分钟)的一段信息,然后远程调用发送验证码方法;假如60秒内前台带着该手机号再次发送/sms/sendcode的请求给后台,后台先到redis中根据key为(“sms:code:”+phone)尝试获取这段redis信息,如果能够获取到这段信息就判断时间差是否小于60s,如果是就不进行发送验证码操做。

在这里插入图片描述

P215—P216 注册页面—注册功能1

1.总说:

p211—p214是完成用户在注册页面的发送验证码的功能,并且添加接口防刷;这里只实现了获取验证码,没有做验证码校验
p215和p216和p217和p218的这些操作是完成注册功能:我们之前把验证码发给用户了,用户会填好验证码和注册信息封装到UserRegistVo后发送给gulimall-auth-server,然后gulimall-auth-server会进行JSR303校验(校验密码格式、手机号格式 )和Redis中验证码校验,校验通过会利用OpenFeign远程调用gulimall-member的regist()方法来进行会员注册,会员注册肯定有失败有成功,对于那些注册失败我们使用异常机制

在这里插入图片描述

2.注册功能的实现:

LoginController类的逻辑如下:
首先进行JSR303校验,若JSR303校验未通过,则通过BindingResult封装错误信息,并重定向至注册页面;
若通过JSR303校验,则需要从redis中取值判断验证码是否正确,正确的话远程调用会员服务注册;
会员服务调用成功则重定向至登录页,否则封装远程服务返回的错误信息返回至注册页面。


编写UserRegistVo类,代码如下:

@Data
public class UserRegistVo  {
     
    @NotEmpty(message = "用户名必须提交")
    @Length(min = 6, max = 18, message = "用户名必须是6-18位字符")
    private String userName;
 
    @NotEmpty(message = "密码必须填写")
    @Length(min = 6, max = 18, message = "密码必须是6-18位字符")
    private String password;
 
    @NotEmpty(message = "手机号必须填写")
    @Pattern(regexp = "^[1]([3-9])[0-9]{9}$",message = "手机号格式不正确")
    private String phone;
 
    @NotEmpty(message = "验证码必须填写")
    private String code;
}

编写LoginController类,下面的注释一定一定要好好看!!!

    /**
     * 下面的代码可以说相当重要,regist()方法一共有三个参数,UserRegistVo是封装前台传过来的数据,BindingResult封装JSR303校验错误信息
     * RedirectAttributes是重定向携带数据。转发的时候session共享数据,重定向的时候如何共享数据呢?
     * 使用RedirectAttributes,它利用session原理。将数据放在session中。只要跳到下一个页面,取出数据以后,session里面的数据就会删掉。
     */
    @PostMapping("/regist")
    public String regist(@Valid UserRegistVo vo, BindingResult result,
                         RedirectAttributes redirectAttributes){
     
        if (result.hasErrors()){
     

            //如果校验不通过,则封装校验结果,将错误信息封装到redirectAttributes中
            Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
            redirectAttributes.addFlashAttribute("errors",errors);
            
            //使用return "reg"; 转发会出现重复提交的问题,不要以转发的方式
            //使用 return "forward:/reg.html"; 会出现问题:Request method 'POST' not supported的问题(原因:用户注册-> /regist[post] ------>转发/reg.html (路径映射默认都是get方式访问的))
            //使用重定向  解决重复提交的问题。但面临着数据不能携带的问题,就用RedirectAttributes。
            return "redirect:http://auth.gulimall.com/reg.html";
        }
 
        //1、校验验证码
        String code = vo.getCode();
        String s = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
        if (!StringUtils.isEmpty(s)) {
     
            if (code.equals(s.split("_")[0])) {
     
                //验证码通过,删除缓存中的验证码;令牌机制
                stringRedisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
                //真正注册调用远程服务注册
                R r = memberFeignService.regist(vo);
                if (r.getCode() == 0) {
     
                    //成功
                    return "redirect:http://auth.gulimall.com/login.html";
                } else {
     
                    Map<String, String> errors = new HashMap<>();
                    errors.put("msg", r.getData(new TypeReference<String>() {
     
                    }));
                    redirectAttributes.addFlashAttribute("errors", errors);
                }
            } else {
     
                Map<String, String> errors = new HashMap<>();
                errors.put("code", "验证码错误");
                redirectAttributes.addFlashAttribute("errors", errors);
                return "redirect:http://auth.gulimall.com/reg.html";
            }
        } else {
     
            Map<String, String> errors = new HashMap<>();
            errors.put("code", "验证码错误");
            redirectAttributes.addFlashAttribute("errors", errors);
            return "redirect:http://auth.gulimall.com/reg.html";//校验出错重定向到注册页
        }
 
        //注册成功回到登录页
        return "redirect:http://auth.gulimall.com/login.html";
    }

远程调用会员服务,会员服务干了什么?会先从ums_member_level表查询用户默认等级;然后会查询ums_member表的phone的数量是否大于0,如果大于0说明手机号已经存在,返回错误信息;查询ums_member表的username的数量是否大于0,如果大于0说明用户名已经存在,返回错误信息;如果ums_member表中手机号数量问0、用户名数据为0,那就把(用户等级、用户名、手机号、密码)一并存入ums_member表,其中密码加密适用了BCrypt加密方式

day02_《谷粒商城》的完整流程(详细版一)_第50张图片
在这里插入图片描述
在这里插入图片描述

P217 注册页面—注册功能3

给用户密码加密的三种方式对比:MD5加密、盐值加密、BCrypt加密

可逆加密:知道了加密算法后通过密文可以推算出原来的明文
不可逆加密:即使知道了加密算法通过密文也不可以推算出原来的明文

①MD5加密:知道了密文可以推算出原来的明文,网上随处可找MD5破解

        String s = DigestUtils.md5Hex("123456");
        System.out.println(s);//e10adc3949ba59abbe56e057f20f883e

②MD5加盐(盐值加密)
可以给随机盐也可以给指定盐值,反正就是对“密码+盐值”进行MD5加密,你只能把盐值保存起来然后下一次对“密码+盐值”进行再加密然后比对密文是否一致来判断用户名密码正确与否

		String s = Md5Crypt.md5Crypt("123456".getBytes(),"$1$qqqqqqqq");  //$1$qqqqqqqq就是你指定的盐值
        System.out.println(s); //$1$qqqqqqqq$AZofg3QwurbxV3KEOzwuI1

③BCrypt加密
Spring家的BCrypt加密,即使明文一样,每次加密的密文都不一样,但是你可以匹配明文和密文,人家就会告诉你这两个匹配与否

        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        String encode = bCryptPasswordEncoder.encode("123456");//$2a$10$GT0TjB5YK5Vx77Y.2N7hkuYZtYAjZjMlE6NWGE2Aar/7pk/Rmhf8S
        boolean matches = bCryptPasswordEncoder.matches("123456", "$2a$10$GT0TjB5YK5Vx77Y.2N7hkuYZtYAjZjMlE6NWGE2Aar/7pk/Rmhf8S");
        System.out.println(matches);//true

P218 注册页面—注册功能4

本届内容就是完成测试

day02_《谷粒商城》的完整流程(详细版一)_第51张图片
day02_《谷粒商城》的完整流程(详细版一)_第52张图片
day02_《谷粒商城》的完整流程(详细版一)_第53张图片
在这里插入图片描述

P219 登陆页面—用户名密码登录

总说:

前台发送 /login到后台的gulimall-auth-server模块中,然后gulimall-auth-server会使用OpenFeign远程调用gulimall-member的login()方法,在该方法中根据用户名查询ums_member表拿到MemberEntity然后进行用户名密码比对完成登录,登录成功重定向到首页,登陆失败重定向到登录页

在这里插入图片描述

P220—P224 登陆页面—完成微博登录

1.微博登陆的流程

这里是引用

2.有两个地址很重要

(1)是“ 在登录页引导用户至授权页”的地址:
这一步是前台完成的,前台html中的url要写成

Get
https://api.weibo.com/oauth2/authorize?client_id=1917008757&response_type=code&redirect_uri=http://gulimall.com/oauth2.0/weibo/success

client_id:是你创建网站应用时的app key,
redirect_uri是用户使用微博登录后重定向到哪里去。

我们指定redirect_uri=http://gulimall.com/oauth2.0/weibo/success也就是说用户用户使用微博登录后,相当于发送 /oauth2.0/weibo/success到后台的gulimall-auth-server模块中,那么gulimall-auth-server会使用code换取token,这就涉及到换取token的url:

(2)是换取token的url
这一步是后台完成的,后台发送这样的url才能获取到token

POST
https://api.weibo.com/oauth2/access_token?client_id=1917008757&client_secret=94d9cc62c60d5f9f3d0c62389593024f&grant_type=authorization_code&redirect_uri=http://auth.gulimall.com/oauth2.0/weibo/success&code=CODE

client_id: 创建网站应用时的app key;
client_secret: 创建网站应用时的app secret
redirect_uri: 认证完成后的跳转链接(需要和平台高级设置一致);
code:换取令牌的认证码

后台发送这么个请求就可以根据用户授权返回的code换取token(换回来的不仅仅是token,还有uid用户id、expires_in令牌的过期时间等等,这些被封装到SocialUser中),拿到SocialUser中的token就可以向微博官方发送别的请求换取用户信息

3.微博登陆的具体流程

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

4.编码总说:

①前台带着code发送 /oauth2.0/weibo/success请求到后台的gulimall-auth-server模块中,然后gulimall-auth-server会先使用code换取SocialUser,然后拿着SocialUser到OpenFeign远程调用gulimall-memberoauth2Login()方法,在该方法中会先用SocialUser的uid查询数据库来判断用户是否是第一次用微博登录,如果是第一次的话我们就得给该用户注册(拿着token到微博里面查询该用户的基本信息,然后insert到咱们的数据库里面);如果该用户之前已经用微博登陆过,那就到数据库中更新一下token。
如果一切顺利,gulimall-member就会带着MemberEntity(封装着用户的所有信息)返回到gulimall-auth-server,然后gulimall-auth-server会把MemberEntity设置到RedirectAttributes然后重定向到http://gulimall.com;如果不顺利就把error信息返回到gulimall-auth-server,然后gulimall-auth-server会把error信息封装到RedirectAttributes然后重定向到http://auth.gulimall.com/login.html

②前台用户用微博登录后我们会拿到用户的code,后台用code到微博里面换取token这样才能用token访问到用户基本信息;用户每登陆一次访问微博的token就会变一次,所以当用户下次用微博登陆时我们需要到数据库更新一下token

5.代码(可看不可不看)

用code换取的用户信息封装到SocialUser中

@Data
public class SocialUser {
     
    private String access_token;
    private String remind_in;
    private long expires_in;
    private String uid;
    private String isRealName;
}

gulimall-auth-server的Controller

@Controller
public class OauthController {
     
 
    @Autowired
    private MemberFeignService memberFeignService;
 
    @RequestMapping("/oauth2.0/weibo/success")
    public String authorize(String code, RedirectAttributes attributes) throws Exception {
     
    
        //使用code换取token
        Map<String, String> query = new HashMap<>();
        query.put("client_id", "2144***074");
        query.put("client_secret", "ff63a0d8d5*****29a19492817316ab");
        query.put("grant_type", "authorization_code");
        query.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/weibo/success");
        query.put("code", code);
        //发送post请求换取token
        HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", new HashMap<String, String>(), query, new HashMap<String, String>());
        
        Map<String, String> errors = new HashMap<>();
        
        if (response.getStatusLine().getStatusCode() == 200) {
     
            //调用member远程接口进行oauth登录
            String json = EntityUtils.toString(response.getEntity());
            SocialUser socialUser = JSON.parseObject(json, new TypeReference<SocialUser>() {
     
            });
            R login = memberFeignService.login(socialUser);
            
            //远程调用成功,返回首页并携带用户信息
            if (login.getCode() == 0) {
     
                String jsonString = JSON.toJSONString(login.get("memberEntity"));
                MemberResponseVo memberResponseVo = JSON.parseObject(jsonString, new TypeReference<MemberResponseVo>() {
     
                });
                attributes.addFlashAttribute("user", memberResponseVo);
                //MemberResponseVo和MemberEntity里的字段一模一样,就是封装着用户的基本信息
                return "redirect:http://gulimall.com";
            }else {
     
                //否则返回登录页
                errors.put("msg", "登录失败,请重试");
                attributes.addFlashAttribute("errors", errors);
                return "redirect:http://auth.gulimall.com/login.html";
            }
        }else {
     
            errors.put("msg", "获得第三方授权失败,请重试");
            attributes.addFlashAttribute("errors", errors);
            return "redirect:http://auth.gulimall.com/login.html";
        }
    }

被远程调用的gulimall-member的登录方法:

@Override
    public MemberEntity login(SocialUser socialUser) {
     
        //根据 uid 判断当前用户是否以前用社交平台登录过系统
        MemberEntity memberEntity = this.baseMapper.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", socialUser.getUid()));
        if (!StringUtils.isEmpty(memberEntity)) {
     
            // 说明这个用户之前已经注册过
            MemberEntity update = new MemberEntity();
            update.setId(memberEntity.getId());
            update.setAccessToken(socialUser.getAccess_token());
            update.setExpiresIn(socialUser.getExpires_in());
            this.baseMapper.updateById(update);
 
            memberEntity.setAccessToken(socialUser.getAccess_token());
            memberEntity.setExpiresIn(socialUser.getExpires_in());
            return memberEntity;
        } else {
     
            // 未找到则注册,说明用户是第一次用微博登录
            MemberEntity register = new MemberEntity();
            try {
     
            	//拿着token到微博里面查询该用户的基本信息
                Map<String, String> query = new HashMap<>();
                query.put("access_token", socialUser.getAccess_token());
                query.put("uid", socialUser.getUid());
                HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", "get", new HashMap<>(), query);
                if (response.getStatusLine().getStatusCode() == 200) {
     
                    String json = EntityUtils.toString(response.getEntity());
                    JSONObject jsonObject = JSON.parseObject(json);
                    String name = jsonObject.getString("name");
                    String gender = jsonObject.getString("gender");
                    // ......
                    register.setNickname(name);
                    register.setGender("m".equals(gender) ? 1 : 0);
                    // .....
                }
            } catch (Exception e) {
     }
            register.setSocialUid(socialUser.getUid());
            register.setAccessToken(socialUser.getAccess_token());
            register.setExpiresIn(socialUser.getExpires_in());
            this.baseMapper.insert(register);
            return register;
        }
    }

P225 SpringSession—session不共享、不跨域问题

1.session不能跨域问题

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

2.分布式下session共享问题

多台服务器都有会员服务,你在A服务器上把用户信息保存到内存上了,下次如果落在B服务器上,即使浏览器带着cookie来了,由于B服务器内存肯定没有存储用户信息,这也是问题。

这里是引用

3.session共享问题的解决方案

  1. session复制
    用户登录后,A服务器得到session后,把session也复制到别的机器上,显然这种处理很不好

  2. 客户端存储
    把session存储到浏览器上,肯定相当不安全

  3. hash一致性
    根据用户,到指定的机器上登录。但是远程调用还是不好解决

  4. redis统一存储
    最终的选择方案,把session放到redis中,这样每个微服务都可以获取到session

4.总说:

浏览器会在auth.gulimall.com里面登录成功,auth.gulimall.com会将登陆成功的用户的从数据库查到的用户相关信息存到session里面,而且存session时不是存到自己的内存里面而是存到redis里面,然后auth.gulimall.com给浏览器发cookie,而且发的cookie的作用域不能仅仅是auth.gulimall.com而是要放大服务到.gulimall.com,此时浏览器访问其它任何服务都会带上这个cookie。

如果你把redis里面的session清空,那就是把登陆过的用户信息清空,虽然前台的浏览器访问后台时携带了cookie信息,但是到redis里面查不到用户信息,所以你就得重新登陆。而且我们设置了redis里面的session默认30分钟过期,也就是30分钟后redis里面的用户信息就没有了

在这里插入图片描述
day02_《谷粒商城》的完整流程(详细版一)_第55张图片
在这里插入图片描述

5.修改微博登陆的代码

①修改sprinsession的存储类型是redis(这很重要·,以后存到session中就是存到redis中)

spring:
  session:
    store-type: redis

②增加一个配置类,由于默认使用jdk进行序列化,通过导入RedisSerializer修改为json序列化,并且通过修改CookieSerializer扩大session的作用域至**.gulimall.com

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

③修改gulimall-auth-server的Controller

以前的逻辑是:

这是微博登陆的代码,前台带着code发送 /oauth2.0/weibo/success请求到后台的gulimall-auth-server模块中,然后gulimall-auth-server会先使用code换取SocialUser,然后拿着SocialUser到OpenFeign远程调用gulimall-memberoauth2Login()方法,在该方法中如果是第一次的话我们就得给该用户注册;如果该用户之前已经用微博登陆过,那就到数据库中更新一下token。
如果一切顺利,gulimall-member就会带着MemberEntity(封装着用户的所有信息)返回到gulimall-auth-server,然后gulimall-auth-server会把MemberEntity设置到RedirectAttributes然后重定向到http://gulimall.com;如果不顺利就把error信息返回到gulimall-auth-server,然后gulimall-auth-server会把error信息封装到RedirectAttributes然后重定向到http://auth.gulimall.com/login.html

现在的逻辑就是:

gulimall-member就会带着MemberEntity(封装着用户的所有信息)返回到gulimall-auth-server,然后gulimall-auth-server会把MemberEntity设置到SpringSession中然后重定向到http://gulimall.com;如果不顺利就把error信息返回到gulimall-auth-server,然后gulimall-auth-server会把error信息封装到RedirectAttributes然后重定向到http://auth.gulimall.com/login.html

@Controller
public class OauthController {
     
 
    @Autowired
    private MemberFeignService memberFeignService;
 
    @RequestMapping("/oauth2.0/weibo/success")
    public String authorize(String code, RedirectAttributes attributes) throws Exception {
     
    
        //使用code换取token
        Map<String, String> query = new HashMap<>();
        query.put("client_id", "2144***074");
        query.put("client_secret", "ff63a0d8d5*****29a19492817316ab");
        query.put("grant_type", "authorization_code");
        query.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/weibo/success");
        query.put("code", code);
        //发送post请求换取token
        HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", new HashMap<String, String>(), query, new HashMap<String, String>());
        
        Map<String, String> errors = new HashMap<>();
        
        if (response.getStatusLine().getStatusCode() == 200) {
     
            //调用member远程接口进行oauth登录
            String json = EntityUtils.toString(response.getEntity());
            SocialUser socialUser = JSON.parseObject(json, new TypeReference<SocialUser>() {
     
            });

=========================================================================================================
            R r = memberFeignService.oauth2Login(socialUser);
            //远程调用成功,返回首页并携带用户信息
            if (r.getCode() == 0) {
     
                // MemberResponseVO和MemberEntity的字段一模一样
                MemberResponseVO loginUser = r.getData(new TypeReference<MemberResponseVO>() {
     });
                log.info("登陆成功:用户信息"+loginUser.toString());
				//有了下面这行代码,一来redis中可以看到你存的session,二来浏览器中session的作用域也被放大到gulimall.com
                session.setAttribute("loginUser", loginUser);
========================================================================================================= 

                return "redirect:http://gulimall.com";
            } else {
     
                //2.2 否则返回登录页
                errors.put("msg", "登录失败,请重试");
                attributes.addFlashAttribute("errors", errors);
                return "redirect:http://auth.catmall.com/login.html ";
            }
        }else {
     
            errors.put("msg", "获得第三方授权失败,请重试");
            attributes.addFlashAttribute("errors", errors);
            return "redirect:http://auth.gulimall.com/login.html";
        }
    }

6.修改账号密码登录的代码

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

P231—P235 单点登录

1.为什么要单点登录?

springsession只能把auth.gulimall.com作用域放大到gulimall.com,解决了同域名的共享session问题,但要是访问同样是尚硅谷的atguigu.com怎么办呢?这种不同的域名也想共享session该怎么做呢?
你在新浪微博里面注册登录了,同时就要保证在新浪体育、新浪新闻里面全都可以拿到session数据

2.单点登录的原理?

两个域名不一样的服务端client1和client2,还有一个负责登录的ssoserver,还有一个浏览器,它们四个之间的故事

请添加图片描述

先说明一下这个路径的含义:http://ssoserver.com:8080/login.html?redirect_url=http:I/client1.com:8081/employees的含义就是让你访问http://ssoserver.com:8080/login.html登陆页面,而 redirect_url=http:I/client1.com:8081/employees的含义是当你完成登陆后会重定向到http:I/client1.com:8081/employees的位置

第1-11步的解析:只有登陆了才能查看员工信息。一开始浏览器访问client1.com的员工信息http:I/client1.com:8081/employees,client1会根据这个url有没有token参数判断是否登录,由于没有token参数也就是没有登陆,服务端会命令浏览器重定向到ssoserver.com的登陆页面http:I/ssoserver.com:8080/login.html?redirect_url=http:I/client1.com:8081/employees,ssoserver.com会判断是否登陆过,没有登陆过就展示这个登陆页面,用户会输入账号密码进行登录,提交登陆请求http:/ssoserver.com:8080/doLogin?usermame,password,redirect_url给ssoserver.com,那么ssoserver.com会保存用户状态到redis,同时ssoserver.com会命令重定向到http: /lclient1.com:8081/employees?token=dadadadsdeuieu(浏览器访问路径),同时ssoserver.com会命令浏览器保存sso_token=dadadadsdeuieu这样式的cookie。浏览器这次就可以访问员工信息了,他的访问路径是刚刚提到的http://lclient1.com:8081/employees?token=dadadadsdeuieu比一开始访问员工信息的http:I/client1.com:8081/employees多了token=dadadadsdeuieu,这就回到第2步了,client1会根据有没有token参数判断是否登录,这次client1会觉得它登陆过了就可以访问员工信息了。

第12-19步解析:这次浏览器要访问客户端2的boss信息http:I/client2.com:8081/boss,client2会根据有没有token参数判断是否登录,由于没有token参数也就是没有登陆,服务端会命令浏览器重定向到ssoserver.com的登陆页面http:I/ssoserver.com:8080/login.html?redirect_url=http:I/client2.com:8081/boss,ssoserver.com会判断是否登陆过,由于浏览器有sso_token=dadadadsdeuieu这样式的cookie,而且从redis能查到,说明它之前在client1或者client2登陆过,ssoserver.com会命令重定向到http:/lclient2.com:8082/boss?token=dadadadsdeuieu,所以浏览器就会访问http://lclient2.com:8082/boss?token=dadadadsdeuieu,这就回到了第2步,client2会根据有没有token参数判断是否登录,登陆过就响应页面。

所以说,以后浏览器无论访问client1还是client2,由于浏览器中保存了cookie,所以ssoserver.com就会判定它登陆过,所以以后都不用登陆。

3.单点登录代码的实现

client1的代码:

@GetMapping(value = "/employees") 
public String employees(Model model,
                        HttpSession session,
                        @RequestParam(value = "redisKey", required = false) String redisKey) {
     

    if (!StringUtils.isEmpty(redisKey)) {
      
    	//redisKey非空(也就是token非空),说明去过server端登录过了
    	
    	// 拿着token去服务器,在服务端从redis中查出来用户的username
        RestTemplate restTemplate=new RestTemplate();
        
        ResponseEntity<Object> forEntity = restTemplate.getForEntity("http://ssoserver.com:8080/userInfo?redisKey="+ redisKey, Object.class);
        
        Object loginUser = forEntity.getBody();
        
        session.setAttribute("loginUser", loginUser);//设置到自己的session中
    }
    //尝试从自己的session中获取"loginUser"
    Object loginUser = session.getAttribute("loginUser");

    if (loginUser == null) {
      
    	//又没有token,session里又没有"loginUser",让它去登录页登录
        return "redirect:" + "http://ssoserver.com:8080/login.html" + "?url=http://clientA.com/employees";
    } else {
     
    	//自己的session里有"loginUser",即使没有token也说明登录过
        List<String> emps = new ArrayList<>();
        emps.add("张三");
        emps.add("李四");
        model.addAttribute("emps", emps);
        return "employees"; //转到前端页面,前端会把数据拿出来展示
    }
}

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

client2的代码:

代码一模一样,就是改一下访问路径@GetMapping(value = "/boss")

ssoserver的代码:

<body>
<form action="/doLogin" method="post">
    
    <input type="hidden" name="url" th:value="${url}">
    

        用户名:<input name="username" value="test"><br/>
        密码:<input name="password" type="password" value="test">
    <input type="submit" value="登录">
form>
body>
@Controller
public class LoginController {
     

	@Autowired
	private StringRedisTemplate stringRedisTemplate;

	@ResponseBody
	@GetMapping("/userInfo") //client1或client2会调用这个方法得到redis中的存储过的user信息
	public Object userInfo(@RequestParam("redisKey") String redisKey){
     
		// 拿着其他域名转发过来的token去redis里查
		Object loginUser = stringRedisTemplate.opsForValue().get(redisKey);
		return loginUser;
	}


	@GetMapping("/login.html") // 子系统都来这
	public String loginPage(@RequestParam("url") String url,
							Model model,
							@CookieValue(value = "redisKey", required = false) String redisKey) {
     
							
		//这是从浏览器中拿到的cookie,非空代表就登录过了
		if (!StringUtils.isEmpty(redisKey)) {
     
			//非空代表就登录过了
			return "redirect:" + url + "?redisKey=" + redisKey;
		}
		
		model.addAttribute("url", url);

		//没登录过才去登录页
		return "login";
	}


	@PostMapping("/doLogin") //在前端输入用户名和密码后就会来到这里,进行server端统一认证
	public String doLogin(@RequestParam("username") String username,
						  @RequestParam("password") String password,
						  HttpServletResponse response,
						  @RequestParam(value="url",required = false) String url){
     
						  
		//确认用户后,生成cookie,浏览器中存储,redis中也存储
		if(!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)){
     //非空就简单认为登录正确

			String redisKey = UUID.randomUUID().toString().replace("-", "");//用uuid代替token
			
			Cookie cookie = new Cookie("redisKey", redisKey);
			
			response.addCookie(cookie);//浏览器中存储cookie
			
			stringRedisTemplate.opsForValue().set(redisKey, username+password+"...", 30, TimeUnit.MINUTES);//redis中存储
			
			return "redirect:" + url + "?redisKey=" + redisKey;//重定向时候带着token
		}
		
		// 登录失败,再次登录
		return "login";
	}

}

演示

代码用的网友的,截屏用到老师的,网友喜欢自己起名字,把token改为redisKey什么的,不要计较细节上的不同

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

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

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

P236 购物车—环境的搭建

P237 购物车—数据模型的分析1

P238 购物车—数据模型的分析2

总说:

本节内容就是说明了用户购物车里的信息应该使用哪个数据库存储(MySQL还是Redis?),以及使用了Redis后是用List存储这些信息呢还是使用Hash存储这些信息?以及购物车VO、购物项VO的编写
day02_《谷粒商城》的完整流程(详细版一)_第64张图片

在这里插入图片描述

day02_《谷粒商城》的完整流程(详细版一)_第65张图片
在这里插入图片描述

P239 购物车—拦截器鉴别用户

1.总说:

在购物车的所有Controller执行之前,我们先执行一个拦截器。在拦截器里判断用户是否登录,从session中获取不到用户信息就说明他没有登录,没有登录的话就从浏览器中获取一下user-key,如果浏览器中没有user-key那就说明用户是第一次没有登录的状态下进入京东,我们就得创建一个cookie名字叫做user-key,而且设置cookie的作用域、过期时间,假如明天他来了,我们能从浏览器中获取到该用户的user-key。

一个用户进来我们执行的 “ 拦截器—Controller—Service—Dao ” 这一套流程让同一个线程执行,这就使用了ThreadLocal技术,ThreadLocal是同一个线程共享数据,这个线程里面的数据会共享,使用过程就是:

ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();//创建一个threadLocal
threadLocal.set(userInfoTo);//把要共享的数据设置进去
....
UserInfoTo userInfoTo = threadLocal.get();//后期就可以获取到这个共享的数据

在这里插入图片描述
day02_《谷粒商城》的完整流程(详细版一)_第66张图片

2.代码

UserInfoTo如下:

@ToString
@Data
public class UserInfoTo {
     
 
    private Long userId;
 
    private String userKey; 
 
    private boolean tempUser = false;  //这个相当重要,我们会根据tempUser是true还是false来决定有没有执行postHandle()方法
}

登陆拦截器如下:

/**
 * @Description: 在执行目标方法之前,判断用户的登录状态。并封装传递给目标请求
 */
public class CartInterceptor implements HandlerInterceptor {
     

    //ThreadLocal同一个线程共享数据
    public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();
    
    /**
     * 在目标方法执行之前拦截
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
     
        UserInfoTo userInfoTo = new UserInfoTo();
        HttpSession session = request.getSession();
        MemberResponseVO member = (MemberResponseVO) session.getAttribute(AuthServerConstant.LOGIN_USER);
        
        if (member != null){
     
            //用户登录
            userInfoTo.setUserId(member.getId());
 
        }

		//用户没有登陆:
        Cookie[] cookies = request.getCookies();
        if (cookies!=null && cookies.length >0){
     
        	//有临时用户信息
            for (Cookie cookie : cookies) {
     
                String name = cookie.getName();
                if (name.equals(CartConstant.TEMP_USER_COOKIE_NAME)){
     
                    userInfoTo.setUserKey(cookie.getValue());
                    userInfoTo.setTempUser(true);
                }
            }
        }
 
        //用户没有登陆,而且没有临时用户信息,一定保存一个临时用户
        if (StringUtils.isEmpty(userInfoTo.getUserKey())){
     
            String uuid = UUID.randomUUID().toString();
            userInfoTo.setUserKey(uuid);
        }
        
        //userInfoTo存到threadLocal中
        threadLocal.set(userInfoTo);
        return true;
    }
 
    /**
     * 业务执行之后 分配临时用户,让浏览器保存
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
     
        UserInfoTo userInfoTo = threadLocal.get();
        //如果没有临时用户,第一次访问购物车就添加临时用户
        if (!userInfoTo.isTempUser()){
     
            //持续的延长用户的过期时间
            Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
            cookie.setDomain("gulimall.com");
            cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
            response.addCookie(cookie);
        }
 
    }
}

请你假设一下以下三种情况在拦截器中会发生什么:
如果用户没有登录,而且浏览器中没有用户的临时信息:UserInfoTo中的userId是空的,但userKey不是空的
如果用户没有登录,但是浏览器中有用户的临时信息:UserInfoTo中的userId是空的,但userKey不是空的
如果用户已经登录,UserInfoTo中的userId不是空的,但userKey是空的

登录Controller如下:

@Controller
public class CartController {
     
 
    /**
     * 登录  session有
     * 没登录,按照cookie里面带来的user-key来做
     * 第一次,如果没有临时用户,帮忙创建一个临时用户
     */
    @GetMapping("/cart.html")
    public String cartListPage(){
     
 
        //快速得到用户信息,id,user-key
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        System.out.println(userInfoTo);
        return "cartList";
    }
}

P240 页面调整

拦截器写了,Controller还有service什么的都没有写,在此之前,我们先打通整个页面(首页可以进入商品页,从商品页添加商品到购物车,然后点击购物车就可以进入购物车页),本节内容是前台代码。

在这里插入图片描述
day02_《谷粒商城》的完整流程(详细版一)_第67张图片
在这里插入图片描述
在这里插入图片描述
day02_《谷粒商城》的完整流程(详细版一)_第68张图片
在这里插入图片描述

P241 添加商品到购物车1

P242 添加商品到购物车2

P243 添加商品到购物车3

1.总说:

getCartOps()方法里面逻辑:

因为是拦截器先执行的,所以先得到拦截器ThreadLocal的返回结果UserInfoTo userInfoTo = threadLocal.get(),如果userInfoTo.getUserId()不为空表示账号用户,反之为临时用户 ,然后决定用临时购物车还是用户购物车。将用户购物车信息存到redis中,redis中肯定需要键值对,账号用户的购物车的redis中的key是gulimall:cart:1(1是用户id,表示1号用户的购物车);临时用户的redis中的key是gulimall:cart:uuid其中uuid就是我们拦截器里存下的user-key。 redisTemplate.boundHashOps(cartKey)是说以后所有对redis的增删改查都是针对redia中key为cartKey的增删改查。

addToCart()方法里面的逻辑:

添加新商品到购物车,第一步先看redis里面能不能查到skuid,查不到说明购物车里面之前没有添加过此商品,那就需要远程查询此商品的一系列信息;能查到说明购物车有此商品,将数据取出修改数量即可。

在这里插入图片描述
在这里插入图片描述
day02_《谷粒商城》的完整流程(详细版一)_第69张图片

2.代码

gulimall-cart的Controller:

    /**
     * 添加商品到购物车
     * @return
     */
    @GetMapping("/addToCart")
    public String addToCart(@RequestParam("skuId") Long skuId,
                            @RequestParam("num") Integer num,
                            Model model) throws ExecutionException, InterruptedException {
     
        CartItem cartItem = cartService.addToCart(skuId,num);
        model.addAttribute("item",cartItem);
        return "success";
    }

gulimall-cart的ServiceImpl:

@Slf4j
@Service
public class CartServiceImpl implements CartService {
     
    private final String CART_PREFIX = "gulimall:cart";
    @Autowired
    StringRedisTemplate redisTemplate;
 
    @Autowired
    ProductFeignService productFeignService;
 
    @Autowired
    ThreadPoolExecutor executor;
    @Override
    public CartItem addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
     
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
 
        String res = (String) cartOps.get(skuId.toString());
        // 1、添加新商品到购物车(购物车无此商品) 
        if (StringUtils.isEmpty(res)){
     
            CartItem cartItem = new CartItem();
            
            /**
             * 异步查询
             */
            CompletableFuture<Void> getSkuInfo = CompletableFuture.runAsync(() -> {
     
                //1.1、远程查询要添加的商品信息
                R skuInfo = productFeignService.getSkuInfo(skuId);
                SkuInfoVo data = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
     
                });
 
                cartItem.setCheck(true);
                cartItem.setCount(1);
                cartItem.setImage(data.getSkuDefaultImg());
                cartItem.setTitle(data.getSkuTitle());
                cartItem.setSkuId(skuId);
                cartItem.setPrice(data.getPrice());
            },executor);
            
            CompletableFuture<Void> getSkuSaleAttrValues = CompletableFuture.runAsync(() -> {
     
                //1.2、远程查询sku的组合信息
                List<String> values = productFeignService.getSkuSaleAttrValues(skuId);
                cartItem.setSkuAttr(values);
            }, executor);
            
            CompletableFuture.allOf(getSkuInfo,getSkuSaleAttrValues).get();
            String jsonString = JSON.toJSONString(cartItem);
            cartOps.put(skuId.toString(),jsonString);
            return cartItem;
        }else {
     
            //2、购物车有此商品,将数据取出修改数量即可
            CartItem cartItem = JSON.parseObject(res, CartItem.class);
            cartItem.setCount(cartItem.getCount() + num);
            cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
            return cartItem;
        }
    }
 
    /**
     * 获取我们要操作的购物车,临时购物车、用户购物车
     * @return
     */
    private BoundHashOperations<String, Object, Object> getCartOps() {
     
        //得到用户信息 账号用户 、临时用户
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        //1、userInfoTo.getUserId()不为空表示账号用户,反之临时用户  然后决定用临时购物车还是用户购物车
        //放入缓存的key
        String cartKey = "";
        if (userInfoTo.getUserId() != null){
     
            cartKey = CART_PREFIX + userInfoTo.getUserId();
        }else {
     
            cartKey = CART_PREFIX + userInfoTo.getUserKey();
        }
        BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
        return operations;
    }


	//getCartItem()是完成上面的添加商品到购物车后从redis中获取商品信息
    @Override
    public CartItemVo getCartItem(Long skuId) {
     
        //拿到要操作的购物车信息
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();

        String redisValue = (String) cartOps.get(skuId.toString());

        CartItemVo cartItemVo = JSON.parseObject(redisValue, CartItemVo.class);

        return cartItemVo;
    }
}

存在的小问题

day02_《谷粒商城》的完整流程(详细版一)_第70张图片

修改gulimall-cart的Controller:

    /**
     * 添加商品到购物车
     */
    @GetMapping("/addToCart")
    public String addToCart(@RequestParam("skuId") Long skuId,
                            @RequestParam("num") Integer num,
                            RedirectAttributes attributes) throws ExecutionException, InterruptedException {
     
        cartService.addToCart(skuId,num);
        attributes.addAttribute("skuId",skuId);
 
        return "redirect:http://cart.gulimall.com/addToCartSuccess.html";
    }
 
    /**
     * 跳转到成功页
     */
    @GetMapping("/addToCartSuccess.html")
    public String addToCartSuccessPage(@RequestParam("skuId") Long skuId,Model model){
     
        CartItem cartItem = cartService.getCartItem(skuId);//调用getCartItem()方法获取商品信息
        model.addAttribute("item",cartItem);
        return "success";
    }

3.测试:

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

P244 获取购物车

1.总说:

若用户未登录,则直接使用user-key获取购物车数据;否则使用userId获取购物车数据,并将user-key对应临时购物车数据与用户购物车数据合并,并删除临时购物车

2.代码:

Controller

    @GetMapping("/cart.html")
    public String cartListPage(Model model) throws ExecutionException, InterruptedException {
     
 
        //快速得到用户信息,id,user-key
//        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        Cart cart = cartService.getCart();
        model.addAttribute("cart",cart);
        return "cartList";
    }

Service:

@Override
    public Cart getCart() throws ExecutionException, InterruptedException {
     
        Cart cart = new Cart();
        //1、登录
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        if (userInfoTo.getUserId() != null){
     
            String cartKey = CART_PREFIX + userInfoTo.getUserId();
 
            //1.1、如果临时购物车的数据还没有合并【合并购物车】
            String tempCartKey = CART_PREFIX + userInfoTo.getUserKey();
            List<CartItem> tempsCartItems = getCartItems(tempCartKey);
            if (tempsCartItems != null){
     
                //临时购物车有数据,需要合并
                for (CartItem item : tempsCartItems) {
     
                    addToCart(item.getSkuId(),item.getCount());
                }
                //清除临时购物车的数据
                clearCart(tempCartKey);
            }
 
            //1.2、获取登录后的购物车数据【包含合并过来的临时购物车的数据,和登录后的购物车数据 】
            List<CartItem> cartItems = getCartItems(cartKey);
            cart.setItems(cartItems);
        }else {
     
            //2、没登录
            String cartKey = CART_PREFIX + userInfoTo.getUserKey();
            //获取临时购物车的所有购物项
            List<CartItem> cartItems = getCartItems(cartKey);
            cart.setItems(cartItems);
        }
        return cart;
    }
     * 获取购物车里面的数据
    private List<CartItemVo> getCartItems(String cartKey) {
     
        //获取购物车里面的所有商品
        BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
        List<Object> values = operations.values();
        if (values != null && values.size() > 0) {
     
            List<CartItemVo> cartItemVoStream = values.stream().map((obj) -> {
     
                String str = (String) obj;
                CartItemVo cartItem = JSON.parseObject(str, CartItemVo.class);
                return cartItem;
            }).collect(Collectors.toList());
            return cartItemVoStream;
        }
        return null;

    }
    @Override
    public void clearCart(String cartKey) {
     
        redisTemplate.delete(cartKey);
    }

3.测试

在这里插入图片描述
day02_《谷粒商城》的完整流程(详细版一)_第74张图片
day02_《谷粒商城》的完整流程(详细版一)_第75张图片
day02_《谷粒商城》的完整流程(详细版一)_第76张图片

P245 选中购物车项

在这里插入图片描述

Controller

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

Service:

    @Override
    public void checkItem(Long skuId, Integer check) {
     
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        CartItem cartItem = getCartItem(skuId);
        cartItem.setCheck(check==1 ? true : false);
        String jsonString = JSON.toJSONString(cartItem);
        cartOps.put(skuId.toString(),jsonString);
    }
 

    private BoundHashOperations<String, Object, Object> getCartOps() {
     
        //得到用户信息 账号用户 、临时用户
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        //1、userInfoTo.getUserId()不为空表示账号用户,反之临时用户  然后决定用临时购物车还是用户购物车
        //放入缓存的key
        String cartKey = "";
        if (userInfoTo.getUserId() != null){
     
            cartKey = CART_PREFIX + userInfoTo.getUserId();
        }else {
     
            cartKey = CART_PREFIX + userInfoTo.getUserKey();
        }
        BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
        return operations;
    }


    @Override
    public CartItemVo getCartItem(Long skuId) {
     
        //拿到要操作的购物车信息
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();

        String redisValue = (String) cartOps.get(skuId.toString());

        CartItemVo cartItemVo = JSON.parseObject(redisValue, CartItemVo.class);

        return cartItemVo;
    }

P246 修改购物项数量

day02_《谷粒商城》的完整流程(详细版一)_第77张图片

Controller:

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

Service:

    @Override
    public void changeItemCount(Long skuId, Integer num) {
     
        CartItem cartItem = getCartItem(skuId);
        cartItem.setCount(num);
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
    }

P247 删除购物项

在这里插入图片描述

Controller:

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

Service:

     @Override
    public void deleteItem(Long skuId) {
     
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        cartOps.delete(skuId.toString());
    }

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