谷粒商城-高级篇-aiueo

105 初步检索 105.1 _cat GET /_cat/nodes : 查看所有节点 GET /_cat/health : 查看es健康状况 GET /_cat/master : 查看主节点 GET /_cat/indices: 查看所有索引 107 乐观锁字段 _seq_no : 并发控制字段 , 每次更新就会 + 1 , 用来做乐观锁 _primary_term : 同上

105 初步检索

105.1 _cat

GET /_cat/nodes : 查看所有节点
GET /_cat/health : 查看es健康状况 
GET /_cat/master : 查看主节点
GET /_cat/indices: 查看所有索引

107 乐观锁字段

_seq_no :并发控制字段 , 每次更新就会 + 1 , 用来做乐观锁

_primary_term : 同上,主分片重新分配 , 如重启 , 就会变化.

107.1 并发修改

发送两次修改同时对同一文档进行修改 , 为了控制并发就可以加上if_seq_no=1&if_primary_term=1 就可以修改

PUT  http://xxx.xxx.xxx.xxx/index/type/1?if_seq_no=1&if_primary_term=1
{
 "name":1
}

PUT  http://xxx.xxx.xxx.xxx/index/type/1?if_seq_no=1&if_primary_term=1
{
 "name":1
}

113.match_phrase短语匹配

将需要匹配的值当成一个整体单词,(不分词)进行检索

GET bank_search
{
	"query";{
	"match_phrase":{
	"address":"mill road"	
	}
  }
}

128.sku在es中存储模型分析

es DSL

PUT product
{ 
    "mappings": { 
        "properties": { 
            "skuId": { 
                "type": "long" 
            },
            "spuId": { 
                "type": "keyword" 
            },
            "skuTitle": { 
                "type": "text", 
                "analyzer": "ik_smart" 
            },
            "skuPrice": { 
                "type": "keyword" 
            },
            "skuImg": { 
                "type": "keyword",
			   "index": false, 
                "doc_values": false #不可聚合等操作
            },
            "saleCount": { 
                "type": "long" 
            },
            "hasStock": { 
                "type": "boolean" 
            },
            "hotScore": {
                "type": "long" 
            },
            "brandId": { 
                "type": "long" 
            },
            "catalogId": { 
                "type": "long" 
            },
            "brandName": { 
                "type": "keyword", 
                "index": false, 
                "doc_values": false 
            },
            "brandImg": { 
                "type": "keyword", 
                "index": false, 
                "doc_values": false 
            },
            "catalogName": { 
                "type": "keyword", 
                "index": false,
                "doc_values": false 
            },"attrs": { 
                "type": "nested", #数组扁平化处理
                "properties": { 
                    "attrId": { 
                        "type": "long" 
                    },
                    "attrName": { 
                        "type": "keyword", 
                        "index": false, 
                        "doc_values": false 
                    },
				   "attrValue": { 
                       "type": "keyword" 
                   } 
                } 
            } 
        } 
    }
}

冗余存储的字段不需要参与检索 , 所以"type"和"doc_value"设置成false

132.R-泛型结果封装

132.1 从List中获取值

如果List里面封装的不是基本类型 , 并且引用类型(对象)里面有非重字段(对应数据库中entity的主键id),如果想要从里面快速的获取数据可以封装成Map.

对象

@Data
public class SkuHasStockVo {
    private Long skuId; #主键Id 唯一
    private Boolean hasStock;
}

将对象集合封装成Map

 try {
            R skuHasStock = wareFeignService.getSkuHasStock(skuIdList);

            List<SkuHasStockVo> data = (List<SkuHasStockVo>) skuHasStock.get("data");

             stockMap = data.stream().collect(Collectors.toMap(
                    skuHasStockVo -> skuHasStockVo.getSkuId(), //key
                    skuHasStockVo -> skuHasStockVo.getHasStock()//value
            ));
        }catch (Exception e){
            log.error("库存服务查询异常:原因{}",e);
        }

在一层循环中,就不必在遍历对象集合,即根据主键Id,查找自己封装的Map

 List<SkuEsModel> upProduct = skus.stream().map(sku -> {
		
            //......
            //TODO : 库存
            if(finalStockMap == null){
                esModel.setHasStock(true);
            }else{
                esModel.setHasStock(finalStockMap.get(sku.getSkuId())); //根据自己封装的Map找到对应的Value,而不是循环遍历查找
            }
            return esModel;
        }).collect(Collectors.toList());

132.2 远程调用的失败Catch

远程调用可能存在失败,需要自己手动处理远程调用失败的情况

   Map<Long, Boolean> stockMap = null;
        try {
            R skuHasStock = wareFeignService.getSkuHasStock(skuIdList);

            List<SkuHasStockVo> data = (List<SkuHasStockVo>) skuHasStock.get("data");

             stockMap = data.stream().collect(Collectors.toMap(
                    skuHasStockVo -> skuHasStockVo.getSkuId(),
                    skuHasStockVo -> skuHasStockVo.getHasStock()
            ));
        }catch (Exception e){
            log.error("库存服务查询异常:原因{}",e);
        }

135.TypeReference

对于远程调用返回R对象想要得到具体的类型不用强转,利用fastjson的TypeReferece

R

	//利用fastjson进行逆转
	public <T> T getData(TypeReference<T> typeReference){
		Object data = this.get("data"); //默认是map
		String s = JSON.toJSONString(data);
		T t = JSON.parseObject(s, (Type) typeReference);
		return t;
	}

从R中获取具体的类型

  R r = wareFeignService.getSkuHasStock(skuIdList);

            TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>() {};

             stockMap = r.getData(typeReference).stream().collect(Collectors.toMap(
                    skuHasStockVo -> skuHasStockVo.getSkuId(),
                    skuHasStockVo -> skuHasStockVo.getHasStock()
            ));

137.渲染一级分类数据

137.1 thymeleaf 名称空间

<html lang="en" xmlns:th="http://www.thrmeleaf.org">

137.2 快速编译页面

快捷键 : ctrl+shift+f9

139.搭建域名访问环境一

server {
    listen       80;
    server_name  gulimall.com;

  
    location / {
        proxy_pass   http://192.168.31.57:7000; #window本机内网地址
    }
}

140.搭建域名访问环境二-负载均衡到网关

140.1 nginx conf

http块

 upstream gulimall {
      server 192.168.31.57:88;
    }

server块

server {
    listen       80;
    server_name  gulimall.com;
    
    location / {
        proxy_pass   http://guilimall;
    }
}

140.2 gateway-applicatoin.yml

一定要放在所有路由的最后面

 - id: gulimall-host-route #renrenfast 路由
            uri: lb://product-service
            predicates:
              - Host=**.gulimall.com,gulimall.com

140.3 nginx代理给网关的问题

nginx代理给网关的时候,会丢失请求的host信息.

server {
    listen       80;
    server_name  gulimall.com;
    
    location / {
        proxy_set_header Host $host; #设置上host头
        proxy_pass   http://guilimall;
    }
}

145.JvisualVM

启动 :

cmd -> jvisualvm

145.1线程

运行 : 正在运行的

休眠 : sleep

等待 : wait

驻留 : 线程池里面的空闲线程

监视 : 阻塞的线程,正在等待锁

146 中间件对性能的影响

146.1 docker 监控容器状态

docker stats

150.优化三级分类数据获取

150.1 Version Origin

不断的嵌套与数据库交互,导致IO开销太大,频繁的网络交互导致接口的性能非常差劲!

 @Override
    public Map<Long, List<Catelog2Vo>> getCatalogJson() {
        
        //1.查出所有1级分类
        List<CategoryEntity> level1Categorys = this.getLevel1Categorys();

        //2.封装数据
        Map<Long, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(
                k -> k.getCatId(),
                v -> {
                    List<CategoryEntity> categoryEntities
                            = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
                    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());
                            //找当前二级分类的三级分类封装成vo
                            List<CategoryEntity> level3Catelog =
                                    baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", l2.getCatId()));
                            if(level3Catelog != null){
                                List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                                    //2.封装成指定格式
                                    Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(),l3.getCatId().toString(),l3.getName());
                                    return catelog3Vo;
                                }).collect(Collectors.toList());
                                catelog2Vo.setCatalog3List(collect);
                            }
                            return catelog2Vo;
                        }).collect(Collectors.toList());
                    }
                    return catelog2Vos;
                }
        ));


        return parent_cid;
    }

150.2 Version X

将数据库的多次查询变为1次 , 如果再有根据父Id查找子分类的需求 , 直接在本地的集合中去找.

   @Override
    public Map<Long, List<Catelog2Vo>> getCatalogJson() {

        List<CategoryEntity> selectList = baseMapper.selectList(null);

        //1.查出所有1级分类
        List<CategoryEntity> level1Categorys = selectList.stream().filter(categoryEntity -> categoryEntity.getParentCid() == 0).collect(Collectors.toList());

        //2.封装数据
        Map<Long, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(
                k -> k.getCatId(),
                v -> {
                    List<CategoryEntity> categoryEntities
                            = findSonCategory(selectList,v.getCatId());
                    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());
                            //找当前二级分类的三级分类封装成vo
                            List<CategoryEntity> level3Catelog =
                                    findSonCategory(selectList,l2.getCatId());
                            if(level3Catelog != null){
                                List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                                    //2.封装成指定格式
                                    Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(),l3.getCatId().toString(),l3.getName());
                                    return catelog3Vo;
                                }).collect(Collectors.toList());
                                catelog2Vo.setCatalog3List(collect);
                            }
                            return catelog2Vo;
                        }).collect(Collectors.toList());
                    }
                    return catelog2Vos;
                }
        ));

        return parent_cid;
    }

156 .锁-解决缓存击穿问题

让高并发情况下,一个服务实例只访问一次数据库.

156.1 version origin

getCatalogJSONFromDB

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

        synchronized (this){
            //得到锁以后,我们应该再去缓存中确定一次,如果没有才需要继续查询
            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("查询了数据库......");
            //查db
            List<CategoryEntity> selectList = baseMapper.selectList(null);

            //1.查出所有1级分类
            List<CategoryEntity> level1Categorys = selectList.stream().filter(categoryEntity -> categoryEntity.getParentCid() == 0).collect(Collectors.toList());

            //2.封装数据
            Map<String , List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(
                    k -> k.getCatId().toString(),
                    v -> {
                        List<CategoryEntity> categoryEntities
                                = findSonCategory(selectList,v.getCatId());
                        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());
                                //找当前二级分类的三级分类封装成vo
                                List<CategoryEntity> level3Catelog =
                                        findSonCategory(selectList,l2.getCatId());
                                if(level3Catelog != null){
                                    List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                                        //2.封装成指定格式
                                        Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(),l3.getCatId().toString(),l3.getName());
                                        return catelog3Vo;
                                    }).collect(Collectors.toList());
                                    catelog2Vo.setCatalog3List(collect);
                                }
                                return catelog2Vo;
                            }).collect(Collectors.toList());
                        }
                        return catelog2Vos;
                    }
            ));
            
            return parent_cid;
        }
    }

@Override

getCatalogJSON

@Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {

        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");

        if(StringUtils.isEmpty(catalogJSON)){
            //缓存中没有,查询数据库
            System.out.println("缓存不命中.....查询数据库");
            Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
            //查到的数据再放入缓存,将对象转为json放在缓存中
            String s = JSON.toJSONString(catalogJsonFromDb);
            redisTemplate.opsForValue().set("catalogJSON", s,1, TimeUnit.DAYS);

            return catalogJsonFromDb;
        }
        System.out.println("缓存命中.....直接返回");
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {});
        return result;
    }

156.2 version X

将老三样三个动作原子性放入同步代码块中.

将数据放入缓存的操作放入同步代码块

 //查到的数据再放入缓存,将对象转为json放在缓存中
String s = JSON.toJSONString(catalogJsonFromDb);
redisTemplate.opsForValue().set("catalogJSON", s,1, TimeUnit.DAYS);

getCatalogJSONFromDb

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

        synchronized (this){
            //得到锁以后,我们应该再去缓存中确定一次,如果没有才需要继续查询
            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("查询了数据库......");
            //查db
            List<CategoryEntity> selectList = baseMapper.selectList(null);

            //1.查出所有1级分类
            List<CategoryEntity> level1Categorys = selectList.stream().filter(categoryEntity -> categoryEntity.getParentCid() == 0).collect(Collectors.toList());

            //2.封装数据
            Map<String , List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(
                    k -> k.getCatId().toString(),
                    v -> {
                        List<CategoryEntity> categoryEntities
                                = findSonCategory(selectList,v.getCatId());
                        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());
                                //找当前二级分类的三级分类封装成vo
                                List<CategoryEntity> level3Catelog =
                                        findSonCategory(selectList,l2.getCatId());
                                if(level3Catelog != null){
                                    List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                                        //2.封装成指定格式
                                        Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(),l3.getCatId().toString(),l3.getName());
                                        return catelog3Vo;
                                    }).collect(Collectors.toList());
                                    catelog2Vo.setCatalog3List(collect);
                                }
                                return catelog2Vo;
                            }).collect(Collectors.toList());
                        }
                        return catelog2Vos;
                    }
            ));

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

@Override

getCatalogJSON

@Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {

        String catalogJSON = redisTemplate.opsForValue().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;
    }

158.分布式锁原理以及使用

总结 : 加锁(setnx , set expire time)的时候保证步骤是原子化的,解锁(根据key查看锁,删除锁)的时候保证步骤是原子化的

利用Lua脚本保证,查看锁和删除锁的动作是原子性的

 /**
     * 获取二三级分类 , 从数据库中查找 , 并封装分类数据 , 用redis分布式锁
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {

            //1.占分布式锁 , 去redis占坑
            String uuid = UUID.randomUUID().toString();
            Boolean lock = redisTemplate.opsForValue().setIfAbsent("local", uuid,300,TimeUnit.SECONDS);

            if(lock == true){
                //lua 脚本解锁
                Map<String, List<Catelog2Vo>> dataFromDb;
                try {
                    dataFromDb = getDataFromDb();
                }finally {
                    String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return  redis.call('del', KEYS[1]) else return 0 end";
                    redisTemplate.execute(new DefaultRedisScript<Long>(luaScript, Long.class), Arrays.asList("lock"), uuid);
                }

            return dataFromDb;
            }else {
                //加锁失败 ... 重试
                try {
                    TimeUnit.MILLISECONDS.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return getCatalogJsonFromDbWithRedisLock(); //自旋的方式
            }
    }

159.分布式锁 Redission

@GetMapping("/hello")
    public void hello(){
        //1.获取一把锁,只要锁的名字一样,就是同一把锁
        RLock lock = redisson.getLock("my-lock");

        //2.加锁,阻塞式等待,只要没拿到,就等待锁
        lock.lock();
        try {
            System.out.println("加锁成功,执行业务=>"+Thread.currentThread().getId());
            TimeUnit.SECONDS.sleep(30);
        }catch (Exception e){

        }finally {
            //3.解锁
            System.out.println("解锁=>"+Thread.currentThread().getId());
            lock.unlock();
   }

Redission自己封装的分布式锁框架,解决了一些问题

锁的自动续期,如果业务执行时间超长,运行期间自动给锁续上新的30s,不用担心由于业务执行时间长,从而导致锁自动过期删除的问题

加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s(默认)之后自动删除

161.WatchDog

161.1 lock.lock(20,TimeUnit.SECONDS);

手动设置锁的过期时间之后,不会自动续期,所以自动解锁的时间设置一定要大于业务的执行时间.

161.2 最佳实践

明显的设置锁的过期时间,省掉了整个续期操作,手动解锁.

162.读写锁

162.1 写锁

@GetMapping("/write")
    public String writeValue(){

        String s = "";
        RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
        RLock wlock = lock.writeLock();
        wlock.lock();
        try {
            s = UUID.randomUUID().toString();
            TimeUnit.SECONDS.sleep(30);
            redisTemplate.opsForValue().set("writeValue", s);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            wlock.unlock();
        }
        return s;
    }

162.2 读锁

 @GetMapping("/read")
    public String readValue(){
        RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
        RLock rlock = lock.readLock();
        String s = "";
        rlock.lock();
        try {
            s = redisTemplate.opsForValue().get("writeValue");
        }finally {
            rlock.unlock();
        }
        return s;
    }

162.3 补充

只要有写锁的存在,都必须等待

163.Semaphore-信号量

bash

set park 3

acquire 占一个车位

@GetMapping("/park")
    public String park() throws InterruptedException {
        RSemaphore park = redisson.getSemaphore("park");
        park.acquire();// 获取一个信号量,获取一个值,占一个车位
        return "acquire ok";
  }

release 释放一个车位

  @GetMapping("/go")
    public String go(){
        RSemaphore park = redisson.getSemaphore("park");
        park.release();
        return "release ok";
  }

164.countDownLatch-闭锁

164.1 set

 @GetMapping("/lockDoor")
    public String lockDoor() throws InterruptedException {
        RCountDownLatch countDownLatch = redisson.getCountDownLatch("door");
        countDownLatch.trySetCount(5);
        countDownLatch.await();
        return "放假了.....";
    }

164.2 countDown

   @GetMapping("/gogogo/{id}")
    public String gogogo(@PathVariable("id") Long id){
        RCountDownLatch countDownLatch = redisson.getCountDownLatch("door");
        countDownLatch.countDown();
        return id+"班的人都走了";
  }

166.缓存一致性

166.1 利用Redision加锁

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

        RLock lock = redisson.getLock("catalogJson-lock");
        lock.lock();
        Map<String, List<Catelog2Vo>> dataFromDb;
            try {
                dataFromDb = getDataFromDb();
            }finally {
                lock.unlock();
            }
            return dataFromDb;
    }

166.2 缓存一致性-双写模式

由于卡顿等原因,导致写缓存2在最前,写缓存1在后面就出现了不一致

这是暂时性的脏数据问题,但是在数据稳定,缓存过期以后,又能得到最新的正确数据

166.3 缓存一致性-失效模式

 

166.4 缓存一致性-解决方案

我们系统的一致性解决方案 :

缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新

对于不经常写的数据,读写数据的时候,加上分布式的读写锁

168. 整合SpringCache

168.1 依赖

 <!--springCache-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>


   <!--redis template-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

168.2 配置

 spring:
 	cache:
       type: redis

168.3 启动

@EnableCaching

168.4 hello-springCache

测试使用缓存

/**
     * 获取一级分类
     * @return
     */
    @Cacheable({"category"})
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        List<CategoryEntity> categoryEntities = baseMapper.selectList(
                new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        return categoryEntities;
    }

169 注解

169.1 @Cacheable

@Cacheable : 触发将数据保存到缓存的操作,如果缓存中有,则方法不用调用.如果缓存中没有,会调用方法,最后将方法的结果放入缓存.每一个缓存的数据我们都来指定要放到哪个名字的缓存[缓存的分区(按照业务类型分),便于管理]

169.1.1 默认行为 :

① 如果缓存中有,方法不会调用

② key默认自动生成 =>缓存的名字 :: SimpleKey[] (自主生成的key值)

③ 缓存的value值,默认使用jdk序列化机制,将序列化的数据存入Redis

④ 默认ttl时间 : -1 (永不过期)

169.1.2 自定义配置

① 指定生成的缓存使用key : key属性指定,接受一个SpEL表达式

/**
     * 获取一级分类
     * @return
     */
    @Cacheable(cacheNames = {"category"} , key = "'level1Categorys'")
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        List<CategoryEntity> categoryEntities = baseMapper.selectList(
                new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        return categoryEntities;
    }

@Cacheable(cacheNames = {"category"} , key = "#root.method.name")

② 指定缓存的数据的存活时间 , 在配置文件中配置

spring:
	cache:
    	type: redis
    	redis:
      		time-to-live: 3600000

③.① 将数据保存为json格式 , version Orign 会使配置文件中的配置失效


@Configuration
@EnableCaching
public class MyCacheConfig {

    /**
     * 配置文件会失效
     * @return
     */
    @Bean
    RedisCacheConfiguration redisCacheConfiguration(){

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();

        config = config.serializeKeysWith(
                RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));

        config = config.serializeValuesWith(
                RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
        return config;

    }
}

③.② 注入CacheProperties.class , 让配置文件配置项生效

@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyCacheConfig {

    @Autowired
    private CacheProperties cacheProperties;

    /**
     * 配置文件会失效
     * @return
     */
    @Bean
    RedisCacheConfiguration redisCacheConfiguration(){

        CacheProperties.Redis redisProperties = cacheProperties.getRedis();

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();

        config = config.serializeKeysWith(
                RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));

        config = config.serializeValuesWith(
                RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }

        if (redisProperties.getKeyPrefix() != null) {
            config = config.prefixKeysWith(redisProperties.getKeyPrefix());
        }

        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }

        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }

        return config;

    }
}

169.1.3 其他配置

spring:
 cache:
    type: redis
    redis:
      time-to-live: 3600000
      key-prefix: CHCHE_ #前缀
      use-key-prefix: true #是否使用前缀
      cache-null-values: true #是否缓存空值,应对缓存穿透

169.2 @CacheEvict

@CacheEvict : 触发将数据从缓存删除的操作.

@Caching 组合以上多个操作.

@CacheEvict(cacheNames = {"category"},allEntries = true) //删除某个分区下的所有数据

  @Caching(evict = {
            @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
            @CacheEvict(value = "category",key = "'getCatalogJson'")
    })

 /**
     * 计量更新所有的关联数据
     * @param category
     */
    //@CacheEvict(cacheNames = {"category"},allEntries = true)
    @Caching(evict = {
            @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
            @CacheEvict(value = "category",key = "'getCatalogJson'")
    })
    @Override
    @Transactional
    public void updateCascade(CategoryEntity category) {
        categoryDao.updateById(category);
        categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
    }

@CachePut : 不影响方法执行更新缓存. 用于双写模式

@CacheConfig : 在类级别共享缓存的相同配置.

172.SpringCache-原理与不足

读模式

        缓存穿透 : 查询一个Null数据=>解决:缓存空数据;ache-null-values=true

        缓存击穿 : 大量并发进来查询一个正好过期的数据.解决:加锁:? 

        缓存雪崩 : 大量key同时过期.解决:加随机时间.加上过期时间 : spring.cache.redis.time-to-          live

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

        读写加锁

        引入Cannl,感知Mysql的更新去更新数据库 

        读多写多,直接去数据库查询即可

172.1 关于数据加分布式锁问题

SpringCache默认是无加锁的.所以要加锁有两种解决办法.

第一种是手写缓存逻辑.

第二种是在注解上加上配置,但是实现的是本地锁

sync = true

@Cacheable(cacheNames = {"category"} , key = "#root.method.name",sync = true)
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        List<CategoryEntity> categoryEntities = baseMapper.selectList(
                new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        return categoryEntities;
    }

172.总结

常规数据(读多写少,即时性,一致性要求不高的数据);完全可以使用Spring-Cache,写模式只要缓存的数据有过期时间即可.

特殊业务.特殊设计

178.检索DSL测试-聚合测试

178.1 数据迁移

POST _reindex 
{
	"source":{
	  "index":"twitter"
	},
	"dest":{
		"index":"new_twitter"
	}
}

178.2 商品数据新映射

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

182.ES-Response封装

182.1 关于聚合类型

  //得到品牌的名字
String brandName = ((ParsedStringTerms) bucket.getAggregations().get("brand_name_agg")).getBuckets().get(0).getKeyAsString();

bucket获取聚合时,返回的类型如何判断?

通过Debug模式分析 es 返回的Response 从而获得 聚合类型

182.2 代码

 /**
     * 封装返回结果
     * @param response
     * @return
     */
    private SearchResult buildSearchResult(SearchResponse response,SearchParam searchParam) {

        SearchResult result = new SearchResult();
        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(searchParam.getKeyword())){
                    HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
                    String highLightField = skuTitle.getFragments()[0].string();
                    esModel.setSkuTitle(highLightField);
                }
                esModels.add(esModel);
            }
        }
        result.setProducts(esModels);

        /**
         * 分类聚合
         */
        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);

        /**
         * 品牌聚合
         */
        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();
            //得到品牌的id
            long brandId = bucket.getKeyAsNumber().longValue();
            //得到品牌的名字
            String brandName = ((ParsedStringTerms) bucket.getAggregations().get("brand_name_agg")).getBuckets().get(0).getKeyAsString();
            //得到品牌的图片
            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);

        /**
         * 属性聚合
         */
        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();
            //得到属性的id
            long attrId = bucket.getKeyAsNumber().longValue();
            //得到属性的名字
            String attrName = ((ParsedStringTerms) bucket.getAggregations().get("attr_name_agg")).getBuckets().get(0).getKeyAsString();
            //得到属性的所有值
            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.setAttrValue(attrValues);
            attrVo.setAttrName(attrName);
            attrVos.add(attrVo);
        }
        result.setAttrs(attrVos);


        //分页信息 - 当前页码
        result.setPageNum(searchParam.getPageNum());
        //分页信息 - 总记录数
        long total = hits.getTotalHits().value;
        result.setTotal(total);
        //分页信息 - 总页码
        int totalPages = (int)total%ESConstant.PRODUCT_PAGE_SIZE == 0 ?
                (int)total/ESConstant.PRODUCT_PAGE_SIZE :
                ((int)total/ESConstant.PRODUCT_PAGE_SIZE + 1);
        result.setTotalPages(totalPages);
        return result;
    }

194.线程池

194.1 参数

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

threadFactory : 线程池的创建工厂 

handler : 如果队列满了 , 按照我们指定的拒绝策略执行任务

194.2 工作顺序

线程池创建 , 准备好core数量的核心线程,准备接受任务

core满了 , 就将再进来的任务放入阻塞队列中 . 空闲的core就会自己去阻塞队列获取任务执行

阻塞队列满了 , 就直接开启新线程执行 , 最大只能开到max指定的数量

max满了就用RejectedExecutionHandler拒绝任务

max 都执行完成 , 有很多空闲时间 , 在指定的时间keepAliveTime以后 , 释放max-core这些线程

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5,
                200,
                10,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(100000), //默认为Integer的最大值 , 太大了 , 根据项目和服务器的内存大小设置
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()); //默认抛弃

194.3 举列子

一个线程池
{
    "corePoolSize" : 7,
    "maximumPoolSize" : 20 ,
    "LinkedBlockingQueue" : 50
}

请问 : 100 并发进来线程池是怎么分配任务的?

进来7个核心线程池执行任务 , 50个任务占满阻塞队列 , 开启再13个线程执行任务

194.4 其他线程池

194.1.1 CachedThreadPool()

core是0,所有都可以回收

Executors.newCachedThreadPool();

 public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

194.1.2 FixedThreadPool(int core)

固定大小, core == max ; 都不可回收 , 全部是核心线程

Executors.newFixedThreadPool(10);

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

194.1.3ScheduledThreadPool(int core)

定时任务的线程池

Executors.newScheduledThreadPool(10);

 public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

194.1.4 SingleThreadExecutor();

单线程线程池,后台从队列中取,逐个执行

Executors.newSingleThreadExecutor();

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

195 Hello-CompletableFuture

195.1 异步无返回值 runAsync

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            System.out.println("执行业务");
        }, executor);

195.2 异步有返回值 supplyAsync

 CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("执行业务");
            return 1;
        }, executor);

        //阻塞获取返回值
        System.out.println(future.get());

197 CompletableFuture-回调与异常感知

197.1 回调

.whenComplete(); //在上一个任务完成之后执行,与上一个任务在同一个线程
.whenCompleteAsync(); //在上一个任务完成之后执行,在线程池中开启新的线程

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("执行业务");
            return 1;
        }, executor).whenComplete((r,e) -> {
            System.out.println("异步任务完成了...结果是: " + r);
            System.out.println("异步任务完成了...异常是: " + e);
        });

197.2 异常 exceptionally

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("执行业务");
            return 1;
        }, executor).whenComplete((r,e) -> {
            System.out.println("异步任务完成了...结果是: " + r);
            System.out.println("异步任务完成了...异常是: " + e);
        }).exceptionally(throwable -> {
            //可以感知异常,并处理异常的返回(catch)
            return 10;
        });
        //阻塞获取返回值
        future.get();

198.CompletableFuture-handle最终处理

whenCompletable 虽然能感知结果并且获取异常 , 但是无法修改救过 , 所以有需求的时候利用handle方法进行异步调用的结果修改

 CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("执行业务");
            return 1;
        }, executor).handle((r,t) ->{
            if(r != null){
                return  r * 2;
            }
            if(t != null){
                return 0;
            }
            return 0; 
        });
        //阻塞获取返回值
        future.get();

199.CompletableFuture-线程串行化

thenApply方法 : 当一个线程依赖另一个线程时,获取上一个任务返回的结果 , 并返回当前任务的返回值

thenAccept : 消费处理结果.接收任务的处理结果,并消费处理,无返回结果

thenRun方法 : 只要上面的任务执行完成,就开始执行thenRun,只是处理完任务后 , 执行thenRun的后续操作

带有Async默认是异步执行的,就是在上一个任务基础上开一个新线程

200.CompletableFuture-两任务组合-都要完成 &&

runAfterBoth : 组合两个future,不需要获取future的结果 , 只需要两个future处理完任务后,处理该任务

 thenAcceptBoth : 组合两个future任务的返回结果,然后处理任务,没有返回值

 runAfterBoth : 组合两个future ,获取future的结果 , 并返回当前任务的返回值

200.1 代码

//future01
        CompletableFuture<Integer> future01 = CompletableFuture.supplyAsync(() -> {
            System.out.println("任务1线程" + Thread.currentThread().getId());
            int i = 10 / 4;
            System.out.println("任务1结束");
            return i;
        }, executor);
        //future02
        CompletableFuture<String> future02 = CompletableFuture.supplyAsync(() -> {
            System.out.println("任务2线程" + Thread.currentThread().getId());
            System.out.println("任务2结束");
            return "Hello";
        }, executor);

        //runAfterBothAsync
        future01.runAfterBothAsync(future02, () ->{
            System.out.println("任务1 和 任务2 执行完毕 不能感知结果");
        });

        //thenAcceptBothAsync
        future01.thenAcceptBothAsync(future02,(r1,r2) ->{
            System.out.println("任务1 和 任务2 执行完毕 感知结果 r1 =>" +r1 +","+" r2 => " + r2);
        },executor);

        //thenCombineAsync
        CompletableFuture<String> future = future01.thenCombineAsync(future02, (r1, r2) -> {
            System.out.println("任务1 和 任务2 执行完毕 感知结果,有返回值 r1 =>" + r1 + "," + " r2 => " + r2);
            return r1 + "," + " r2 => " + r2;
        }, executor);
        
        future.get();

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

当两个任务中 , 任意一个future任务完成的时候 , 执行任务.

runAfterEither : 两个任务有一个执行完成 , 不需要获取future的结果 , 处理任务 , 也没有返回值

 acceptEither : 两个任务有一个执行完成 , 获取它的返回值 , 处理任务 , 没有新的返回值

 applyToEither : 两个任务有一个执行完成 , 获取它的返回值 , 处理任务并有新的返回值

202 CompletableFuture-多任务组合

202.1 allOf 等待所有任务完成

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

        CompletableFuture<String> attrFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("查询商品的属性信息");
            return "hello.jpg";
        },executor);

        CompletableFuture<String> descFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("查询商品的描述信息");
            return "hello.jpg";
        },executor);

        CompletableFuture<Void> future = CompletableFuture.allOf(imgFuture, attrFuture, descFuture);
        future.get();
        System.out.println(imgFuture.get()+"|"+attrFuture.get()+"|"+descFuture.get());

202.2 anyOf 只要有一个任务完成

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

        CompletableFuture<String> attrFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("查询商品的属性信息");
            return "hello.jpg";
        },executor);

        CompletableFuture<String> descFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("查询商品的描述信息");
            return "hello.jpg";
        },executor);

        CompletableFuture<Void> future = CompletableFuture.allOf(imgFuture, attrFuture, descFuture);
        future.get();

205.mybatis内部类映射

 SpuItemAttrGroupVo为内部类,"<resultMap>"无法映射找不到!

206 sql-组连接

GROUP_CONCAT //组连接

206.1 例子

select GROUP_CONCAT(distinct name) from `pms_category` GROUP BY cat_level

211.认证

211.1 viewController

将请求和页面映射过来 , 无需在写controller

@Configuration
public class MyWebConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login.html").setViewName("login");
        registry.addViewController("/reg.html").setViewName("reg");
    }
}

相当于

@Controller
public class LoginController {
    @GetMapping("/login.html")
    public String login(){
        return "login";
    }
}


@Controller
public class RegController {
    @GetMapping("/reg.html")
    public String reg(){
        return "reg";
    }
}

214.接口防刷策略

214.1 再次校验

用户在接收到验证码之后,验证码在一定时间内有效

将手机号和验证码放入redis暂存

 redisTemplate.opsForValue().set(
                AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone , //sms:code:15941148736
                code,
                10,
                TimeUnit.MINUTES); //设置验证码10分钟内有效

214.2 防止快速获取

真正实现60秒内只能发送一次验证码 , 而不是可以通过刷新页面 , 让60秒重新停止计时.

将key加上当前系统时间A,当这个手机号再次进来的时候,获取当前系统时间B,对key进行截串获取系统时间A , 判断系统时间B-系统时间A 是否大于60秒

  String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
        if(!StringUtils.isEmpty(redisCode)){
            long l = Long.parseLong(redisCode.split("_")[1]);
            //接口防刷
            if(System.currentTimeMillis() - l < 60000){
                return R.error(BizCodeEnume.VAILD_SMS_CODE_EXCEPTION.getCode(),BizCodeEnume.VAILD_SMS_CODE_EXCEPTION.getMessage());
            }
        }

        String code = UUID.randomUUID().toString().substring(0, 5) + "_" +System.currentTimeMillis();

214.3 代码

@Controller
public class LoginController {

    @Autowired
    private ThirdPartyFeignClient thirdPartyFeignClient;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @GetMapping("/sms/sendCode")
    @ResponseBody
    public R sendCode(@RequestParam("phone") String phone){
        String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
        if(!StringUtils.isEmpty(redisCode)){
            long l = Long.parseLong(redisCode.split("_")[1]);
            //接口防刷
            if(System.currentTimeMillis() - l < 60000){
                return R.error(BizCodeEnume.VAILD_SMS_CODE_EXCEPTION.getCode(),BizCodeEnume.VAILD_SMS_CODE_EXCEPTION.getMessage());
            }
        }

        String code = UUID.randomUUID().toString().substring(0, 5) + "_" +System.currentTimeMillis();
        System.err.println(code);
        redisTemplate.opsForValue().set(
                AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone , //sms:code:15941148736
                code,
                10,
                TimeUnit.MINUTES); //设置验证码10分钟内有效


        thirdPartyFeignClient.sendCode(phone,code);
        return R.ok();
    }

}

215 注册

215.1 转发下的405

405 : 请求方式不正确

 @PostMapping("/regist")
    public String regist(@Valid UserRegisterVo vo , BindingResult result , Model model){
        //...
        return "forward:/reg.html"; //页面跳转都是get请求
    }

请求 regist 的方式为 Post 方式 , 转发是将请求原封不动进行传递 , 而页面跳转是 get 请求 , 所以会 405

解决方案

将页面跳转改为页面渲染

@PostMapping("/regist")
public String regist(@Valid UserRegisterVo vo , BindingResult result , Model model){
    //...
    return "reg"; //页面跳转都是get请求
}

215.2 重复提交

重复提交

return "reg" //转发,刷新页面相当于重复提交

解决:使用重定向的方式

return "redirect:/reg.html";

215.3 请求域

Model model 的作用范围无法作用于重定向的情况下

解决:使用RedirectAttribute的方式

RedirectAttributes  redirectAttributes 模拟重定向携带数据

@PostMapping("/regist")
    public String regist(@Valid UserRegisterVo vo , BindingResult result , RedirectAttributes  redirectAttributes){

        if(result.hasErrors()){
            //校验出错,转发到注册页

            Map<String, String> errorsMap = result.getFieldErrors().stream().collect(Collectors.toMap(
                    k -> k.getField(),
                    v -> v.getDefaultMessage()
            ));
            redirectAttributes.addFlashAttribute("errors",errorsMap);

            return "redirect:http://auth.gulimall.com/reg.html";
        }

        //注册 调用远程服务进行注册

        //注册成功回到首页 , 回到登录页
        return "redirect:/login.html";
    }

216.注册 - 异常机制

将用户名存在 和 手机号的存在 的情况以异常的形式返回给controller

216.1 service

/**
     * 检查邮箱是否唯一
     * @param email
     * @return
     */
    void checkPhoneUnique(String email) throws PhoneExistException;

    /**
     * 检查用户名是否唯一
     * @param userName
     * @return
     */
    void checkUsernameUnique(String userName) throws UserNameExistException;

216.2 serviceImpl

/**
     * 检查邮箱是否唯一
     * @param phone
     * @return
     */
    @Override
    public void checkPhoneUnique(String phone) throws PhoneExistException {
        Integer mobile = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
        if (mobile >= 1){
            throw new PhoneExistException();
        }
    }

    /**
     * 检查有户名是否唯一
     * @param userName
     * @return
     */
    @Override
    public void checkUsernameUnique(String userName) throws UserNameExistException {
        Integer username = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("username", userName));
        if(username >= 1){
            throw new UserNameExistException();
        }
    }

注册

/**
     * 注册
     * @param vo
     */
    @Override
    public void register(UserRegisterVo vo) {
        MemberEntity entity = new MemberEntity();

        //设置默认等级
        entity.setLevelId(1L);

        //检查用户名和手机号是否唯一
        checkPhoneUnique(vo.getPhone());
        checkUsernameUnique(vo.getUserName());

        entity.setMobile(vo.getPhone());
        entity.setUsername(vo.getUserName());

    }

216.3 controller

 /**
     * 注册
     */
     @PostMapping("/regist")
     public R register(@RequestBody UserRegisterVo vo){
         try {
             memberService.register(vo);
         }catch (PhoneExistException p){
             return R.error(1000,p.getMessage());
         }catch (UserNameExistException u){
             return R.error(1001,u.getMessage());
         }
         return R.ok();
     }

217 密码 明文 加盐 保存

217.1 MD5 盐值加密

MD5 信息摘要虽然是不可逆的 , 但是 明文 和 密文 存在严格的一对一函数关系 , 现有的破解网站就是基于暴力存储的检索的方式进行"破解"的. 比如"123456" 的 加密算法 "aaaaa".

所谓 , 加盐就是在原有明文的基础上 , 进行修改 , 让得出的明文更加具有不确定性.

218.2 Spring工具类

public class Test {
    public static void main(String[] args) {

        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

        String encode = passwordEncoder.encode("123456");
        System.err.println(encode);
        //$2a$10$ZVf5r/1uGlR1wtEWxwI0GOfewAPrCu3jkZvXKmeyrUBk2MlxUaf0G
        //$2a$10$pDoj2ZAprhZ.Wu2yAGBLD.ix3/oeijJRh8wuGi5ZZ9Q0urB8Kb/Ba

     passwordEncoder.matches("123456","$2a$10$ZVf5r/1uGlR1wtEWxwI0GOfewAPrCu3jkZvXKmeyrUBk2MlxUaf0G");
        //true
passwordEncoder.matches("123456", "$2a$10$pDoj2ZAprhZ.Wu2yAGBLD.ix3/oeijJRh8wuGi5ZZ9Q0urB8Kb/Ba");
        //true
    }
}

存在用户的密码存在相同的情况 , 利用工具类自动给明文加不同的盐 , 再由工具类判断 , 数据库字段就不需要再设置salt字段 6!

//密码 加盐 加密 存储
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode(vo.getPassword());
entity.setPassword(encode);

225.分布式Session

225.1 普通Sessoin的问题

无法跨域名共享Session数据

不同服务,session不能共享

225.2 问题解决-统一存储

227.SpringSession

227.1 start

227.1 auth服务

将用户信息保存再redis session中

依赖

<!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

    <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
            <version>2.5.1</version>
        </dependency>

配置

 spring:
   session:
     store-type: redis
     server:
 port: 20000
   servlet:
     session:
       timeout: 30m

start

@EnableRedisHttpSession

@EnableRedisHttpSession
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
public class AuthApp {
    public static void main(String[] args) {
        SpringApplication.run(AuthApp.class,args);
    }
}

放入session

    session.setAttribute("loginUser",userName);

 @PostMapping("/login")
    public String login(UserLoginVo vo , RedirectAttributes redirectAttributes , HttpSession session ){

        R login = memberFeignService.login(vo);
        if(login.getCode() == 0){
            //成功
            String userName = "leifengyang mock";
            //SpringData
            System.err.println("将用户信息存储值redis session");
            session.setAttribute("loginUser",userName);
            return "redirect:http://gulimall.com";
        }else {
            HashMap<String, String> errors = new HashMap<>();
            errors.put("msg","账号或密码错误");
            redirectAttributes.addFlashAttribute("errors",errors);
            return  "redirect:http://auth.gulimall.com/login.html";

        }
    }

227.2 product服务

需要从rediss session中获取 , 也需要引入依赖 , 配置相同!

228.SpringSession-自定义

228.1 子域Session共享 与 序列化

@Configuration
public class GulimallSessionConfig {

    /**
     * 子域共享session,扩大作用域
     * @return
     */
    @Bean
    public CookieSerializer cookieSerializer(){
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        
        cookieSerializer.setDomainName("gulimall.com");
        cookieSerializer.setCookieName("GULISESSION");
        return cookieSerializer;
    }

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

认证服务 和 商品 服务都需要配置 !

页面获取session信息

   <a href="http://auth.gulimall.com/login.html">你好,请登录 :  [[${session.loginUser == null ?'':session.loginUser}]] </a>

231.单点登录

231.1 多系统

127.0.0.1 ssoserver.com 127.0.0.1 client1.com 127.0.0.1 client2.com

中央认证服务器 ; ssoserver.com

其他系统,想要登录去ssoserver.com登录,登录成功跳转回来

只要有一个登录,其他都不用登录

全系统唯一一个sso.sessionid

232.2 单点登录-流程一

客户端有一个受保护的方法 , 需要登录后才能访问 , 如果没登录 (session中没有) , 则去重定向到登录页面(登录服务器),

但是认证服务器成功认证之后需要调回原路径 , 所以再重定向地址上加上参数 , 让认证服务器能够跳转回来

<span style="background-color:#333333"><span style="color:#c88fd0">return</span> <span style="color:#d26b6b">"redirect:"</span> <span style="color:#b8bfc6">+</span> <span style="color:#b8bfc6">ssoServerUrl</span> <span style="color:#b8bfc6">+</span><span style="color:#d26b6b">"?redirect_url=http://client1.com:8081/employees"</span>;</span>

client1

controller

@Controller
public class HelloController {

    @Value("${sso.server.url}")
    public String ssoServerUrl;

    /**
     * 无需访问就可登录
     * @return
     */
    @ResponseBody
    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }
	
     /**
     * 需要登录后才能访问
     * @param model
     * @param session
     * @return
     */
    @GetMapping("/employees")
    public String employees(Model model , HttpSession session){

        Object loginUser = session.getAttribute("loginUser");
        if(loginUser == null){
            //没登录 , 跳转到登录服务器进行登录
            
            //跳转过去之后 , 使用url的参数表示我们是哪个页面跳转到登录页面的
            return "redirect:" + ssoServerUrl +"?redirect_url=http://client1.com:8081/employees";
        }else{
            ArrayList<String> emps = new ArrayList<>();
            emps.add("张三");
            emps.add("李四");
            model.addAttribute("emps",emps);
            return "list";
        }
    }
}

application.yml

server:
  port: 8081
sso:
  server:
    url: http://sso.com:8080/login.html

ssoServer

controller

@Controller
public class LoginController {

    @GetMapping("/login.html")
    public String loginPage(@RequestParam("redirect_url") String redirectUrl){

        return "login";
    }

    @PostMapping("/doLogin")
    public String doLogin(){

        //登录成功跳转 , 跳回到之前的页面
        return "";
    }
}

232.3 单点登录-流程二

232.3.1 redirectUrl丢失

执行 /login.html  的请求映射(跳转到登录页面) 后需要进行登录 , 传递的url需要传给 /doLogin方法进行跳转 , 但是这样会丢失 redirectUrl , 所以将得到的url放入页面(model)中

  @GetMapping("/login.html")
    public String loginPage(@RequestParam("redirect_url") String url , Model model){
        model.addAttribute("url",url); //将url放入model中
        return "login";
    }

 <form action="/doLogin" method="post">
        用户名 : <input name="username"><br/>
        密码 : <input name="password"> <br/>
        <input type="hidden" name="url" th:value="${url}"> 
     	<!--放入model中取,doLoin方法再从表单的数据中取-->
        <input type="submit" value="登录"/>
    </form>

 @PostMapping("/doLogin")
    public String doLogin(@RequestParam("username") String username ,
                          @RequestParam("password") String password ,
                          @RequestParam("url")String url){
        if(!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)){
            //登录成功 , 跳回之前的页面
            return "redirect:" + url;
        }
        //登录成功跳转 , 跳回到之前的页面
        return "login";
    }

232.3.2 死循环

client1

 @GetMapping("/employees")
    public String employees(Model model , HttpSession session){

        Object loginUser = session.getAttribute("loginUser");
        if(loginUser == null){
            //没登录 , 跳转到登录服务器进行登录
            //跳转过去之后 , 使用url的参数表示我们是哪个页面跳转到登录页面的
            return "redirect:" + ssoServerUrl +"?redirect_url=http://client1.com:8081/employees";
        }else{
            ArrayList<String> emps = new ArrayList<>();
            emps.add("张三");
            emps.add("李四");
            model.addAttribute("emps",emps);
            return "list";
        }
    }

从登录页跳转回来之后 , 重定向相当于再发一次 /employees 请求 , 此时客户端无法判断这次请求是正常的请求,还是跳转回来的请求,这时 session依然为空 , 还会跳转到 登录页面 !

所以 , 再登录跳转回来的时候 , 再在请求路径上添加一个 token 参数 , 并且将 用户信息保存起来(方式不限)

ssoServer

/**
     * 需要登录后才能访问
     * @param model
     * @param session
     * @param token 登录跳转回来才会带上token
     * @return
     */
    @GetMapping("/employees")
    public String employees(Model model ,
                            HttpSession session ,
                            @RequestParam( value = "token" , required = false) String token){

        if(!StringUtils.isEmpty(token)){
            //去sessino登录成功跳回来就会带上
            //TODO : 去 ssoServer获取当前token真正对应的用户信息
            session.setAttribute("loginUser","zhangsan");
        }


        Object loginUser = session.getAttribute("loginUser");
        if(loginUser == null){
            //没登录 , 跳转到登录服务器进行登录
            //跳转过去之后 , 使用url的参数表示我们是哪个页面跳转到登录页面的
            return "redirect:" + ssoServerUrl +"?redirect_url=http://client1.com:8081/employees";
        }else{
            ArrayList<String> emps = new ArrayList<>();
            emps.add("张三");
            emps.add("李四");
            model.addAttribute("emps",emps);
            return "list";
        }
    }

232.4 单点登录-流程三 一处登录,处处登录

给当前系统留一个记号(cookie)sso_token,客户端只要由任何一个登录成功了,就让服务器端给网页留下一个cookie(key=sso_token,value=uuid)

浏览器以后访问这个域名都要带上这个域名下的所有cookie

sso-server

  Cookie sso_token = new Cookie("sso_token", uuid);
  response.addCookie(sso_token);

@Controller
public class LoginController {

    @Autowired
    private StringRedisTemplate redisTemplate;


    @GetMapping("/login.html")
    public String loginPage(@RequestParam("redirect_url") String url , Model model){
        model.addAttribute("url",url);
        return "login";
    }

    @PostMapping("/doLogin")
    public String doLogin(@RequestParam("username") String username ,
                          @RequestParam("password") String password ,
                          @RequestParam("url")String url,
                          HttpServletResponse  response 
    ){
        if(!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)){
            //登录成功 , 跳回之前的页面
         	//....
            Cookie sso_token = new Cookie("sso_token", uuid);
            response.addCookie(sso_token);
            return "redirect:" + url + "?token=" +uuid;
        }
        //登录成功跳转 , 跳回到之前的页面
        return "login";
    }

}

判断是否登录过,判断是否有关键cookie,sso_token,如果没有就展示登录页,如果有就直接返回之前的页面


     @GetMapping("/login.html")
    public String loginPage(@RequestParam("redirect_url") String url ,
                            Model model,
                            @CookieValue(value = "sso_token",required = false) String sso_token

    ){
        if(!StringUtils.isEmpty(sso_token)){
            //说明之前有人登录过,浏览器留下了痕迹
            return "redirect:"+url+"?token="+sso_token;
        }
        model.addAttribute("url",url);
        return "login";
    }

234.5 代码

234.5.1 sso-server

@Controller
public class LoginController {

    @Autowired
    private StringRedisTemplate redisTemplate;


    @ResponseBody
    @GetMapping("/userInfo")
    public String userInfo(@RequestParam("token") String token){
        String s = redisTemplate.opsForValue().get(token);
        return s;
    }

    @GetMapping("/login.html")
    public String loginPage(@RequestParam("redirect_url") String url ,
                            Model model,
                            @CookieValue(value = "sso_token",required = false) String sso_token

    ){
        if(!StringUtils.isEmpty(sso_token)){
            //说明之前有人登录过,浏览器留下了痕迹
            return "redirect:"+url+"?token="+sso_token;
        }
        model.addAttribute("url",url);
        return "login";
    }

    @PostMapping("/doLogin")
    public String doLogin(@RequestParam("username") String username ,
                          @RequestParam("password") String password ,
                          @RequestParam("url")String url,
                          HttpServletResponse  response
    ){
        if(!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)){
            //登录成功 , 跳回之前的页面
            String uuid = UUID.randomUUID().toString().replace("-", "");
            redisTemplate.opsForValue().set(uuid,username);
            Cookie sso_token = new Cookie("sso_token", uuid);
            response.addCookie(sso_token);
            return "redirect:" + url + "?token=" +uuid;
        }
        //登录成功跳转 , 跳回到之前的页面
        return "login";
    }

}

234.5.2 client1

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" >
<head>
    <meta charset="UTF-8">
    <title>员工列表</title>
</head>
<body>
    <h1>欢迎 :[[${session.loginUser}]]</h1>
    <ul>
        <li th:each = "emp:${emps}">姓名 : [[${emp}]]</li>
    </ul>
</body>
</html>

@Controller
public class HelloController {

    @Value("${sso.server.url}")
    public String ssoServerUrl;

    /**
     * 无需访问就可登录
     * @return
     */
    @ResponseBody
    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }




    /**
     * 需要登录后才能访问
     * @param model
     * @param session
     * @param token 登录跳转回来才会带上token
     * @return
     */
    @GetMapping("/employees")
    public String employees(Model model ,
                            HttpSession session ,
                            @RequestParam( value = "token" , required = false) String token){

        if(!StringUtils.isEmpty(token)){
            //去sessino登录成功跳回来就会带上
            //TODO : 去 ssoServer获取当前token真正对应的用户信息
            RestTemplate restTemplate = new RestTemplate();
            ResponseEntity<String> forEntity = restTemplate.getForEntity("http://sso.com:8080/userInfo?token=" + token, String.class);
            String body = forEntity.getBody();

            session.setAttribute("loginUser",body);
        }


        Object loginUser = session.getAttribute("loginUser");
        if(loginUser == null){
            //没登录 , 跳转到登录服务器进行登录
            //跳转过去之后 , 使用url的参数表示我们是哪个页面跳转到登录页面的
            return "redirect:" + ssoServerUrl +"?redirect_url=http://client1.com:8081/employees";
        }else{
            ArrayList<String> emps = new ArrayList<>();
            emps.add("张三");
            emps.add("李四");
            model.addAttribute("emps",emps);
            return "list";
        }
    }
}

234.5.3 client2

@Controller
public class HelloController {

    @Value("${sso.server.url}")
    public String ssoServerUrl;

    /**
     * 无需访问就可登录
     * @return
     */
    @ResponseBody
    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }

    /**
     * 需要登录后才能访问
     * @param model
     * @param session
     * @param token 登录跳转回来才会带上token
     * @return
     */
    @GetMapping("/boss")
    public String employees(Model model ,
                            HttpSession session ,
                            @RequestParam( value = "token" , required = false) String token){

        if(!StringUtils.isEmpty(token)){
            //去sessino登录成功跳回来就会带上
            //TODO : 去 ssoServer获取当前token真正对应的用户信息
            RestTemplate restTemplate = new RestTemplate();
            ResponseEntity<String> forEntity = restTemplate.getForEntity("http://sso.com:8080/userInfo?token=" + token, String.class);
            String body = forEntity.getBody();
            session.setAttribute("loginUser",body);
        }


        Object loginUser = session.getAttribute("loginUser");
        if(loginUser == null){
            //没登录 , 跳转到登录服务器进行登录
            //跳转过去之后 , 使用url的参数表示我们是哪个页面跳转到登录页面的
            return "redirect:" + ssoServerUrl +"?redirect_url=http://client2.com:8082/boss";
        }else{
            ArrayList<String> emps = new ArrayList<>();
            emps.add("张三");
            emps.add("李四");
            model.addAttribute("emps",emps);
            return "list";
        }
    }
}

236 购物车 - redis 存储模型分析

236.1 redis 存储模型分析

236.2 Cart

/**
 * 购物车
 * 需要计算的属性,必须重写他的get方法,保证每次获取属性都会进行计算
 */
publi c class Cart {

    private List<CartItem> items;
    private Integer countNum; //商品数量
    private Integer countType; //商品类型数量
    private BigDecimal totalAmount; //商品总价
    private BigDecimal reduce = new BigDecimal("0.00"); //减免价格

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

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

    /**
     * 获取商品数量
     * @return
     */
    public Integer getCountNum() {
        int count = 0;
        if(items != null && items.size()>0){
            for (CartItem item : items) {
                count += item.getCount();
            }
        }
        return count;
    }

    /**
     * 获取商品类型数量
     * @return
     */
    public Integer getCountType() {
        int count = 0;
        if(items != null && items.size()>0){
            for (CartItem item : items) {
                count += 1;
            }
        }
        return count;
    }

    /**
     * 获取购物车总金额
     * @return
     */
    public BigDecimal getTotalAmount() {
        BigDecimal amount = new BigDecimal("0");
        //计算购物项总价
        if(items != null && items.size() > 0){
            for (CartItem item : items) {
                BigDecimal totalPrice = item.getTotalPrice();
                amount = amount.add(totalPrice);
            }
        }

        //减去优惠总价
        BigDecimal subtract = amount.subtract(getReduce());
        return subtract;
    }



    public BigDecimal getReduce() {
        return reduce;
    }

    public void setReduce(BigDecimal reduce) {
        this.reduce = reduce;
    }
}

236.3 CartItem

public class CartItem {
    private Long skuId;
    private Boolean check = true;
    private String title;
    private String image;
    private List<String> skuAttr;
    private BigDecimal price;
    private Integer count;
    private BigDecimal totalPrice;

    public BigDecimal getTotalPrice() {
        return this.price.multiply(new BigDecimal(""+this.count));
    }

    public void setTotalPrice(BigDecimal totalPrice) {
        this.totalPrice = totalPrice;
    }

    public Long getSkuId() {
        return skuId;
    }

    public void setSkuId(Long skuId) {
        this.skuId = skuId;
    }

    public Boolean getCheck() {
        return check;
    }

    public void setCheck(Boolean check) {
        this.check = check;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getImage() {
        return image;
    }

    public void setImage(String image) {
        this.image = image;
    }

    public List<String> getSkuAttr() {
        return skuAttr;
    }

    public void setSkuAttr(List<String> skuAttr) {
        this.skuAttr = skuAttr;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    public Integer getCount() {
        return count;
    }

    public void setCount(Integer count) {
        this.count = count;
    }

}

239.ThreadLocal

239.1 拦截器-判断用户是否登录了

拦截器 : 再执行controller方法之前做一些事情

@Component
public class CartInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        UserInfoTo userInfoTo = new UserInfoTo();
        HttpSession session = request.getSession();
        String userName = (String) session.getAttribute(AuthServerConstant.LOGIN_USER);
        if(userName != null && !userName.equals("")){
            //登录了
            userInfoTo.setUserId(userName);
            return true;
        }else{
            //没登录
            return false;
        }
    }
}

.

添加配置

addInterceptors

@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {

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

239.2 ThreadLocal

拦截器Interceptor 与 controller 再同一次请求中一直使用的是一个线程,所以利用ThreadLocal进行数据传递

ThreadLocal-interceptor

public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();

ThreadLocal-controller

UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();

interceptor

public class CartInterceptor implements HandlerInterceptor {

    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();
        String userName = (String) session.getAttribute(AuthServerConstant.LOGIN_USER);
        if(userName != null && !userName.equals("")){
            //登录了
            userInfoTo.setUserId(userName);
            threadLocal.set(userInfoTo);
            return true;
        }else{
            //没登录
            return false;
        }
    }
}

controller

@Controller
public class CartController {

    @GetMapping("/cart.html")
    public String cartListPage( ){
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        return "cartList";
    }
}

241 添加购物车

241.1 opsForHash

绑定对hash的操作

 /**
     * 获取到要操作的购物车
     * @return
     */
    private BoundHashOperations<String, Object, Object> getCartOps() {
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        String cartKey = "";
        if(userInfoTo.getUserId() != null){
            cartKey = CART_PREFIX + userInfoTo.getUserId();
        }
        BoundHashOperations<String, Object, Object> stringObjectObjectBoundHashOperations = redisTemplate.boundHashOps(cartKey);
        return stringObjectObjectBoundHashOperations;
    }

241.2 code

 @Override
    public CartItem addToCart(Long skuId, Integer num) {
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();

        CartItem cartItem = new CartItem();

        //添加商品到购物车 getSkuInfoTask
        CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> {
            R skuInfo = productFeignService.getSkuInfo(skuId);
            SkuInfoVo data = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {});
            cartItem.setCheck(true);
            cartItem.setCount(num);
            cartItem.setImage(data.getSkuDefaultImg());
            cartItem.setSkuId(skuId);
            cartItem.setPrice(data.getPrice());
        }, executor);


        //远程查询sku销售属性组合信息 getSkuSaleAttrTask
        CompletableFuture<Void> getSkuSaleAttrTask = CompletableFuture.runAsync(() -> {
            List<String> skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId);
            cartItem.setSkuAttr(skuSaleAttrValues);
        }, executor);

        //阻塞等待任务全部完成
        CompletableFuture<Void> task = CompletableFuture.allOf(getSkuInfoTask, getSkuSaleAttrTask);
        try {
            task.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        //将添加的商品放入对应用户的redis hash 中
        String cartItemJSON = JSON.toJSONString(cartItem);
        cartOps.put(skuId,cartItemJSON);
        return cartItem;
    }

241.3 细节处理

如果购物车不存在这个购物项则为添加新购物项操作

如果购物车中存在该商品则为增加数量操作

 @Override
    public CartItem addToCart(Long skuId, Integer num) {
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();

        //判断购物车现在是否有此商品
        String skuInfoJSON = (String) cartOps.get(skuId.toString());
        if(StringUtils.isEmpty(skuInfoJSON)){
            CartItem cartItem = new CartItem();
           //购物车无此商品
            //添加商品到购物车 getSkuInfoTask
            CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> {
                R skuInfo = productFeignService.getSkuInfo(skuId);
                SkuInfoVo data = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {});
                cartItem.setCheck(true);
                cartItem.setCount(num);
                cartItem.setImage(data.getSkuDefaultImg());
                cartItem.setSkuId(skuId);
                cartItem.setPrice(data.getPrice());
            }, executor);


            //远程查询sk销售属性u组合信息 getSkuSaleAttrTask
            CompletableFuture<Void> getSkuSaleAttrTask = CompletableFuture.runAsync(() -> {
                List<String> skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId);
                cartItem.setSkuAttr(skuSaleAttrValues);
            }, executor);

            //阻塞等待任务全部完成
            CompletableFuture<Void> task = CompletableFuture.allOf(getSkuInfoTask, getSkuSaleAttrTask);
            try {
                task.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }

            //将添加的商品放入对应用户的redis hash 中
            String cartItemJSON = JSON.toJSONString(cartItem);
            cartOps.put(skuId,cartItemJSON);
            return cartItem;
        }
        else {
            //购物车有此商品,修改数量
            CartItem cartItem = JSON.parseObject(skuInfoJSON, CartItem.class);
            cartItem.setCount(cartItem.getCount()+num);
            cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
            return cartItem;
        }
    }

241.4 重复添加

再添加成功页面刷新会重复添加商品

添加商品成功后重定向到另一个页面,在这个页面没有添加购物车操作,只是展示成功添加了的商品详情

241.4.1 RedirectAttribute

/**
 *
 *RedirectAttributes redirectAttributes
    *  redirectAttributes.addFlashAttribute(); 将数据放在session里面可以再页面中取出,但是只能取一次
    *  redirectAttributes.addAttribute(); 将数据放在url后面
    * 添加购物侧
    * @return
 */

241.4.2 code

 /**
     *
     *RedirectAttributes redirectAttributes
     *  redirectAttributes.addFlashAttribute(); 将数据放在session里面可以再页面中取出,但是只能取一次
     *  redirectAttributes.addAttribute(); 将数据放在url后面
     * 添加购物侧
     * @return
     */
    @GetMapping("/addToCart")
    public String addToCart(@RequestParam("skuId") Long skuId,
                            @RequestParam("num") Integer num,
                            RedirectAttributes redirectAttributes
                            ){
      cartService.addToCart(skuId,num);
      //将数据自动放到url后面
      redirectAttributes.addAttribute("skuId",skuId);
      return "redirect:http://cart.gulimall.com/addToCartSuccess.html";
    }

    /**
     * 跳转到购物车添加成功页
     * @param skuId
     * @param model
     * @return
     */
    @GetMapping("/addToCartSuccess.html")
    public String addToCartSuccessPage(@RequestParam("skuId") Long skuId,Model model){
        //重定向到成功页面,再次查询购物车数据即可
        CartItem cartItem = cartService.getCartItem(skuId);
        model.addAttribute("item",cartItem);
        return "success";
    }

return "redirect:http://cart.gulimall.com/addToCartSuccess.html";
//重定向到下一个页面 , 刷新下一个页面没有添加商品的操作 , 只有展示的商品详细信息的操作

249 RabbitMQ

249.1 安装

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

默认账户名和密码 : guest , guest

249.2 SpringBoot rabbit starter

pom

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

yml

spring:      
    rabbitmq:
        host: 192.168.64.140
        port: 5672
        virtual-host: /

249.3 SpringBootTemplate

249.3.1 declearExchange

 @Autowired
    private AmqpAdmin amqpAdmin;

    /**
     * 创建direct交换机
     */
    @GetMapping("/declareDirectExchange")
    public void createDirectExchange(){
        /**
         * DirectExchange(String name, boolean durable, boolean autoDelete, Map<String, Object> arguments)
         */
        DirectExchange directExchange = new DirectExchange("hello-java-exchange",true,false);
        amqpAdmin.declareExchange(directExchange);
        log.info("exchange[{}]创建成功","hello-java-exchange");
    }

249.3.2 declearQueue


    /**
     * 创建队列
     */
    @GetMapping("/declareQueue")
    public void createQueue(){
        /**
         * public Queue(String name, boolean durable, boolean exclusive, boolean autoDelete,@Nullable Map<String, Object> arguments)
         */
        Queue queue = new Queue("hello-java-queue",true,false,false);
        amqpAdmin.declareQueue(queue);
        log.info("queue[{}]创建成功","hello-java-queue");
    }

249.3.3 declearBinding

 /**
     * 绑定交换机和队列
     */
    @GetMapping("declearBinding")
    public void createBinding(){

        Binding binding = new Binding("hello-java-queue",   //目的地 , 交换机或者队列的名称
                                       Binding.DestinationType.QUEUE, //目的地类型 , 枚举 : 交换机或者队列
                                       "hello-java-exchange",//交换机名
                                        "hello.java",       //路由键
                                        null                //map
                );
        amqpAdmin.declareBinding(binding);
        log.info("bingding[{}]创建成功","hello-java-binding");

    }

249.3.4 sendMessage

/**
     * 发送消息
     */
    @GetMapping("/sendMessage")
    public void sendMessage(){
        //发送消息 , 如果发送的消息是一个对象 , 我们会使用序列化机制 , 将对象写出去 , 对象必须实现Serializable
        OrderReturnReasonEntity orderReturnReasonEntity = new OrderReturnReasonEntity();
        orderReturnReasonEntity.setId(1L);
        orderReturnReasonEntity.setCreateTime(new Date());
        orderReturnReasonEntity.setName("哈哈");
        rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",orderReturnReasonEntity);
        log.info("消息发送完成entity:{}",orderReturnReasonEntity);
    }

默认将对象存储在队列,队列会被jdk的方式序列化.

 将序列化方式改为JSON序列化

@Configuration
public class MyRabbitConfig {

    /**
     * 消息序列化转换器默认使用jdk序列化,转变为JSON序列化
     * @return
     */
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

249.4 @RabbitListener & @RabbitHandler

@RabbitListener  : 可以标注在类和方法上

@RabbitHandler : 可以标注方法上(区分不同的消息)

对于同一队列中不同类型的消息对应不同的listener接收

@RabbitListener(queues = {"hello-java-queue"})
public class QueueConsumer{
    /**
     * RabbitHandler 接收 hello-java-queue 中 OrderReturnReasonEntity的消息
     */
    @RabbitHandler
    public void recieveMessage1(Message message,
                               OrderReturnReasonEntity orderReturnReasonEntity,
                               Channel channel
                               ){
        log.error("从队列hello-java-queue消费消息,message:{}",message);
        log.error("消息体:{}",entity);
    }
    
    /**
     * RabbitHandler 接收 hello-java-queue 中 Order的消息
     */
    @RabbitHandler
    public void recieveMessage2(Message message,
                               Order order,
                               Channel channel
                               ){
        log.error("从队列hello-java-queue消费消息,message:{}",message);
        log.error("消息体:{}",entity);
    }
    
}

249.5 RabbitMQ 消息确认机制

  • publisher confirmCallback 确认模式

  • publisher returnCallback 未投递到 quene 退回模式

  • consumer ack机制

249.5.1 可靠抵达-ConfirmCallback (过时了)

  • 在创建 connectionFactory的时候设置PublisherConfirm(true)选项,开启confirmCallback.

  • CorrelationData: 用来表示当前消息唯一性

  • 消息只要被broker接收就会执行confirmCallback,如果是cluster模式,需要被所有broker接收到才会调用confirmCallback

  • 被broker接收到只能表示message已经到达服务器,并不能保证消息一定会被投递到queue中.所以需要用到接下来的returnCallback

第一步 : 开启生产者发送确认回调

<span style="background-color:#333333"><span style="color:#84b6cb">spring</span><span style="color:#b7b3b3">:</span>
<span style="color:#84b6cb">    rabbitmq</span><span style="color:#b7b3b3">:</span>
<span style="color:#84b6cb">        publisher-confirms</span><span style="color:#b7b3b3">: </span>true <span style="color:#da924a">#开启confirmCallback</span></span>

第二部 : 自定义rabbitTemplate,设置确认回调

@Configuration
public class MyRabbitConfig {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 消息序列化转换器默认使用jdk序列化,转变为JSON序列化
     * @return
     */
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }

    @PostConstruct //MyRabbitConfig对象创建完成以后,执行这个方法
    public void initRabbitTemplate(){
        //设置确认回调
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             * @param correlationData 当前消息的唯一关联数据(这个是消息的唯一id)
             * @param ack             消息是否成功收到
             * @param cause           失败的原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                System.err.println("correlationData"+correlationData);
                System.err.println("ack"+ack);
                System.err.println("cause"+cause);
            }
        });
    }
}

249.5.2 消息抵达队列-returnCallback

  • confirm 模式只能保证消息到达broker , 不能保证消息准确投递到目标queue里,在有些业务场景中,我们需要保证消息一定要投递到目标queue里,此时就需要用到return退回模式.

  • 这样如果未能投递到目标queue里将调用returnCallback,可以记录下详细到投递数据,定期的巡检或者自动纠错都需要这些数据.

第一步 : 开启生产者发送消息成功抵达队列回调

spring: 
  rabbitmq:
	publisher-returns: true #开启发送端消息抵达队列的确认
      template:
       mandatory: true #只要抵达队列,一异步发送优先回调我们这个returnconfirm

第二部 : 自定义rabbitTemplate,设置消息抵达队列的确认回调

@Configuration
public class MyRabbitConfig {

    @Autowired
    private RabbitTemplate rabbitTemplate;

	    //..................
        //设置消息抵达队列的确认回调
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            /**
             * 只要消息没有投递到制定的队列,就触发这个失败回调
             * @param message 投递失败的消息详细信息
             * @param replyCode callback 状态码
             * @param replyText callback 文本内容
             * @param exchange
             * @param routingKey
             */
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                System.err.println("message:"+message);
                System.err.println("replyCode:"+replyCode);
                System.err.println("replyText:"+replyText);
                System.err.println("exchange:"+exchange);
                System.err.println("routingKey:"+routingKey);
            }
        });
    }

}

249.5.3 指定消息的id

  rabbitTemplate.convertAndSend("hello-java-exchange",
                                "hello.java",
                                orderReturnReasonEntity,
                			   new CorrelationData(UUID.randomUUID().toString())//指定消息的id
        );

249.5.4 消费者成功消费 - ack

默认自动ack,只要消息接收到,客户端会自动确认(通道一打开,消息一进来就自动确认了),服务端就会移除这个消息

问题 : 我们收到很多消息,自动回复给服务器ack,只有一个消息处理成功,宕机了,发送消息丢失,所以需要手动ack

手动ack,只要我们没有明确的告诉MQ消息已经被消费,没有ack,消息就会一直处于unacked状态,即使Consumer宕机,消息也不会丢失,会重新变为ready状态,下一次有新的consumer连接进来就发给他

  • 消费者获取到消息,成功处理 , 可以回复Ack给broker

    • basic.ack : 用于肯定确认 , broker将移除此消息.

    • basic.nack : 用于否认确定 , 可以指定broker是否丢弃此消息 , 可以批量

    • basic.reject : 用于否认确定 , 同上 但是不能批量

yml

spring:
 rabbitmq: 
  listener:
      simple:
        acknowledge-mode: manual #设置手动ack

ack

/**
     * 接收消息
     * @param message
     * @param entity
     * @param channel 通道
     */
    @RabbitListener(queues = {"hello-java-queue"})
    public void recieveMessage(Message message,
                               OrderReturnReasonEntity entity,
                               Channel channel
                               ){
        log.error("从队列hello-java-queue消费消息,message:{}",message);
        log.error("消息体:{}",entity);

        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            //手抖ack
            channel.basicAck(deliveryTag,false);
            /**
             * channel.basicNack():用于否认确定 , 可以指定broker是否丢弃此消息 ,可以批量
             * requeue : 是否重新入队列
             */
            //channel.basicNack(deliveryTag,false,false);
            /**
             * channel.basicReject():用于否认确定 , 可以指定broker是否丢弃此消息 ,不可以批量
             * requeue : 是否重新入队列
             */
            //channel.basicReject(deliveryTag,false);
        } catch (IOException e) {
            //网络中断
            e.printStackTrace();
        }
    }

263. 订单

263.1 返回订单结算数据

  @Override
    public OrderConfirmVo confirmOrder() {
        OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
        String userName = LoginUserInterceptor.loginUser.get();

        //远程查询所有的收货地址列表
        List<MemberAddressVo> addresses = memberFeignService.getAddressByMemberId(1L);
        orderConfirmVo.setAddress(addresses);

        //远程查询购物车所有选中的购物项
        List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItem();
        orderConfirmVo.setItems(currentUserCartItems);

        //查询用户积分
        orderConfirmVo.setIntegration(1);

        //TODO : 防重令牌

        //其他数据自动计算
        return orderConfirmVo;
    }

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

远程查询购物车所有选中的购物项的serviecImpl

 /**
     * 获取当前用户购物车所有购物项
     * @return
     */
    @Override
    public List<CartItem> getUserCartItems() {

        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        if(userInfoTo.getUserId() == null){
            return null;
        }else {
            String cartKey = CART_PREFIX + userInfoTo.getUserId();
            List<CartItem> cartItems = getCartItems(cartKey);

            //获取所有被选中的购物项
            List<CartItem> collect = cartItems.stream().
                    filter(cartItem -> cartItem.getCheck()).
                    map(cartItem -> {
                        //远程去数据库查询最新商品价格
                        BigDecimal price = productFeignService.getPrice(cartItem.getSkuId());
                        cartItem.setPrice(price);
                        return cartItem;
                    }).
                    collect(Collectors.toList());
            return collect;

        }
    }

feign再远程调用之前会经过拦截器,会对请求进行增强

 template请求模板中没有参数也没有头

263.2.1 原因

263.2.2 解决

如上,给feign的远程调用之前增加拦截器,对feign远程调用进行增强.

Controller , 返回订单确认页需要的数据入口controller

  /**
     * 点击"去结算"调到订单结算页,返回订单确认页需要的数据
     * @return
     */
    @GetMapping("/toTrade")
    public String toTrade(Model model , HttpServletRequest request){
        OrderConfirmVo orderConfirmVo = orderService.confirmOrder();
        model.addAttribute("orderConfirmData",orderConfirmVo);
        return "confirm";
    }

RequestInterceptor 给feign远程调用增加拦截器

ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
             HttpServletRequest request = attributes.getRequest();

这里获取的request相当于入口controller中获取的request

@Configuration
public class GuliFeignConfig {

    @Bean("requestInterceptor")
    public RequestInterceptor  requestInterceptor(){
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                //RequestContextHolder 拿到刚进来的这个请求
                ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
                HttpServletRequest request = attributes.getRequest(); //原请求
                
                //同步请求头数据 , Cookie
                String cookie = request.getHeader("Cookie");
                //给新请同步了原请求的cookie
                template.header("Cookie",cookie);

            }
        };
    }
}

263.3 Feign异步调用丢失上下文问题

263.3.1 异步编排

为了让加速程序运行,无关的查找进行异步编排

/**
     * 返回订单确认页需要的数据
     * @return
     */
    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
        String userName = LoginUserInterceptor.loginUser.get();

        //getAddressListTask
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            //远程查询所有的收货地址列表
            List<MemberAddressVo> addresses = memberFeignService.getAddressByMemberId(1L);
            orderConfirmVo.setAddress(addresses);
        }, executor);

        //getCartItemsTask
        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            //远程查询购物车所有选中的购物项
            List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItem();
            orderConfirmVo.setItems(currentUserCartItems);
        }, executor);


        //查询用户积分
        orderConfirmVo.setIntegration(1);

        //TODO : 防重令牌
        //阻塞等待全部异步任务完成
        CompletableFuture.allOf(getAddressFuture,cartFuture).get();
        //其他数据自动计算
        return orderConfirmVo;
    }

263.3.2 原因

再RequestInterceptor中

ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
             HttpServletRequest request = attributes.getRequest();

RequestContextHolder,的原理是ThreadLocal,同一线程内共享数据

 

如图,一号线程中ThreadLocal数据共享不到二号线程中的RequestInterceptor中

363.3.3 解决

异步编排在各自的线程在次设置头信息

 /**
     * 返回订单确认页需要的数据
     * @return
     */
    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        //.....获取之前的请求
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        //getAddressListTask
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            //远程查询所有的收货地址列表
            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);
           //.....
        }, executor);

        //getCartItemsTask
        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            //远程查询购物车所有选中的购物项
            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);
            //....
        }, executor);
	
        //...
        return orderConfirmVo;
    }

/**
     * 返回订单确认页需要的数据
     * @return
     */
    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
        String userName = LoginUserInterceptor.loginUser.get();
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();


        //getAddressListTask
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            //远程查询所有的收货地址列表
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<MemberAddressVo> addresses = memberFeignService.getAddressByMemberId(1L);
            orderConfirmVo.setAddress(addresses);
        }, executor);

        //getCartItemsTask
        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            //远程查询购物车所有选中的购物项
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItem();
            orderConfirmVo.setItems(currentUserCartItems);
        }, executor);


        //查询用户积分
        orderConfirmVo.setIntegration(1);

        //TODO : 防重令牌
        //阻塞等待全部异步任务完成
        CompletableFuture.allOf(getAddressFuture,cartFuture).get();
        //其他数据自动计算
        return orderConfirmVo;
    }

274.订单-接口幂等性讨论

274.1幂等性解决方案

274.1.1 token机制(验证码)

在查询显示订单页面信息的后端方法ToTrade中保存一个令牌α

在查询显示订单页面信息的页面中保存一个令牌β

每次调用submitOrder方法时对比令牌α和令牌β,如果相等则删除令牌,如果不能则为重复提交的情况

获取令牌 , 对比令牌 , 删除令牌 应该是一个原子操作 , 使用lua脚本进行实现

if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

274.1.2 代码

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

        //TODO : 防重令牌
        String token = UUID.randomUUID().toString().replace("-","");
        //给redis 端保存令牌 (server端)
        redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + userName,token,30, TimeUnit.MINUTES);
        //给网页端 保存令牌(client端)
        orderConfirmVo.setOrderToken(token);
        //其他数据自动计算
        return orderConfirmVo;
    }

生成订单.java

 @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
        String userName = LoginUserInterceptor.loginUser.get();
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        //getAddressListTask
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            //远程查询所有的收货地址列表
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<MemberAddressVo> addresses = memberFeignService.getAddressByMemberId(1L);
            orderConfirmVo.setAddress(addresses);
        }, executor);

        //getCartItemsTask
        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            //远程查询购物车所有选中的购物项
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItem();
            orderConfirmVo.setItems(currentUserCartItems);
        }, executor).thenRunAsync(() -> {
            //查询商品库存信息
            List<OrderItemVo> items = orderConfirmVo.getItems();
            List<Long> skuIds = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
            R r = wareFeignService.getSkuHasStock(skuIds);
            List<SkuHasStockVo> skuHasStockVos = r.getData(new TypeReference<List<SkuHasStockVo>>() {
            });

            if (skuHasStockVos != null && skuHasStockVos.size() > 0) {
                Map<Long, Boolean> skuHasStockMap = skuHasStockVos.stream().collect(Collectors.toMap(
                        k -> k.getSkuId(),
                        v -> v.getHasStock()));

                orderConfirmVo.setSkuHasStockMap(skuHasStockMap);
            }
        });

        //查询用户积分
        orderConfirmVo.setIntegration(1);

        //TODO : 防重令牌
        String token = UUID.randomUUID().toString().replace("-","");
        //给redis 端保存令牌 (server端)
        redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + userName,token,30, TimeUnit.MINUTES);
        //给网页端 保存令牌(client端)
        orderConfirmVo.setOrderToken(token);

        //阻塞等待全部异步任务完成
        CompletableFuture.allOf(getAddressFuture, cartFuture).get();
        //其他数据自动计算
        return orderConfirmVo;
    }

275.订单下单

275.1 连接字符串集合

StringUtils.collectionToDelimitedString(@Nullable Collection<?> coll, String delim);

以符号连接字符串集合

public class Test {
    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        stringList.add("a");
        stringList.add("b");
        stringList.add("c");
        String s = StringUtils.collectionToDelimitedString(stringList, ";");
        System.out.println(s);
    }
}
//结果 : a;b;c

275.2 流程

下订单是一个复杂的流程 , 即使这里也是简化了很多步骤;

275.2.1 验证令牌

在前面分别在Redis和网页端保存了令牌 , 在点击提交订单即下单的时候 , 需要校验令牌.

同上解释 , 为了防止多次提交 , 获得令牌α , 对比令牌β , 删除令牌γ必须是原子性的 , 所以必须用lua脚本

/**
         * 验证令牌
         */
        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();
        //0 : 验证令牌失败
        //1 : 验证令牌成功 , 删除令牌
        Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
                Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + userName),
                orderToken
        );

        if(result == 0L){
            //令牌验证失败 ...
        }else{
            //令牌验证成功
            /**
             * 创建订单
             */      
            /**
             * 验价
             */
            /**
             * 锁定库存
             */
            }else{
              //令牌校验失败 ...
            }

275.2.2 创建订单

给订单实体类的属性赋值 , 为保存数据库做好准备

275.2.3 验证价格

从页面数据获取并且计算后的价格也不不是最新价格 , 当用户下单的时候要验证价格(京东并没有进行价格验证 , 生成订单的时候的价格 , 与支付页面的实际应付金额 , 不一样是正常情况 , 京东并没有进行任何提示)

275.3.3 锁定库存

下单完成后需要锁定库存.

第一步 : 找出SkuId对应那些仓库有库存

 //找到每个商品在哪个仓库都有库存
        List<OrderItemVo> locks = vo.getLocks();
        List<SkuWareHasStockVo> collect = locks.stream().map(item -> {
            SkuWareHasStockVo stock = new SkuWareHasStockVo();
            Long skuId = item.getSkuId();
            stock.setSkuId(skuId);
            stock.setNum(item.getCount());
            //查询这个商品在哪里有库存
            List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId);
            stock.setWareId(wareIds);
            return stock;
        }).collect(Collectors.toList());

 <!--查询这个商品在哪里有库存-->
    <select id="listWareIdHasSkuStock" resultType="java.lang.Long">
            select ware_id from `wms_ware_sku` where sku_id = #{skuId} and stock-stock_lock > 0
    </select>

第二步 : 按地理位置优先的顺序(这里并没有),扣减库存,只要有一个购物项库存不足都无法生成订单

   Boolean allLock = true;
        /**
         * 锁定库存
         */
        for (SkuWareHasStockVo hasStockVo : collect) {
            Boolean skuStocked = false;
            Long skuId = hasStockVo.getSkuId();
            List<Long> wareIds = hasStockVo.getWareId();
            if(wareIds == null || wareIds.size() == 0 ){
                //没有任何仓库有这个商品的库存
                throw new NoStockException(skuId);
            }
            //尝试每一个仓库
            for (Long wareId : wareIds) {
                //成功返回1 , 否则返回0
                Long l = wareSkuDao.lockSkuStock(skuId,wareId,hasStockVo.getNum());
                if(l == 1){
                    skuStocked = true;
                    break;
                }else {
                    //当前仓库锁定失败,重试下一个仓库
                }
                if(skuStocked == false){
                    //当前商品所有仓库都没有锁住
                    throw new NoStockException(skuId);
                }
            }

   <!--锁定并且扣减库存-->
    <select id="lockSkuStock" resultType="java.lang.Long">
        UPDATE `wms_ware_sku`
        set stock = stock_locked + #{num}
        WHERE sku_id = #{skuId} and ware_id = #{wareId} and stock-stock_locked >= #{num}
    </select>

283.分布式事务

283.1 本地事务在分布式下的问题

  • 远程服务假失败

    • 远程服务其实成功了, 由于网络故障等没有返回.

    • 导致 : 订单回滚 , 库存却扣减了.

  • 远程服务执行完成 , 下面的其他方法出现问题

    • 导致 : 已执行的远程请求 , 肯定不能回滚.

@Transactional 本地事务 , 在分布式系统中 只能控制住自己的回滚 , 控制不了其他服务的回滚

出现分布式事务的最大原因就是网络问题

mysql 默认隔离级别:

该隔离级别是 MySQL 默认的隔离级别,在同一个事务里,select 的结果是事务开始时时间 点的状态,因此,同样的 select 操作读到的结果会是一致的.

283.2 Spring本地事务

传播行为

OrderServiceImpl.class

@Transactional(timeout = 30)
public void a(){ 
    b();
    c();
    int a = 10/0;
/*
 *  a中有异常需要回滚 , b会回滚 , 而c不会
 */
}

@Transactional(propagation = Propagation.REQUIRED , timeout = 2)
pubic void b(){ //b的传播行为为 Propagation.REQUIRED 与调用者共享事务 , 自己的设置全部失效
    
}
@Transactional(propagation = Propagation.REQUIRED_NEW , timeout = 20)
public void c(){ //c的传播行为为 Propagation.REQUIRED_NEW 自己新建一个事务
    			
}

同一对象(this)内事务方法互相调用默认失效问题 , 原因 : 绕过了代理对象 , 事务使用代理对象来控制的

解决

  • 引入aop-stater;spring-boot-starter-aop 引入 aspectj

  • @EnableAspectAutoProxy(exposeProxy = true);开启aspectj动态代理功能

  • 本类对象(this.)互相调用

@Transactional(timeout = 30)
public void a(){ 
    OrderServiceImpl orderService = (OrderServiceImpl)AopContext.currentProxy();
    orderService.b();
    orderService.c();
    int a = 10/0;
/*
 *  a中有异常需要回滚 , b会回滚 , 而c不会
 */
}

@Transactional(propagation = Propagation.REQUIRED , timeout = 2)
pubic void b(){ //b的传播行为为 Propagation.REQUIRED 与调用者共享事务 , 自己的设置全部失效
    
}
@Transactional(propagation = Propagation.REQUIRED_NEW , timeout = 20)
public void c(){ //c的传播行为为 Propagation.REQUIRED_NEW 自己新建一个事务
    			
}

285.分布式事务X-Raft理论

这里就不赘述了 , 详见谷粒商城P285

286.分布式事务X-Base理论

实际情况

对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所 以节点故障、网络故障是常态,而且要保证服务可用性达到 99.99999%(N 个 9),即保证 P 和 A,舍弃 C。=>AP

最终一致性

是对 CAP 理论的延伸,思想是即使无法做到强一致性(CAP 的一致性就是强一致性),但可 以采用适当的采取弱一致性,即最终一致性

287.分布式事务解决方案

287.1 柔性事务-TCC 事务补偿型方案

详见博客.

287.2 柔性事务-最大努力通知型方案

按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对。这种 方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种 方案也是结合 MQ 进行实现,例如:通过 MQ 发送 http 请求,设置最大通知次数。达到通 知次数后即不再通知。 案例:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询校对、对 账文件),支付宝的支付成功异步回调

287.3 柔性事务-可靠消息+最终一致性方案(异步确保型)

实现:业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只 记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,向实时消息服务确 认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。 防止消息丢失:

288.Seata-AT(很挫)

前面说性能很差还演示....

第一步 : 每一个参与事务的微服务的数据库必须创建undo_log表

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

第二步 : 下载java项目 seata-server , 并且启动(TC) , 版本应该与seata-server一致 1.3.0

 第三步 : 引入依赖

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

第四步 : 配置seata-server

registry.conf : 注册中心相关配置和配置中心相关配置.

file.conf : 配置文件

第五步 : 在事务方法上加上@GlobalTransactional注解

@GlobalTransactional
@Transactional
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {}

第六步 : 所有想要用到分布式事务的微服务使用seata DatasourceProxy代理自己的数据源


@Configuration
public class MySeataConfig {

    @Autowired
    private DataSourceProperties dataSourceProperties;

    @Bean
    public DataSource dataSource(DataSourceProperties dataSourceProperties){
        HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        if(StringUtils.hasText(dataSourceProperties.getName())){
            dataSource.setPoolName(dataSourceProperties.getName());
        }
        return new DataSourceProxy(dataSource);
    }
}

第七步 : 每个微服务导入 registry.conf  和  file.conf 

290.最终一致性库存解锁逻辑

seata-samples/tcc at master · seata/seata-samples · GitHub TCC-github参考代码

利用延时队列 ,  如果库存锁定成功 , 但是害怕订单下失败 , 可以把锁定库存成功的消息发给消息队列(延时) , 30分钟后再把成功锁定的消息发给库存服务 , 库存服务查询相应的订单 , 来对库存操作(如果失败就解锁库存等...) 

291.RabbitMQ延时队列

291.1 使用场景

292.1 延时队列实现

利用死信路由实现延迟队列.

给队列设置过期时间

292.3 业务用元素

@Configuration
public class MyMQConfig {

    @Bean
    public Queue orderDelayQueue(){
        Map<String,Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange","order-event-exchange");
        arguments.put("x-dead-letter-routing-key","order.release.order");
        arguments.put("x-message-ttl",6000);
        Queue queue = new Queue("order.delay.queue",
                true,
                false,
                false,
                arguments);
        return queue;
    }

    @Bean
    public Queue orderReleaseOrderQueue(){
        Queue queue = new Queue("order.release.order.queue",
                true,
                false,
                false);
        return queue;
    }

    @Bean
    public Exchange orderEventExchange(){
        TopicExchange topicExchange = new TopicExchange("order-event-exchange", true, false);
        return topicExchange;
    }


    @Bean
    public Binding orderReleaseOrderBinding(){
        return new Binding("order.delay.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.create.order",
                null
        );
    }


    @Bean
    public Binding orderCreateOrderBinding(){
        return new Binding("order.release.order.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.release.order",
                null
        );
    }
}

293.利用延迟队列完成库存解锁功能

293.1 业务用元素

创建库存服务用队列元素

需要监听一下队列 , rabbitMQ发现指定元素没有 , 才会创建出来 , 因此可以增加一个消费的空方法(只使用一次)

@Configuration
public class MyMQConfig {

    @RabbitListener(queues = "stock.release.stock.queue")
    public void handle(Message message){

    }


    @Bean
    public Exchange stockEventExchange(){
        return new TopicExchange("stock-event-exchange",true,false);
    }

    @Bean
    public Queue stockReleaseStockQueue(){
        return new Queue("stock.release.stock.queue",true,false,false);
    }

    @Bean
    public Queue stockDelayQueue(){
        Map<String,Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange","stock-event-exchange");
        arguments.put("x-dead-letter-routing-key","stock.release");
        arguments.put("x-message-ttl",12000);
        return new Queue("stock.delay.queue",true,false,false,arguments);
    }

    @Bean
    public Binding stockReleaseBinding(){
        return new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.release.#",
                null);
    }


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

}

293.2 监听库存解锁(P294)

  • 库存解锁的场景

    • 下订单成功 , 订单过期没有支付 , 被系统自动取消或被用户手动取消.都要解锁库存.

    • 下订单成功 , 库存锁定成功 , 接下来业务调用失败, 导致订单回滚 , 之前锁定的库存就要自动解锁.

293.3 远程调用时,排除路径

远程调用order服务的方法时, 由于order服务有需要登录的拦截器 , 而远程请求不需要登录 , 所以需要排除一些路径

 String uri = request.getRequestURI();
        boolean match = new AntPathMatcher().match("/order/order/status/**", uri);
        if(match){
            return true;
        }

order服务的登录拦截器

@Component
public class LoginUserInterceptor implements HandlerInterceptor {

    public static ThreadLocal<String> loginUser = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String uri = request.getRequestURI();
        boolean match = new AntPathMatcher(.match("/order/order/status/**", uri);
        if(match){
            return true;
        }

        String userName = (String) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
        if(userName != null && !userName.equals("")){
            loginUser.set(userName);
            return true;
        }else {
            //没去登录就去登录
            request.getSession().setAttribute("msg","请先进行登录");
            response.sendRedirect("http://auth.gulimall.com/login.html");
            return false;
        }
    }
}

293.4 订单解锁的主动通知

正常情况

库存无法释放的情况

解决 : 当订单释放完成之后注定进行通知

	/**
     * 订单释放直接和库存释放直接绑定
     */
    @Bean
    public Binding orderReleaseOtherBinding(){
        return new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.release.order.#",
                null);
    }

300.消息的不可靠因素

300.1 消息丢失

做好消息确认机制(无论是生产者还是消费者), consumer(手动ack)

每一个发送的消息都在数据库做好记录,定期将失败的消息再次发送一次

300.1.1 消息发送出去 , 由于网络问题没有抵达服务器

CREATE TABLE `mq_message` (
`message_id` char(32) NOT NULL, `content` text, `to_exchane` varchar(255) DEFAULT NULL, `routing_key` varchar(255) DEFAULT NULL, `class_type` varchar(255) DEFAULT NULL, `message_status` int(1) DEFAULT '0' COMMENT '0-新建 1-已发送 2-错误抵达 3-已抵达', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`message_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb

解决 : 保证消息一定会发送出去 , 每一个消息都可以做好日志记录 (给数据库保存每一个消息的详细信息) , 定期扫描数据库将师表的消息再次发送一遍

try {
                rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
            }catch (Exception e){
                //TODO 将没法成功发送的消息进行重试发送
            }

300.1.2 消息抵达Broker,Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚 未持久化完成,宕机。

 

解决 : publisher也必须加入确认回调机制,确认成功的消息,修改数据库消息状态。 

300.1.3 自动ACK的状态下。消费者收到消息,但没来得及消息然后宕机

解决 : 一定开启手动ACK,消费成功才移除,失败或者没来得及处理就noAck并重新入队 

300.2 消息重复

300.2.1 消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消息 重新由unack变为ready,并发送给其他消费者

手动ack失败

解决一 : 将收到消息后的处理做成幂等的(增加各种判断 , 比如除了已锁定库存的状态之外不进行锁定库存操作之类的) ** 解决二 : rabbitMQ的每一个消息都有redelivered字段,可以获取是否 是被重新投递过来的,而不是第一次投递过来的 **

  Boolean redelivered = message.getMessageProperties().getRedelivered();

300.3 消息积压

  • 上线更多的消费者,进行正常消费

  • 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理

300.Ω 总结-注册

300.Ω.1 @GetMapping("/sms/sendCode")-发送验证码

@GetMapping("/sms/sendCode")
    @ResponseBody
    public R sendCode(@RequestParam("phone") String phone){
        String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
        if(!StringUtils.isEmpty(redisCode)){
            long l = Long.parseLong(redisCode.split("_")[1]);
            //接口防刷
            if(System.currentTimeMillis() - l < 60000){
                return R.error(BizCodeEnume.VAILD_SMS_CODE_EXCEPTION.getCode(),BizCodeEnume.VAILD_SMS_CODE_EXCEPTION.getMessage());
            }
        }

        String code = UUID.randomUUID().toString().substring(0, 5) + "_" +System.currentTimeMillis();
        System.err.println("我这里直接在控制台mock,获得验证码了"+code);
        redisTemplate.opsForValue().set(
                AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone , //sms:code:15941148736
                code,
                10,
                TimeUnit.MINUTES); //设置验证码10分钟内有效


        return R.ok();
    }

①接口防刷 : 从redis中获取上次用户注册对应的验证码 , 获取后面设置的毫秒数 , 与再次进入接口的毫秒数详见 , 如果小于60000 , 则相当于多次发送验证码 , 在后端控制接口防刷 , 如果在前端设置60s内无法再次发送则,页面刷新又开始读秒 , 是控制不住的 , 这里并没有引入第三方的发送短信的功能 , 而是在控制台打印了验证码 , mock了发送验证的功能

②生成验证码 , 并在后面加上_当前毫秒数 , 并且存进redis中

300.Ω.2 @PostMapping("/regist")-注册

@PostMapping("/regist")
    public String regist(@Valid UserRegisterVo vo , BindingResult result , RedirectAttributes  redirectAttributes){
        System.out.println("注册!");
        System.out.println(result);
        if(result.hasErrors()){
            //校验出错,转发到注册页
            Map<String, String> errorsMap = result.getFieldErrors().stream().collect(Collectors.toMap(
                    k -> k.getField(),
                    v -> v.getDefaultMessage()
            ));
            redirectAttributes.addFlashAttribute("errors",errorsMap);

            return "redirect:http://auth.gulimall.com/reg.html";
        }

        //注册 调用远程服务进行注册
        String code = vo.getCode();
        String s = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
        if(!StringUtils.isEmpty(s)){
            if(code.equals(s.split("_")[0])){
                //删除验证码,令牌机制
                redisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
                //验证码通过 注册 添加数据库
                System.err.println("调用远程服务,将用户信息保存到数据库");
                R r = memberFeignService.register(vo);
                if(r.getCode() == 0){
                    return  "redirect:http://auth.gulimall.com/login.html";
                }else{
                    HashMap<String, String> errors = new HashMap<>();
                    errors.put("msg","error");
                    redirectAttributes.addFlashAttribute("errors",errors);
                    return "redirect:http://auth.gulimall.com/reg.html";
                }
            }else{
                HashMap<String, String> errors = new HashMap<>();
                errors.put("code","验证码错误");
                redirectAttributes.addFlashAttribute("errors",errors);
                //校验出错 , 转发到注册页
                return "redirect:http://auth.gulimall.com/reg.html";
            }
        }else{
            HashMap<String, String> errors = new HashMap<>();
            errors.put("code","验证码错误");
            redirectAttributes.addFlashAttribute("errors",errors);
            //校验出错 , 转发到注册页
            return "redirect:http://auth.gulimall.com/reg.html";
        }

        //注册成功回到首页 , 回到登录页
        //return "redirect:/login.html";
    }

① 利用JSR-303 校验注册的表达数据是否符合格式

②校验验证码是否正确 , 如果正确就删除验证码的令牌(我感觉没有必要删除 , 如果用户服务保存用户信息的时候做了 , 此手机号已被注册的判断的话 , 就让他自动过期就好了 ) , 调用远程服务将用户信息保存在数据库 

③保存用户信息 => 判断用户手机号是否已经存在 =>判断用户名是否存在 =>利用MD5加盐算法加密密码 

controller

 /**
     * 注册
     */
     @PostMapping("/regist")
     public R register(@RequestBody UserRegisterVo vo){
         try {
             memberService.register(vo);
         }catch (PhoneExistException p){
             return R.error(BizCodeEnume.PHONE_EXIST_EXCEPTION.getCode(),
                     BizCodeEnume.PHONE_EXIST_EXCEPTION.getMessage());
         }catch (UserNameExistException u){
             return R.error(BizCodeEnume.USER_EXIST_EXCEPTION.getCode(),
                     BizCodeEnume.USER_EXIST_EXCEPTION.getMessage());
         }
         return R.ok();
     }

serviceImpl

/**
     * 注册
     * @param vo
     */
    @Override
    public void register(UserRegisterVo vo) {
        MemberEntity entity = new MemberEntity();

        //设置默认等级
        entity.setLevelId(1L);

        //检查用户名和手机号是否唯一
        checkPhoneUnique(vo.getPhone());
        checkUsernameUnique(vo.getUserName());

        entity.setMobile(vo.getPhone());
        entity.setUsername(vo.getUserName());
        entity.setNickname(vo.getUserName());

        //密码 加盐 加密 存储
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encode = passwordEncoder.encode(vo.getPassword());
        entity.setPassword(encode);

        memberDao.insert(entity);
    }

300.Ω.3 @GetMapping("/login.html")-登录页

 @GetMapping("/login.html")
    public String loginPage(HttpSession session){
        Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);
        if(attribute == null){
            //没登录
            return "login";
        }else{
            return "redirect:http://gulimall.com";
        }
    }

①判断session是否存在

300.Ω.4 @PostMapping("/login")-登录

@PostMapping("/login")
    //账号 15941148735 密码 rrrrrrr
    public String login(UserLoginVo vo , RedirectAttributes redirectAttributes , HttpSession session ){

        R login = memberFeignService.login(vo);
        if(login.getCode() == 0){
            //成功
            String userName = "leifengyang mock";
            //SpringData
            System.err.println("将用户信息存储值redis session");
            session.setAttribute(AuthServerConstant.LOGIN_USER,userName);
            return "redirect:http://gulimall.com";
        }else {
            HashMap<String, String> errors = new HashMap<>();
            errors.put("msg","账号或密码错误");
            redirectAttributes.addFlashAttribute("errors",errors);
            return  "redirect:http://auth.gulimall.com/login.html";

        }
    }

①调用远程用户服务的登录方法

controller

/**
 * 登录
*/
    @PostMapping("/login")
    public R login(@RequestBody UserLoginVo vo){
       MemberEntity entity =   memberService.login(vo);
       if(entity != null){
           return R.ok();
       }else {
           return R.error(BizCodeEnume.LOGINACCOUNT_PASSWORD_INVALID_EXCEPTION.getCode(),
                          BizCodeEnume.LOGINACCOUNT_PASSWORD_INVALID_EXCEPTION.getMessage()
                   );
       }
    }

serviceImpl

/**
     * 登录
     * @param vo
     * @return
     */
    @Override
    public MemberEntity login(UserLoginVo vo) {

        String loginAccount = vo.getLoginAccount();
        String password = vo.getPassword();

        MemberEntity entity = memberDao.selectOne(new QueryWrapper<MemberEntity>().eq("username", loginAccount).
                or().eq("mobile", loginAccount));
        if(entity == null){
            //登录失败
            return null;
        }else{
            String MD5Password = entity.getPassword();
            BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
            //密码匹配
            boolean matches = passwordEncoder.matches(password, MD5Password);
            //password : 页面传过来的 password
            //MD5Password : 从数据库中取得的 加盐后的password
            //
            if(matches){
                return entity;
            }else{
                return null;
            }

        }
    }

②利用Spring家工具类BCryptPasswordEncoder来对页面传来的password和从数据库中取出的MD5password进行匹配

300.Ω.4 GulimallSessionConfig-自定义SpringSession

@EnableRedisHttpSession
@Configuration
public class GulimallSessionConfig {

    /**
     * 子域共享session,扩大作用域
     * @return
     */
    @Bean
    public CookieSerializer cookieSerializer(){
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName("gulimall.com");
        cookieSerializer.setCookieName("GULISESSION");
        return cookieSerializer;
    }

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

①扩大Session的作用于 , 凡是参与用户信息的服务都需要方大Session的作用于 , 由于auth.gulimall.com内设置的session的作用于为 auth.gulimall.com 其他的服务不可用 , 所以需要将session的作用于方大

②存储在redis中的信息默认是jdk序列化的 , 变为JSON序列化

300.α 总结-购物车

300.α.1 登录拦截器

public class CartInterceptor implements HandlerInterceptor {

    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();
        String userName = (String) session.getAttribute(AuthServerConstant.LOGIN_USER);
        if(userName != null && !userName.equals("")){
            //登录了
            userInfoTo.setUserId(userName);
            threadLocal.set(userInfoTo);
            return true;
        }else{
            //没登录
            return false;
        }
    }
}

300.α.2 获取购物车信息

controller

 /**
     * 获取购物车信息
     * @param model
     * @return
     */
    @GetMapping("/cart.html")
    public String cartListPage(Model model){
       Cart cart =  cartService.getCart();
       model.addAttribute("cart",cart);
       return "cartList";
    }

cartServiceImpl.getCart()

这里getUserId 的作用主要是判断是否是临时购物车 , 但是实际上 , 临时购物车的功能很挫 , 包括京东自己都砍掉了

/**
     * 获取购物车信息
     * @return
     */
    @Override
    public Cart getCart() {
        Cart cart = new Cart();
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        if(userInfoTo.getUserId() != null){
            //登录了
            String cartKey =  CART_PREFIX + userInfoTo.getUserId();
            List<CartItem> cartItems = getCartItems(cartKey);
            cart.setItems(cartItems);
            return cart;
        }
        return null;
    }

cartServiceImpl.getCartItems()

 private List<CartItem> getCartItems(String cartKey){
        BoundHashOperations<String, Object, Object> hashOperations = redisTemplate.boundHashOps(cartKey);
        List<Object> values = hashOperations.values();
        if(values != null && values.size()>0){
            List<CartItem> collect = values.stream().map(obj -> {
                String str = (String) obj;
                CartItem cartItem = JSON.parseObject(str, CartItem.class);
                return cartItem;
            }).collect(Collectors.toList());
            return collect;
        }
        return null;
    }

300.α.3 添加购物车

controller

写了两个映射 , 当添加购物车方法执行完毕后 , 为了防止刷新页面就重复添加 , 重定向到另一个页面 , 只做查询购物车数据操作

/**
     *
     *RedirectAttributes redirectAttributes
     *  redirectAttributes.addFlashAttribute(); 将数据放在session里面可以再页面中取出,但是只能取一次
     *  redirectAttributes.addAttribute(); 将数据放在url后面
     * 添加购物侧
     * @return
     */
    @GetMapping("/addToCart")
    public String addToCart(@RequestParam("skuId") Long skuId,
                            @RequestParam("num") Integer num,
                            RedirectAttributes redirectAttributes
                            ){
      cartService.addToCart(skuId,num);
      //将数据自动放到url后面
      redirectAttributes.addAttribute("skuId",skuId);
      return "redirect:http://cart.gulimall.com/addToCartSuccess.html";
    }



/**
     * 跳转到购物车添加成功页
     * @param skuId
     * @param model
     * @return
     */
    @GetMapping("/addToCartSuccess.html")
    public String addToCartSuccessPage(@RequestParam("skuId") Long skuId,Model model){
        //重定向到成功页面,再次查询购物车数据即可
        CartItem cartItem = cartService.getCartItem(skuId);
        model.addAttribute("item",cartItem);
        return "success";
    }

return "redirect:http://cart.gulimall.com/addToCartSuccess.html";
//重定向到下一个页面 , 刷新下一个页面没有添加商品的操作 , 只有展示的商品详细信息的操作

cartServiceImpl.addToCart()

@Override
    public CartItem addToCart(Long skuId, Integer num) {
        //返回绑定对redis中hash类型的操作.
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();

        //判断购物车现在是否有此商品
        String skuInfoJSON = (String) cartOps.get(skuId.toString());
        if(StringUtils.isEmpty(skuInfoJSON)){
            CartItem cartItem = new CartItem();
           //购物车无此商品
            //添加商品到购物车 getSkuInfoTask
            CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> {
                R skuInfo = productFeignService.getSkuInfo(skuId);
                SkuInfoVo data = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {});
                cartItem.setCheck(true);
                cartItem.setCount(num);
                cartItem.setImage(data.getSkuDefaultImg());
                cartItem.setSkuId(skuId);
                cartItem.setPrice(data.getPrice());
            }, executor);


            //远程查询sk销售属性u组合信息 getSkuSaleAttrTask
            CompletableFuture<Void> getSkuSaleAttrTask = CompletableFuture.runAsync(() -> {
                List<String> skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId);
                cartItem.setSkuAttr(skuSaleAttrValues);
            }, executor);

            //阻塞等待任务全部完成
            CompletableFuture<Void> task = CompletableFuture.allOf(getSkuInfoTask, getSkuSaleAttrTask);
            try {
                task.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }

            //将添加的商品放入对应用户的redis hash 中
            String cartItemJSON = JSON.toJSONString(cartItem);
            cartOps.put(skuId,cartItemJSON);
            return cartItem;
        }
        else {
            //购物车有此商品,修改数量
            CartItem cartItem = JSON.parseObject(skuInfoJSON, CartItem.class);
            cartItem.setCount(cartItem.getCount()+num);
            cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
            return cartItem;
        }
    }

cartServiceImpl.getCartOps()

返回绑定对redis中hash类型的操作.

/**
     * 获取到要操作的购物车
     * @return
     */
    private  BoundHashOperations<String, Object, Object> getCartOps() {
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        String cartKey = "";
        if(userInfoTo.getUserId() != null){
            cartKey = CART_PREFIX + userInfoTo.getUserId();
        }
        BoundHashOperations<String, Object, Object> stringObjectObjectBoundHashOperations = redisTemplate.boundHashOps(cartKey);
        return stringObjectObjectBoundHashOperations;
    }

300.α.4 改变购物车选中状态

controller

 /**
     * 改变购物车项数量
     * @param skuId
     * @param num
     * @return
     */
    @GetMapping("/countItem")
    public String countItem(@RequestParam("skuId") Long skuId,
                            @RequestParam("num") Integer num
                            ){
        cartService.changeItemCount(skuId,num);
        return "redirect:http://cart.gulimall.com/cart.html";
    }

cartServiceImpl.checkItem()

/**
     * 改变购物项的选择状态
     * @param skuId
     * @param check
     * @return
     */
    @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 cartItemJSON = JSON.toJSONString(cartItem);
        cartOps.put(skuId.toString(),cartItemJSON);
    }

300.α.5 改变购物项数量

controller

 /**
     * 改变购物车项数量
     * @param skuId
     * @param num
     * @return
     */
    @GetMapping("/countItem")
    public String countItem(@RequestParam("skuId") Long skuId,
                            @RequestParam("num") Integer num
                            ){
        cartService.changeItemCount(skuId,num);
        return "redirect:http://cart.gulimall.com/cart.html";
    }

cartServiceImpl.changeItemCount()

/**
     * 改变购物车项数量
     * @param skuId
     * @param num
     * @return
     */
    @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));
    }

300.α.6 删除购物项

controller

/**
     * 删除购物车购物项
     * @param skuId
     * @return
     */
    @GetMapping("/deleteItem")
    public String deleteItem(@RequestParam("skuId") Long skuId){
        cartService.deleteItem(skuId);
        return "redirect:http://cart.gulimall.com/cart.html";
    }

cartServiceImpl.deleteItem()

/**
     * 删除购物车购物项
     * @param skuId
     * @return
     */
    @Override
    public void deleteItem(Long skuId) {
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        cartOps.delete(skuId.toString());
    }

300.α.7 获取当前用户购物车所有购物项

controller

/**
     * 获取当前用户购物车所有购物项
     * @return
     */
    @ResponseBody
    @GetMapping("/currentUserCartItems")
    public List<CartItem> getCurrentUserCartItem(){
        return cartService.getUserCartItems();
    }

cartServiceImpl.getUserCartItems()

/**
     * 获取当前用户购物车所有购物项
     * @return
     */
    @Override
    public List<CartItem> getUserCartItems() {

        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        if(userInfoTo.getUserId() == null){
            return null;
        }else {
            String cartKey = CART_PREFIX + userInfoTo.getUserId();
            List<CartItem> cartItems = getCartItems(cartKey);

            //获取所有被选中的购物项
            List<CartItem> collect = cartItems.stream().
                    filter(cartItem -> cartItem.getCheck()).
                    map(cartItem -> {
                        //远程去数据库查询最新商品价格
                        BigDecimal price = productFeignService.getPrice(cartItem.getSkuId());
                        cartItem.setPrice(price);
                        return cartItem;
                    }).
                    collect(Collectors.toList());
            return collect;
        }
    }

cartServiceImpl.getCartItems()

 private List<CartItem> getCartItems(String cartKey){
        BoundHashOperations<String, Object, Object> hashOperations = redisTemplate.boundHashOps(cartKey);
        List<Object> values = hashOperations.values();
        if(values != null && values.size()>0){
            List<CartItem> collect = values.stream().map(obj -> {
                String str = (String) obj;
                CartItem cartItem = JSON.parseObject(str, CartItem.class);
                return cartItem;
            }).collect(Collectors.toList());
            return collect;
        }
        return null;
    }

300.β 总结-订单

300.β.1 登录拦截器

LoginUserInterceptor.java

@Component
public class LoginUserInterceptor implements HandlerInterceptor {

    public static ThreadLocal<String> loginUser = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String uri = request.getRequestURI();
        boolean match = new AntPathMatcher().match("/order/order/status/**", uri);
        if(match){
            return true;
        }

        String userName = (String) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
        if(userName != null && !userName.equals("")){
            loginUser.set(userName);
            return true;
        }else {
            //没去登录就去登录
            request.getSession().setAttribute("msg","请先进行登录");
            response.sendRedirect("http://auth.gulimall.com/login.html");
            return false;
        }
    }
}

300.β.2 给feign请求添加头信息

@Configuration
public class GuliFeignConfig {

    @Bean("requestInterceptor")
    public RequestInterceptor  requestInterceptor(){
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                //RequestContextHolder 拿到刚进来的这个请求
                ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
                if(attributes != null){
                    HttpServletRequest request = attributes.getRequest(); //原请求
                    if(request != null){
                        //同步请求头数据 , Cookie
                        String cookie = request.getHeader("Cookie");
                        //给新请同步了原请求的cookie
                        template.header("Cookie",cookie);
                    }
                }
            }
        };
    }

}

300.β.3 返回订单确认页需要的数据

/**
     * 返回订单确认页需要的数据
     *
     * @return
     */
    @Transactional
    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
        String userName = LoginUserInterceptor.loginUser.get();
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        //getAddressListTask
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            //远程查询所有的收货地址列表
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<MemberAddressVo> addresses = memberFeignService.getAddressByMemberId(1L);
            orderConfirmVo.setAddress(addresses);
        }, executor);

        //getCartItemsTask
        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            //远程查询购物车所有选中的购物项
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItem();
            orderConfirmVo.setItems(currentUserCartItems);
        }, executor).thenRunAsync(() -> {
            //查询商品库存信息
            List<OrderItemVo> items = orderConfirmVo.getItems();
            List<Long> skuIds = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
            R r = wareFeignService.getSkuHasStock(skuIds);
            List<SkuHasStockVo> skuHasStockVos = r.getData(new TypeReference<List<SkuHasStockVo>>() {
            });

            if (skuHasStockVos != null && skuHasStockVos.size() > 0) {
                Map<Long, Boolean> skuHasStockMap = skuHasStockVos.stream().collect(Collectors.toMap(
                        k -> k.getSkuId(),
                        v -> v.getHasStock()));

                orderConfirmVo.setSkuHasStockMap(skuHasStockMap);
            }
        });

        //查询用户积分
        orderConfirmVo.setIntegration(1);

        //TODO : 防重令牌
        String token = UUID.randomUUID().toString().replace("-","");
        //给redis 端保存令牌 (server端)
        redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + userName,token,30, TimeUnit.MINUTES);
        //给网页端 保存令牌(client端)
        orderConfirmVo.setOrderToken(token);

        //阻塞等待全部异步任务完成
        CompletableFuture.allOf(getAddressFuture, cartFuture).get();
        //其他数据自动计算
        return orderConfirmVo;
    }

让我们看看这个方法做了多少事?

①可以看到远程调用购物车的服务并没有传入参数 , 而是从session中获取的用户信息再去查询购物车 , 远程的getCurrentUserCartItem()方式是通过拦截器的LocalThread来获取用户信息的,但是由于远程调用并不像正常调用 , 无法从原请求中获取头信息, 所以导致session为空 , 因此需要对feign的远程调用的template加头信息 ,详见263.2

②采用了异步编排的方式进行数据获取 , 详见 263.3

③对于防止订单重复提交的令牌设置

//TODO : 防重令牌
String token = UUID.randomUUID().toString().replace("-","");
//给redis 端保存令牌 (server端)
redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + userName,token,30, TimeUnit.MINUTES);
//给网页端 保存令牌(client端)
orderConfirmVo.setOrderToken(token);

300.β.4 重头戏-下单

controller

/**
     * 下单功能
     */
    @PostMapping("/submitOrder")
    public String submitOrder(OrderSubmitVo vo , Model model){
        //验证令牌
        //验证价格
        //扣减击飞
        //锁定库存
        //保存订单
        SubmitOrderResponseVo responseVo = orderService.submitOrder(vo);
        if(responseVo.getCode() == 0){
            //下单成功 -> 支付页
            model.addAttribute("submitOrderResp",responseVo);
            return "pay";
        }else{
            //下单失败 -> 重新确认
            String msg = "下单失败";
            switch (responseVo.getCode()){
                case 1 : msg += "订单信息过期,请刷新后再次提交" ;break;
                case 2 : msg += "订单商品价格发送变化,请确认后再次提交" ;break;
                case 3 : msg += "库存锁定失败,商品库存不足" ;break;

            }
            return "redirect:http://order.gulimall.com/toTrade";
        }
    }

orderServiceImpl.submitOrder()

/**
     * 下单功能
     *
     *  验证令牌
     *  验证价格
     *  锁定库存
     *  保存订单
     */
    @Transactional
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
        confirmVoThreadLocal.set(vo);
        SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
        String userName = LoginUserInterceptor.loginUser.get();
        responseVo.setCode(0);

        /**
         * 验证令牌
         */
        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();
        //0 : 验证令牌失败
        //1 : 验证令牌成功 , 删除令牌
        Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
                Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + userName),
                orderToken
        );

        if(result == 0L){
            //令牌验证失败
            responseVo.setCode(1);
            return responseVo;
        }else{
            //令牌验证成功
            /**
             * 创建订单
             */
            OrderCreateTo order = createOrder();
            /**
             * 验价
             */
            BigDecimal payAmount = order.getOrder().getPayAmount();
            BigDecimal payPrice = vo.getPayPrice();
            if(Math.abs(payAmount.subtract(payPrice).doubleValue())<0.01){
                //验证价格成功
                /**
                 * 保存订单
                 */
                saveOrder(order);
                /**
                 * 锁定库存
                 */
                WareSkuLockVo lockVo = new WareSkuLockVo();
                lockVo.setOrderSn(order.getOrder().getOrderSn());
                List<OrderItemVo> locks = order.getOrderItems().stream().map(item -> {
                    OrderItemVo itemVo = new OrderItemVo();
                    itemVo.setSkuId(item.getSkuId());
                    itemVo.setCount(item.getSkuQuantity());
                    itemVo.setTitle(item.getSkuName());
                    return itemVo;
                }).collect(Collectors.toList());
                lockVo.setLocks(locks);


                R r = wareFeignService.orderLockStock(lockVo);
                if(r.getCode() == 0){
                    //锁定成功
                    OrderVo orderVo = new OrderVo();
                    BeanUtils.copyProperties(order.getOrder(),order);
                    responseVo.setOrderVo(orderVo);
                    //订单创建成功发送消息给MQ
                    rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());
                    return responseVo;
                }else{
                    throw new RuntimeException("无库存");
                    //return responseVo;
                }
            }else{
                responseVo.setCode(2);
                return responseVo;
            }

        }

    }

orderServiceImpl.createOrder()

/**
     * 封装订单数据
     * @return
     */
    private OrderCreateTo createOrder(){
        OrderCreateTo createTo = new OrderCreateTo();
        //生成订单号
        String orderSn = IdWorker.getTimeId();
        //创建订单
        OrderEntity orderEntity = buildOrder(orderSn);

        //获取到所有所有订单项信息
        List<OrderItemEntity> orderItemEntities = buildOrderItems(orderSn);

        //计算价格相关
        computePrice(orderEntity,orderItemEntities);
        createTo.setOrder(orderEntity);
        createTo.setOrderItems(orderItemEntities);
        return createTo;
    }

orderServiceImpl.buildOrderItems()

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

orderServiceImpl.buildOrder()

private OrderEntity buildOrder(String orderSn) {
        String userName = LoginUserInterceptor.loginUser.get();
        OrderEntity entity = new OrderEntity();
        entity.setOrderSn(orderSn);
        entity.setMemberId(1L);
        OrderSubmitVo orderSubmitVo = confirmVoThreadLocal.get();

        //获取收货地址信息
        R fare = wareFeignService.getFare(orderSubmitVo.getAddrId());
        FareResponseVo fareResponseVo = fare.getData(new TypeReference<FareResponseVo>() {});

        //设置运费信息
        entity.setFreightAmount(fareResponseVo.getFare());
        //设置收货人信息
        entity.setReceiverCity(fareResponseVo.getMemberAddressVo().getCity());
        entity.setReceiverDetailAddress(fareResponseVo.getMemberAddressVo().getDetailAddress());
        entity.setReceiverName(fareResponseVo.getMemberAddressVo().getName());
        entity.setReceiverPhone(fareResponseVo.getMemberAddressVo().getPhone());
        entity.setReceiverPostCode(fareResponseVo.getMemberAddressVo().getPostCode());
        entity.setReceiverProvince(fareResponseVo.getMemberAddressVo().getProvince());
        entity.setReceiverRegion(fareResponseVo.getMemberAddressVo().getRegion());

        //设置订单的相关状态信息
        entity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
        entity.setAutoConfirmDay(7);
        entity.setDeleteStatus(0);


        return entity;
    }

orderServiceImpl.computePrice()

/**
     * 计算价格相关
     * @param orderEntity
     * @param orderItemEntities
     */
    private void computePrice(OrderEntity orderEntity, List<OrderItemEntity> orderItemEntities) {

        BigDecimal total = new BigDecimal("0.0");
        BigDecimal coupon = new BigDecimal("0.0");
        BigDecimal integration = new BigDecimal("0.0");
        BigDecimal promotion = new BigDecimal("0.0");
        BigDecimal gift = new BigDecimal("0.0");
        BigDecimal growth = new BigDecimal("0.0");
        for (OrderItemEntity entity : orderItemEntities) {
            coupon = coupon.add(entity.getCouponAmount());
            integration = integration.add(entity.getIntegrationAmount());
            promotion = promotion.add(entity.getPromotionAmount());
            total = total.add(entity.getRealAmount());
            gift = gift.add(new BigDecimal(entity.getGiftIntegration().toString()));
            growth = growth.add(new BigDecimal(entity.getGiftGrowth().toString()));
        }
        //订单价格相关
        orderEntity.setTotalAmount(total);
        //应付总额
        orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));
        orderEntity.setPromotionAmount(promotion);
        orderEntity.setIntegrationAmount(integration);
        orderEntity.setCouponAmount(coupon);
        //设置积分等信息
        orderEntity.setIntegration(gift.intValue());
        orderEntity.setGrowth(growth.intValue());

    }

orderServiceImpl.saveOrder()

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

orderServiceImpl.buildOrderItem()

/**
     * 构建某一个订单项
     * @return
     * @param cartItem
     */
    private OrderItemEntity buildOrderItem(OrderItemVo cartItem) {
        OrderItemEntity itemEntity = new OrderItemEntity();
        /**
         * 订单信息 : 订单号
         */

        /**
         * 商品SPU信息
         */
        Long skuId = cartItem.getSkuId();
        R r = productFeignService.getSpuInfoBySkuId(skuId);
        SpuInfoVo data = r.getData(new TypeReference<SpuInfoVo>() {});
        itemEntity.setSpuId(data.getId());
        itemEntity.setSpuBrand(data.getBrandId().toString());
        itemEntity.setSpuName(data.getSpuName());
        itemEntity.setCategoryId(data.getCatalogId());
        /**
         * 商品sku信息
         */
        itemEntity.setSkuId(cartItem.getSkuId());
        itemEntity.setSkuName(cartItem.getTitle());
        itemEntity.setSkuPic(cartItem.getImage());
        itemEntity.setSkuPrice(cartItem.getPrice());
        String skuAttr = StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(), ";");
        itemEntity.setSkuAttrsVals(skuAttr);
        itemEntity.setSkuQuantity(cartItem.getCount());
        /**
         * 优惠券信息[不做]
         */

        /**
         * 积分信息
         */
        itemEntity.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount())).intValue());
        itemEntity.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount())).intValue());

        /**
         * 订单项价格信息
         */
        itemEntity.setPromotionAmount(new BigDecimal("0"));
        itemEntity.setCouponAmount(new BigDecimal("0"));
        itemEntity.setIntegrationAmount(new BigDecimal("0"));
        //当前订单项的实际金额
        BigDecimal origin = itemEntity.getSkuPrice().multiply(new BigDecimal(itemEntity.getSkuQuantity().toString()));
        BigDecimal subtract = origin.subtract(itemEntity.getCouponAmount()).
                subtract(itemEntity.getPromotionAmount()).
                subtract(itemEntity.getIntegrationAmount());
        itemEntity.setRealAmount(subtract);

        return itemEntity;
    }

①利用Lua脚本验证令牌

/**
         * 验证令牌
         */
        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();
        //0 : 验证令牌失败
        //1 : 验证令牌成功 , 删除令牌
        Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
                Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + userName),
                orderToken
        );

②创建订单 , 这里就是set 订单项属性 , 这里就不在赘述了

 /**
 * 创建订单
 */
OrderCreateTo order = createOrder();

③价格验证 

这里 payPrice 是页面来的数据 , payAmount是最新的从数据库中获得的商品价格

/**
 * 验价
*/
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
if(Math.abs(payAmount.subtract(payPrice).doubleValue())<0.01){
    //......
}

④保存订单

将订单保存在数据库中

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

⑤锁定库存

 /**
   * 锁定库存
 */
WareSkuLockVo lockVo = new WareSkuLockVo();
lockVo.setOrderSn(order.getOrder().getOrderSn());
List<OrderItemVo> locks = order.getOrderItems().stream().map(item -> {
    OrderItemVo itemVo = new OrderItemVo();
    itemVo.setSkuId(item.getSkuId());
    itemVo.setCount(item.getSkuQuantity());
    itemVo.setTitle(item.getSkuName());
    return itemVo;
}).collect(Collectors.toList());
lockVo.setLocks(locks);

//远程锁定库存
R r = wareFeignService.orderLockStock(lockVo);

⑤.① WareSkuController =>远程锁定库存controller   

 /**
     * 锁定库存
     *
     * @param vo
     * @return
     */
    @GetMapping("/lock/order")
    public R orderLockStock(@RequestBody WareSkuLockVo vo) {
        try {
            Boolean stockResults = wareSkuService.orderLockStock(vo);
            return R.ok();
        }catch (NoStockException e){
            return R.error(BizCodeEnume.NO_STOCK_EXCEPTION.getCode(),BizCodeEnume.NO_STOCK_EXCEPTION.getMessage());
        }
    }

⑤.② WareSkuServiceImpl =>远程锁定库存ServiceImpl 

/**
     * 锁定库存
     * @param vo
     * @return
     */
    @Transactional
    @Override
    public Boolean orderLockStock(WareSkuLockVo vo) {

        /**
         * 保存库存工作详情 , 相当于 TCC 的 frozen , 用于追溯库存锁定状态
         */
        WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
        taskEntity.setOrderSn(vo.getOrderSn());
        wareOrderTaskService.save(taskEntity);


        //找到每个商品在哪个仓库都有库存
        List<OrderItemVo> locks = vo.getLocks();
        List<SkuWareHasStockVo> collect = locks.stream().map(item -> {
            SkuWareHasStockVo stock = new SkuWareHasStockVo();
            Long skuId = item.getSkuId();
            stock.setSkuId(skuId);
            stock.setNum(item.getCount());
            //查询这个商品在哪里有库存
            List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId);
            stock.setWareId(wareIds);
            return stock;
        }).collect(Collectors.toList());

        Boolean allLock = true;
        /**
         * 锁定库存
         */
        for (SkuWareHasStockVo hasStockVo : collect) {
            Boolean skuStocked = false;
            Long skuId = hasStockVo.getSkuId();
            List<Long> wareIds = hasStockVo.getWareId();
            if(wareIds == null || wareIds.size() == 0 ){
                //没有任何仓库有这个商品的库存
                throw new NoStockException(skuId);
            }
            //尝试每一个仓库
            // 成功 :
            // 失败 :
            for (Long wareId : wareIds) {
                //成功返回1 , 否则返回0
                Long l = wareSkuDao.lockSkuStock(skuId,wareId,hasStockVo.getNum());
                if(l == 1){
                    skuStocked = true;
                    //TODO : 告诉MQ库存锁定成功
                    WareOrderTaskDetailEntity entity =
                            new WareOrderTaskDetailEntity(null, skuId, null, hasStockVo.getNum(), taskEntity.getId(), wareId, 1);
                    wareOrderTaskDetailService.save(entity);
                    //发送库存锁定成功的消息
                    StockLockedTo lockedTo = new StockLockedTo();
                    lockedTo.setId(taskEntity.getId());
                    StockDetailTo stockDetailTo = new StockDetailTo();
                    BeanUtils.copyProperties(entity,stockDetailTo);
                    lockedTo.setStockDetailTo(stockDetailTo);
                    rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);
                    break;
                }else {
                    //当前仓库锁定失败,重试下一个仓库
                }
                if(skuStocked == false){
                    //当前商品所有仓库都没有锁住
                    throw new NoStockException(skuId);
                }
            }
        }
        return true;
    }

自己看代码吧 我已经说不清楚了......

301-309 支付 略

302 内网穿透

304.SpringMCV 指定返回类型

/**
 *
 */
@GetMapping(value = "/payOrder" , produces = "text/html")
public String payOrder(){
    //...
    return pay;
}

支付成功后支付宝返回支付成功页面的html代码(pay) , 这既不是json , 也不是服务的template模板页面

306 远程传输对象

远程调用传输对象推荐用@ResponseBody

/**
     * 查询当前登录用户的订单分页信息
     */
    @PostMapping("/listWithItem")
    public R listWithItem(@ResponseBody Map<String, Object> params){
        PageUtils page = orderService.queryPageWithItem(params);

        return R.ok().put("page", page);
    }

307 内网穿透到Nginx 请求Host头不匹配

 

通知结果异步回调

@PostMapping("/payed/notif")
public String handleAlipayed(HttpServletRequest request){
    //...
    return success;
} 

修改nginx 配置文件X

location /payed/nofif {
    proxy_set_header Host order.gulimall.com;
    proxy_pass http://gulimall
}

修改nginx 配置文件Ω

server {
    listen 08;
    server_name gulimall.com *.guilimall.com 497n86m7k.52http.net;
}

309 收单

①在支付页不动 , 等待订单过期 , 再支付 , 这个时候库存是已解锁状态

 

1.在支付页面一定时间不支付则不能支付.

2.在下单成功之后 , 队列收到消息时(30min) , 手动调用支付宝收单 ,让此次交易无法支付


@Service

public class OrderCloseListener {

    @Autowired
    private OrderService orderService;


    /**
     * 延迟关单
     * @param entity
     * @param channel
     * @param message
     * @throws IOException
     */
    @RabbitListener(queues = "order.release.order.queue")
    public void listener(OrderEntity entity , Channel channel , Message message) throws IOException {
        System.out.println("收到过期的订单信息 , 准备关闭订单 " + entity.getOrderSn());
        try {
            orderService.closeOrder(entity);
            //TODO : 手动调用支付宝收单
            channel.basicAck(message.getMessageProperties().getDeliveryTag() , false);
        }catch (Exception e){
            channel.basicReject(message.getMessageProperties().getDeliveryTag() , true);
        }

    }
}

310.秒杀-高并发的商家不定期促销

秒杀具有瞬间高并发的特点 , 针对这一特点 , 必须要做到限流 + 异步 + 缓存 (页面静态化) + 独立部署(专门的秒杀服务).

 

312.秒杀-Spring定时任务和异步任务

  • 定时任务

    • @EnableScheduling 开启定时任务

    • @Schedule 开启一个定时任务

  • 异步任务 : 我们希望即使定时任务的执行耗时大于执行时间的不长 , 也不会阻塞

    • @EnableAsync 开启异步任务功能

    • @Async 给希望异步执行的方法上标注注解

@Slf4j
@Component
@EnableAsync
@EnableScheduling
public class HelloSchedule {

    @Async
    @Scheduled(cron = "* * * * * ?")
    public void hello() throws InterruptedException {
        log.info("hello....");
        TimeUnit.SECONDS.sleep(3);
    }
}

313秒杀- 时间日期处理

其他计算如法炮制.获取当天00:00 到 两天后的23:59

LocalDate now = LocalDate.now();    //2022-11-11
LocalDate plus1 = now.plusDays(1); //当天加一天 2022-11-12
LocalDate plus2 = now.plusDays(2); //当前天加两天 2022-11-13

LocalTime min =  LocalTime.MIN; //00:00
LocalTime max = LocalTime.MAX; //23:59

//拼接
LocalDateTime start = LocalDateTime.of(now, min); //2022-11-11 00:00
LocalDateTime end = LocalDateTime.of(plus2, max); //2022-11-13 23:59

314.秒杀-秒杀商品上架

/**
     * 每天晚上3点执行 上架最近三天需要秒杀的商品
     */
    @Override
    public void uploadSeckillSkuLatest3Days() {
        R session = couponFeignService.getLatest3DaySession();
        if(session.getCode() == 0){
            List<SeckillSessionWithSkus> sessionData = session.getData(new TypeReference<List<SeckillSessionWithSkus>>() {
            });
            //缓存活动信息
            saveSessionInfos(sessionData);
            //缓存活动的关联商品信息
            saveSessionSkuInfos(sessionData);
        }
    }

缓存活动信息 key: seckill:session:16111111111_16212222222 value:List<String> {1,2,3,4,5}

 /**
     * 缓存活动信息 key: seckill:session:16111111111_16212222222  value:List<String> {1,2,3,4,5}
     * @param sessions
     */
    private void saveSessionInfos(List<SeckillSessionWithSkus> sessions){
        sessions.stream().forEach(session -> {
            Long startTime = session.getStartTime().getTime();
            Long endTime = session.getEndTime().getTime();
            String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;
            List<String> collect = session.getRelationSkus().stream().map(item -> item.getId().toString()).collect(Collectors.toList());
            //缓存活动信息
            stringRedisTemplate.opsForList().leftPushAll(key,collect);
        });
    }

缓存活动的关联商品信息 key:SKUKILL_CACHE_PREFIX field:skuId value:skuInfo

/**
     * 缓存活动的关联商品信息 key:SKUKILL_CACHE_PREFIX    field:skuId   value:skuInfo
     * @param sessions
     */
    private void saveSessionSkuInfos(List<SeckillSessionWithSkus> sessions){
        sessions.stream().forEach(session -> {
            //准备hash操作
            BoundHashOperations<String, Object, Object> ops = stringRedisTemplate.boundHashOps("SKUKILL_CACHE_PREFIX");
            session.getRelationSkus().stream().forEach(seckillSkuVo -> {
                //缓存商品详情
                SecKillSkuRedisTo redisTo = new SecKillSkuRedisTo();
                // sku的基本数据
                R skuInfo = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
                if(skuInfo.getCode() == 0){
                    SkuInfoVo info = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                    });
                    redisTo.setSkuInfo(info);
                }
                // sku秒杀数据
                BeanUtils.copyProperties(seckillSkuVo,redisTo);

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

                //随机码 seckill?skuId=1&key=d35ga3f5
                String token = UUID.randomUUID().toString().replace("-", "");
                redisTo.setRandomCode(token);
                String s = JSON.toJSONString(seckillSkuVo);

                //使用库存作为分布式信号量 限流
                RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                //商品可以秒杀的数量作为信号量
                semaphore.trySetPermits(seckillSkuVo.getSeckillCount());


                ops.put(seckillSkuVo.getSkuId().toString(),s);
            });
        });
    }

317.秒杀-幂等性保证

 

保证redis中存储的hash sku信息不重复添加.

①利用分布式锁控制秒杀商品的上架

@Slf4j
@Component
@EnableAsync
@EnableScheduling
public class SeckillScheduled {

    @Autowired
    private SecKillService secKillService;

    @Autowired
    private RedissonClient redissonClient;

    private final String upload_lock = "seckill:upload:lock";

    //TODO : 幂等性处理 上架之后就不用再上架了
    @Async
    @Scheduled(cron = "0 0 3 * * ?") //每天晚上凌晨3点执行
    public void hello() throws InterruptedException {
        log.info("上架秒杀的商品信息......");
        //重复上架无需处理

        RLock lock = redissonClient.getLock(upload_lock);
        lock.lock(10, TimeUnit.SECONDS);
        try {
            secKillService.uploadSeckillSkuLatest3Days();
        }finally {
            lock.unlock();
        }

    }
}

②校验key时候已经存在 , 防止重复提交秒杀活动

  /**
     * 缓存活动信息 key: seckill:session:16111111111_16212222222  value:List<String> {1,2,3,4,5}
     * @param sessions
     */
    private void saveSessionInfos(List<SeckillSessionWithSkus> sessions){
        sessions.stream().forEach(session -> {
            Long startTime = session.getStartTime().getTime();
            Long endTime = session.getEndTime().getTime();
            String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;
            Boolean hasKey = stringRedisTemplate.hasKey(key);
            if(!hasKey){
                List<String> collect = session.getRelationSkus().stream().map(item -> item.getSkuId().toString()).collect(Collectors.toList());
                //缓存活动信息
                stringRedisTemplate.opsForList().leftPushAll(key,collect);
            }
        });
    }

③校验key时候已经存在 , 防止重复提交hash->sku信息

 /**
     * 缓存活动的关联商品信息 key:SKUKILL_CACHE_PREFIX    field:skuId   value:skuInfo
     * @param sessions
     */
    private void saveSessionSkuInfos(List<SeckillSessionWithSkus> sessions){
        sessions.stream().forEach(session -> {
            //准备hash操作
            BoundHashOperations<String, Object, Object> ops = stringRedisTemplate.boundHashOps("SKUKILL_CACHE_PREFIX");

            session.getRelationSkus().stream().forEach(seckillSkuVo -> {
                String token = UUID.randomUUID().toString().replace("-", "");
            if(!ops.hasKey(seckillSkuVo.getPromotionId().toString() +"_"+seckillSkuVo.getSkuId().toString())){
                //缓存商品详情
                SecKillSkuRedisTo redisTo = new SecKillSkuRedisTo();
                // sku的基本数据
                R skuInfo = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
                if(skuInfo.getCode() == 0){
                    SkuInfoVo info = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                    });
                    redisTo.setSkuInfo(info);
                }
                // sku秒杀数据
                BeanUtils.copyProperties(seckillSkuVo,redisTo);

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

                //随机码 seckill?skuId=1&key=d35ga3f5

                redisTo.setRandomCode(token);
                String s = JSON.toJSONString(seckillSkuVo);

                ops.put(seckillSkuVo.getPromotionId()+"_"+seckillSkuVo.getSkuId().toString(),s);


                //使用库存作为分布式信号量 限流
                RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                //商品可以秒杀的数量作为信号量
                semaphore.trySetPermits(seckillSkuVo.getSeckillCount());

                }
            });
        });
    }
}

 

318 秒杀-查询秒杀商品

/**
     * 返回当前时间可以参与秒杀商品信息
     * @return
     */
    @Override
    public List<SecKillSkuRedisTo> getCurrentSeckillSkus() {
        //确定当前时间属于哪个秒杀场次
        long time = new Date().getTime();
        Set<String> keys = stringRedisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
        for (String key : keys) {
            String replace = key.replace(SESSIONS_CACHE_PREFIX, "");
            String[] s = replace.split("_");
            long start = Long.parseLong(s[0]);
            long end = Long.parseLong(s[1]);
            if(time >= start && time <= end){
                List<String> range = stringRedisTemplate.opsForList().range(key, -100, 100);
                BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
                List<String> list = hashOps.multiGet(range);
                if(list != null){
                    List<SecKillSkuRedisTo> collect = list.stream().map(item -> {
                        SecKillSkuRedisTo redis = JSON.parseObject((String) item, SecKillSkuRedisTo.class);
                        return redis;
                    }).collect(Collectors.toList());
                    return collect;
                }
                break;
            }
        }
        return null;
    }

319.秒杀-商品秒杀

/**
     * 获取商品秒杀信息
     */
    @Override
    public SecKillSkuRedisTo getSkuSeckillInfo(Long skuId) {
        //找到所有需要参与秒杀的商品的key
        BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        Set<String> keys = hashOps.keys();
        if(keys != null && keys.size() > 0){
            String regx = "\\d" + skuId;
            for (String key : keys) {
                if(Pattern.matches(regx,key)){
                    String json = hashOps.get(key);
                    SecKillSkuRedisTo skuRedisTo = JSON.parseObject(json, SecKillSkuRedisTo.class);

                    //随机码 , 只有当前时间在秒杀场次时间范围内则存储随机码
                    long current = new Date().getTime();
                    Long startTime = skuRedisTo.getStartTime();
                    Long endTime = skuRedisTo.getEndTime();
                    if(!(current >= startTime && current <= endTime)){
                        skuRedisTo.setRandomCode(null);
                    }
                    return skuRedisTo;
                }
            }
        }
        return null;
    }

320.秒杀-秒杀系统设计

 

 

321.秒杀-登录检查

加登录拦截器 和 其他服务一样.

322.秒杀-秒杀流程

流程一

 

 流程二

无数据库交互 , 无远程调用.

①在登录拦截器完成了登录判断

②检验合法性 =>时间是否在秒杀活动范围内 =>校验商品随机码和商品Id 

③验证用户购买限制

④生成订单号

 /**
     * kill 秒杀
     * @param killId
     * @param key
     * @param num
     * @return
     */
    @Override
    public String kill(String killId, String key, Integer num) {
        String userName = LoginUserInterceptor.loginUser.get();
        BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        String json = hashOps.get(killId);
        if(StringUtils.isEmpty(json)){
            return null;
        }else {
            SecKillSkuRedisTo redis = JSON.parseObject(json, SecKillSkuRedisTo.class);
            //校验合法性
            Long startTime = redis.getStartTime();
            Long endTime = redis.getEndTime();
            long time = new Date().getTime();
            long userKeyTTL = endTime - time;
            //校验时间的合法性
            if(time >= startTime && time <= endTime){
                //校验随机码和商品id
                String randomCode = redis.getRandomCode();
                String skuId = redis.getPromotionSessionId() + "_" + redis.getSkuId();
                if(randomCode.equals(key) && killId.equals(skuId)){
                    //验证购物数量是否合理
                    if(num <= redis.getSeckillLimit()){
                        //验证这个人是否已经购买过 , 幂等性 , 如果秒杀成功 就去站位 userId_SessionId_skuId
                        String redisKey =  userName + "_" + redis.getPromotionSessionId() + "_" + redis.getSkuId();
                        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), userKeyTTL, TimeUnit.MILLISECONDS);
                        if(aBoolean){
                            //占位成功说明没有买过
                            //获取信号量 tryAcquire => 非阻塞   acquire=>阻塞
                            RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE);
                            try {
                                boolean b = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
                                String timeId = IdWorker.getTimeId();
                                //TODO : 发送MQ
                                return timeId; // 返回订单号
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }else{
                            //说明已经买过了
                            return null;
                        }
                    }
                }else{
                    return null;
                }
            }
        }
        return null;
    }
}

 

323.秒杀-生成订单号发送给MQ

 

异步下订单.

323.1 秒杀用队列元素

    /**
     * 秒杀用队列
     */
    @Bean
    public Queue orderSeckillOrderQueue(){
        return new Queue("order.seckill.order.queue",true,false,false);
    }

    /**
     * 秒杀用绑定关系
     * @return
     */
    @Bean
    public Binding orderSeckillOrderQueueBinding(){
        return new Binding("order.seckill.order.queue",
                         Binding.DestinationType.QUEUE,
                        "order-event-exchange",
                        "order.seckill.order",
                        null);
    }

323.2 发送

 String timeId = IdWorker.getTimeId();
                                //TODO : 发送MQ
                                SeckillOrderTo orderTo = new SeckillOrderTo();
                                orderTo.setOrderSn(timeId);
                                orderTo.setMemberId(1L); //mock userId
                                orderTo.setNum(num);
                                orderTo.setPromotionSessionId(redis.getPromotionSessionId());
                                orderTo.setSkuId(redis.getSkuId());
                                orderTo.setSeckillPrice(1);
                                rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo);
                                return timeId; // 返回订单号

/** 上架商品的时候每一个数据都有过期时间
     * kill 秒杀
     * @param killId
     * @param key
     * @param num
     * @return
     */
    @Override
    public String kill(String killId, String key, Integer num) {
        String userName = LoginUserInterceptor.loginUser.get();
        BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        String json = hashOps.get(killId);
        if(StringUtils.isEmpty(json)){
            return null;
        }else {
            SecKillSkuRedisTo redis = JSON.parseObject(json, SecKillSkuRedisTo.class);
            //校验合法性
            Long startTime = redis.getStartTime();
            Long endTime = redis.getEndTime();
            long time = new Date().getTime();
            long userKeyTTL = endTime - time;
            //校验时间的合法性
            if(time >= startTime && time <= endTime){
                //校验随机码和商品id
                String randomCode = redis.getRandomCode();
                String skuId = redis.getPromotionSessionId() + "_" + redis.getSkuId();
                if(randomCode.equals(key) && killId.equals(skuId)){
                    //验证购物数量是否合理
                    if(num <= redis.getSeckillLimit()){
                        //验证这个人是否已经购买过 , 幂等性 , 如果秒杀成功 就去站位 userId_SessionId_skuId
                        String redisKey =  userName + "_" + redis.getPromotionSessionId() + "_" + redis.getSkuId();
                        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), userKeyTTL, TimeUnit.MILLISECONDS);
                        if(aBoolean){
                            //占位成功说明没有买过
                            //获取信号量 tryAcquire => 非阻塞   acquire=>阻塞
                            RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE);
                            boolean b = semaphore.tryAcquire(num);
                            if(b){
                                String timeId = IdWorker.getTimeId();
                                //TODO : 发送MQ
                                SeckillOrderTo orderTo = new SeckillOrderTo();
                                orderTo.setOrderSn(timeId);
                                orderTo.setMemberId(1L); //mock userId
                                orderTo.setNum(num);
                                orderTo.setPromotionSessionId(redis.getPromotionSessionId());
                                orderTo.setSkuId(redis.getSkuId());
                                orderTo.setSeckillPrice(1);
                                rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo);
                                return timeId; // 返回订单号
                            }
                            return null;

                        }else{
                            //说明已经买过了
                            return null;
                        }
                    }
                }else{
                    return null;
                }
            }
        }
        return null;
    }

323.3 监听

@Slf4j
@RabbitListener(queues = {"order.seckill.order.queue"})
@Component
public class OrderSeckillListener {

    @Autowired
    OrderService orderService;

    /**
     * 创建秒杀订单
     * @param seckillOrderTo
     * @param channel
     * @param message
     * @throws IOException
     */
    @RabbitHandler
    public void listener(SeckillOrderTo seckillOrderTo , Channel channel , Message message) throws IOException {
       try {
           log.info("准备创建秒杀单的详细信息....");
           orderService.createSeckillOrder(seckillOrderTo);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
       }catch (Exception e){
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
       }
    }

}

323.4 保存订单

他并没有扣库存的动作?

/**
     * 创建秒杀订单
     * @param seckillOrderTo
     */
    @Override
    public void createSeckillOrder(SeckillOrderTo seckillOrderTo) {
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setOrderSn(seckillOrderTo.getOrderSn());
        orderEntity.setMemberId(seckillOrderTo.getMemberId());
        orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
        int payAmount = seckillOrderTo.getSeckillPrice() * seckillOrderTo.getNum();
        orderEntity.setPayAmount(new BigDecimal(payAmount + ""));
        this.save(orderEntity);

        //保存订单项信息
        OrderItemEntity itemEntity = new OrderItemEntity();
        itemEntity.setOrderSn(seckillOrderTo.getOrderSn());
        itemEntity.setRealAmount(new BigDecimal(payAmount + ""));
        //获取当前SKU的详细信息进行设置
        itemEntity.setSkuQuantity(seckillOrderTo.getNum());
        orderItemService.save(itemEntity);

    }

325.Sentinel-高并发方法论

限流&熔断&降级

327.Sentinel-整合SpringBoot

第一步 : 引入依赖

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

第二步  : 启动sentinel

java -jar

 第三步 : 配置

spring:
  sentinel:
      transport:
        dashboard: localhost:8333 #控制台端口
        port: 8719 #服务与控制台传输数据端口

331.Sentinel-Feign

331.1 调用方熔断保护

第一步 : 引入sentinel依赖

第二步 : 引入openFeign

第三步 : 配置

feign:
  sentinel:
    enabled: true

第四部:给feign客户端配置降级类

@FeignClient(value = "seckill-service",fallback = SeckillFeignServiceFallBack.class)
public interface SeckillFeignService {


    @GetMapping("/sku/seckill/{skuId}")
    R getSkuSeckillInfo(@PathVariable("skuId") Long skuId);

}

第五步 : 编写降级类

@Component
@Slf4j
public class SeckillFeignServiceFallBack implements SeckillFeignService {
    @Override
    public R getSkuSeckillInfo(Long skuId) {
        log.info("远程调用熔断");
        return R.error(BizCodeEnume.TOO_MANY_REQUEST.getCode(),BizCodeEnume.TOO_MANY_REQUEST.getMessage());
    }
}

331.2 调用方控制台指定远程服务的降级策略

 

332.自定义受保护资源

332.1 抛出异常的方式自定义资源

  try (Entry entry = SphU.entry("seckillSkus")){ //自定义资源名
            //受保护的资源
  }catch (BlockException e){
      log.error("资源被限流",e.getMessage());
  }

332.2 基于注解自定义资源

Sentinel支持通过@SentinelResource注解定义资源并配置blockHandler 和 fallback 函数来进行限流之后的处理.

blockHandler函数会在原方法被限流/降级/系统保护的时候调用 , 而fallback函数会针对所有类型的异常 , 请注意 blockHander 和 fallback函数的形式要求.

@Override
@SentinelResource(value = "getCurrentSeckillSkusResource",
                  blockHandler = "blockHandler",//降级方法名
               	  fallback = xxxx.class
                 ) 
public List<SecKillSkuRedisTo> getCurrentSeckillSkus(){
    //......
}

/**
  * 返回当前时间可以参与秒杀商品信息降级
  */
public List<SecKillSkuRedisTo> blockHandler(BlockException e){
    log.error("getCurrentSeckillSkusResource被限流降级了");
    return null;
}

332.3 配置异常返回

无论那种方式一定要配置被限流后的默认返回.

url请求可以设置统一返回.

333.网关流控

好处 : 流控失败时都不需要走到服务 , 直接在网关拦截

Sentinel 提供了 Spring Cloud Gateway 的适配模块 , 可以提供两种资源维度的限流.

  • route维度 : 即在Spring配置文件在配置的路由条目 , 资源名为对应的routeid

  • 自定义API维度 : 用户可以利用Sentinel提供的API来自定义一些API分组

第一步 : 引入依赖

 <!--sentinel gateway-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        </dependency>

第二步 : 启动控制台

 路由名即为资源名

 

 

333.1 针对属性请求

 

333.2 API分组

 

333.3 自定义返回数据

 

 可以在 GatewayCallbackManager 注册回调进行定制:

@Configuration
public class SentinelGatewayConfig {

    public SentinelGatewayConfig(){
        GatewayCallbackManager.setBlockHandler(new BlockRequestHandler() {
            @Override
            public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {

                R error = R.error(BizCodeEnume.TOO_MANY_REQUEST.getCode(), BizCodeEnume.TOO_MANY_REQUEST.getMessage());
                String errJSON = JSON.toJSONString(error);
                Mono<ServerResponse> body = ServerResponse.ok().body(Mono.just(errJSON), String.class);
                return body;
            }
        });
    }
}

335 Sleuth-链路追踪

335.1 基本概念

见P335

335.2 Spring整合Sleuth

 第一步 : 服务

<!--sleuth-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

提供者与消费者导入依赖 (common) 

logging:
  level:
	 org.springframework.cloud.openfeign: debug
	 org.springframework.cloud.sleuth: debug

 第三步 : 发起一次远程调用,观察控制台 

DEBUG [user-service,541450f08573fff5,541450f08573fff5,false]

user-service:服务名

541450f08573fff5:是 TranceId,一条链路中,只有一个

TranceId 541450f08573fff5:是 spanId,链路中的基本工作单元 id

false:表示是否将数据输出到其他服务,true 则会把信息输出到其他可视化的服务上观察

336.Sleuth-整合Zipkin 链路追踪

第一步 : docker安装zipkin服务器 

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

第二步 : 导入依赖

<!--zipkin-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

zipkin 依赖也同时包含了 sleuth,可以省略 sleuth 的引

第三步 : 添加配置

spring:
 application:
  name: user-service
 zipkin:
  base-url: http://192.168.56.10:9411/ # zipkin 服务器的地址
  # 关闭服务发现,否则 Spring Cloud 会把 zipkin 的 url 当做服务名称
 discoveryClientEnabled: false
 保存:
  type: web # 设置使用 http 的方式传输数据
sleuth:
 sampler:
  probability: 0.5 # 设置抽样采集率为 100%,默认为 0.1,即 10%

336.1 Zipkin 数据持久化

保存再es中

docker run --env STORAGE_TYPE=elasticsearch --env ES_HOSTS=192.168.56.10:9200
openzipkin/zipkin-dependencies

知秋君
上一篇 2024-08-02 18:36
下一篇 2024-08-02 18:02

相关推荐