当前位置:   article > 正文

谷粒商城-分布式高级篇【业务编写】_谷粒商城 分布式怎么做的

谷粒商城 分布式怎么做的
  1. 谷粒商城-分布式基础篇【环境准备】
  2. 谷粒商城-分布式基础【业务编写】
  3. 谷粒商城-分布式高级篇【业务编写】持续更新
  4. 谷粒商城-分布式高级篇-ElasticSearch
  5. 谷粒商城-分布式高级篇-分布式锁与缓存
  6. 项目托管于gitee

分布式高级篇[业务编写]

这里做个讲解,我的mac系统更新到12.3 ,查看PID:控制中心占用了7000端口,故自此把gulimall-coupon 服务的端口改为 7001
谷粒商城-分布式高级篇-ElasticSearch


一、商品上架

1.1、product-es准备


ES在内存中,所以在检索中优于mysql。ES也支持集群,数据分片存储。

需求:

  • 上架的商品才可以在网站展示。
  • 上架的商品需要可以被检索。
1.1.1、分析sku在es中如何存储

分析sku在es中如何存储

商品mapping

分析:商品上架在es中是存sku还是spu?

1)、检索的时候输入名字,是需要按照sku的title进行全文检索的
2)、检素使用商品规格,规格是spu的公共属性,每个spu是一样的
3)、按照分类id进去的都是直接列出spu的,还可以切换。
4〕、我们如果将sku的全量信息保存到es中(包括spu属性〕就太多字段了

  • 方案1:

    {
        skuId:1
        spuId:11
        skyTitile:华为xx
        price:999
        saleCount:99
        attr:[
            {尺寸:5},
            {CPU:高通945},
            {分辨率:全高清}
    	]
    缺点:如果每个sku都存储规格参数(如尺寸),会有冗余存储,因为每个spu对应的sku的规格参数都一样
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
  • 方案2:

    sku索引
    {
        spuId:1
        skuId:11
    }
    attr索引
    {
        skuId:11
        attr:[
            {尺寸:5},
            {CPU:高通945},
            {分辨率:全高清}
    	]
    }
    先找到4000个符合要求的spu,再根据4000个spu查询对应的属性,封装了4000个id,long 8B*4000=32000B=32KB
    1K个人检索,就是32MB
    
    结论:如果将规格参数单独建立索引,会出现检索时出现大量数据传输的问题,会引起网络网络
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

因此选用方案1,以空间换时间

1.1.2、建立product索引

最终选用的数据模型:

  • { “type”: “keyword” }, # 保持数据精度问题,可以检索,但不分词
  • “analyzer”: “ik_smart” # 中文分词器
  • “index”: false, # 不可被检索,不生成index
  • “doc_values”: false # 默认为true,不可被聚合,es就不会维护一些聚合的信息
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": {			# 商品的库存 true:有库存 false:无库存
        "type": "boolean"
      },
      "hotScore": {			# 热度评分
        "type": "long"
      },
      "brandId": {			# 品牌id
        "type": "long"
      },
      "catalogId": {		# 分类id
        "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": {				# 属性的id
            "type": "long"
          },
          "attrName": {				# 属性的名字
            "type": "keyword",
            "index": false,
            "doc_values": false
          },
          "attrValue": {			# 属性的值
            "type": "keyword"
          }
        }
      }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

冗余存储的字段:不用来检索,也不用来分析,节省空间

  • 库存是bool。
  • 检索品牌id,但是不检索品牌名字、图片
  • 用skuTitle检索
1.1.3、nested 嵌入式对象

属性是"type": “nested”,因为是内部的属性进行检索

数组类型的对象会被扁平化处理(对象的每个属性会分别存储到一起)

user.name=["aaa","bbb"]
user.addr=["ccc","ddd"]

这种存储方式,可能会发生如下错误:
错误检索到{aaa,ddd},这个组合是不存在的
  • 1
  • 2
  • 3
  • 4
  • 5

数组的扁平化处理会使检索能检索到本身不存在的,为了解决这个问题,就采用了嵌入式属性,数组里是对象时用嵌入式属性(不是对象无需用嵌入式属性)

1.2、SpringBoot整合ES


创建模块: gulimall-search

选择依赖web,但不要在里面选择es、openfeign。 并做降版本处理

1.2.1、注册进入注册中心中

gulimall-search 服务加入注册中心nacos中去

  1. 导入依赖
<dependency>
    <groupId>com.hgw.gulimall</groupId>
    <artifactId>gulimall-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  1. 配置nacos注册中心信息

application.properties:

spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.application.name=gulimall-search
  • 1
  • 2
1.2.2、服务整合ES

第一步、导入依赖 这里的版本要和所按照的ELK版本匹配

<dependency>
  <groupId>org.elasticsearch.client</groupId>
  <artifactId>elasticsearch-rest-high-level-client</artifactId>
  <version>7.4.2</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5

在spring-boot-dependencies中所依赖的ES版本位6.8.4,要改掉

<properties>
    <java.version>1.8</java.version>
    <elasticsearch.version>7.4.2</elasticsearch.version>
</properties>
  • 1
  • 2
  • 3
  • 4

编写配置,给容器中注入一个 RestHighLevelClient

com.hgw.gulimall.search.config包下创建一个ES的配置类

请求测试项,比如es添加了安全访问规则,访问es需要添加一个安全头,就可以通过requestOptions设置

官方建议把requestOptions创建成单实例

@Configuration
public class GulimallElasticSearchConfig {
    public static final RequestOptions COMMON_OPTIONS;

    static {
        RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
        COMMON_OPTIONS = builder.build();
    }

    @Bean
    public RestHighLevelClient esRestClient() {
        RestClientBuilder builder = null;
        // 可以指定多个es
        builder = RestClient.builder(new HttpHost("124.222.223.222",9200,"http"));
        RestHighLevelClient client = new RestHighLevelClient(builder);
        return client;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

此外还有多种方法

1.3、商品上架


POST: /product/spuinfo/{spuId}/up

按skuId上架

第一步、在 SpuInfoController 类中编写商品上架Controller层的实现方法

第一步、在 SpuInfoController 类中编写商品上架Controller层的实现方法

@RestController
@RequestMapping("product/spuinfo")
public class SpuInfoController {
    @Autowired
    private SpuInfoService spuInfoService;

    /**
     * 商品上架
     * /product/spuinfo/{spuId}/up
     * @param spuId
     * @return
     */
    @PostMapping("/{spuId}/up")
    public R spuUp(@PathVariable("spuId") Long spuId){
        spuInfoService.up(spuId);
        return R.ok();
    }
  // ......
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
第二步、编写上架实体类

第二步、编写上架实体类

product里组装好,search里上架

商品上架需要在es中保存spu信息并更新spu的状态信息,由于SpuInfoEntity与索引的数据模型并不对应,所以我们要建立专门的vo进行数据传输。

gulimall-common 服务下的

@Data
public class SkuEsModel {
    private Long skuId;
    private Long spuId;
    private String skuTitle;
    private BigDecimal skuPrice;
    private String skuImg;
    private Long saleCount;     // 商品的销量
    private boolean hasStock;   // 商品的库存
    private Long hotScore;      // 热度评分
    private Long brandId;       // 品牌ID
    private Long catalogId;     // 分类ID
    private String brandName;   // 品牌名
    private String brandImg;    // 品牌图片
    private String catalogName; //分类的名字
    private List<Attr> attrs;

    /**
     * 商品规格
     */
    @Data
    public static class Attr{
        private Long attrId;       // 属性id
        private String attrName;    // 属性名
        private String attrValue;   // 属性值
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
第三步、编写 查处当前spuId对应的所有sku信息 的实现方法

第三步、编写 查处当前spuId对应的所有sku信息 的实现方法

SkuInfoServiceImpl 实现类下 :

/**
 * 根据spuID查询spuId对应的所有sku信息
 * @param spuId
 * @return
 */
@Override
public List<SkuInfoEntity> getSkusBySpuId(Long spuId) {
    List<SkuInfoEntity> list = this.list(new QueryWrapper<SkuInfoEntity>().eq("spu_id", spuId));
    return list;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
第四步、构造sku检索属性

第四步、构造sku检索属性

//TODO 4、查询当前sku的所有可以被检索的规格属性
List<ProductAttrValueEntity> baseAttrs = attrValueService.baseAttrListForSpu(spuId);
List<Long> attrIds = baseAttrs.stream().map(attr -> {
    return attr.getAttrId();
}).collect(Collectors.toList());
// 过滤掉可以被检索检索的规格属性id
List<Long> searchAttrIds =  attrValueService.selectSearchAttrsIds(attrIds);
Set<Long> idSet = new HashSet<>(searchAttrIds);

List<SkuEsModel.Attr> attrsList = baseAttrs.stream().filter(item -> {
    return idSet.contains(item.getAttrId());
}).map(item -> {
    SkuEsModel.Attr attr = new SkuEsModel.Attr();
    BeanUtils.copyProperties(item, attr);
    return attr;
}).collect(Collectors.toList());
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

updateSpuAttr类中

/**
 * 在指定的所有属性集合里面,挑出检索属性
 * SELECT attr_id FROM pms_attr WHERE attr_id IN(?) AND search_type = 1;
 * @param attrIds
 * @return
 */
@Override
public List<Long> selectSearchAttrsIds(List<Long> attrIds) {
    return this.baseMapper.selectSearchAttrsIds(attrIds);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

src/main/resources/mapper/product/ProductAttrValueDao.xml

<select id="selectSearchAttrsIds" resultType="java.lang.Long">
    SELECT attr_id FROM pms_attr WHERE attr_id IN
     <foreach collection="attrIds" item="id" separator="," open="(" close=")">
         #{id}
     </foreach>
     AND search_type = 1;
</select>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
第五步、库存量查询

上架要确保还有库存

1. 在ware微服务里添加"查询sku是否有库存"的controller

在ware微服务里添加"查询sku是否有库存"的controller

  1. WareSkuController
@RestController
@RequestMapping("ware/waresku")
public class WareSkuController {
    @Autowired
    private WareSkuService wareSkuService;

    // 查询sku是否有库存
    @PostMapping("/hasstock")
    public R getSkusHasStock(@RequestBody List<Long> skuIds){
        // sku_id,stock
        List<SkuHasStockVo> vos = wareSkuService.getSkusHasStock(skuIds);

        return R.ok().setData(vos);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  1. WareSkuServiceImpl
@Override
public List<SkuHasStockVo> getSkusHasStock(List<Long> skuIds) {
    List<SkuHasStockVo> collect = skuIds.stream().map(skuId -> {
        SkuHasStockVo vo = new SkuHasStockVo();
        // 查询当前sku的总库存量
        // SELECT SUM(stock-stock_locked) FROM wms_ware_sku WHERE sku_id=1
        Long count =  baseMapper.getSkuStock(skuId);
        vo.setSkuId(skuId);
        vo.setHasStock(count>0?true:false);
        return vo;
    }).collect(Collectors.toList());
    return collect;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  1. WareSkuDao
@Mapper
public interface WareSkuDao extends BaseMapper<WareSkuEntity> {

    void addStock(@Param("skuId") Long skuId, @Param("wareId") Long wareId, @Param("skuNum") Integer skuNum);

    Long getSkuStock(@Param("skuId") Long skuId);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

WareSkuDao.xml

<select id="getSkuStock" resultType="java.lang.Long">
    SELECT SUM(stock-stock_locked) FROM wms_ware_sku WHERE sku_id=#{skuId}
</select>
  • 1
  • 2
  • 3
2、在gulimall-product模块使用feign远程调用该接口

  1. 设置R,加上setData()getData()方法
public class R extends HashMap<String, Object> {
   private static final long serialVersionUID = 1L;

   public R setData(Object data){
      put("data", data);
      return this;
   }

   /**
    * 利用fastjson进行逆转
    * @param typeReference
    * @param <T>
    * @return
    */
   public <T> T getData(TypeReference<T> typeReference){
      Object data = get("data"); // 默认是map
      String s = JSON.toJSONString(data);
      T t = JSON.parseObject(s, typeReference);
      return t;
   }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  1. 在 gulimall-product模块 中编写feign接口service
@FeignClient("gulimall-ware")
public interface WareFeignService {

    /**
     * 1、R设计的时候可以加上泛型
     * 2、直接返回我们想要的结果
     * 3、自己封装解析结果
     * 查询sku是否有库存
     * @param skuIds
     * @return
     */
    @PostMapping("/ware/waresku/hasstock")
    R<List<SkuHasStockVo>> getSkusHasStock(@RequestBody List<Long> skuIds);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  1. up方法进行调用
Map<Long, Boolean> stockMap = null;
try{
    R<List<SkuHasStockVo>> skusHasStock = wareFeignService.getSkusHasStock(skuIdList);
    stockMap = skusHasStock.getData().stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
} catch (Exception e){
    log.error("库存服务查询异常,原因:{}",e);
}
Map<Long, Boolean> finalStockMap = stockMap;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

自此商品上架方法的数据都封装好了

    /**
     * 商品上架
     * @param spuId
     */
    @Override
    public void up(Long spuId) {

        // 1、查找当前spuId对应的所有sku信息
        List<SkuInfoEntity> skus = skuInfoService.getSkusBySpuId(spuId);
        List<Long> skuIdList = skus.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());

        //TODO 4、查询当前sku的所有可以被检索的规格属性
        List<ProductAttrValueEntity> baseAttrs = attrValueService.baseAttrlistforspu(spuId);
        List<Long> attrIds =baseAttrs.stream().map(attr->{
            return  attr.getAttrId();
        }).collect(Collectors.toList());
        // 过滤掉可以被检索检索的规格属性id
        List<Long> searchAttrIds = attrService.selectSearchAttrIds(attrIds);
        Set<Long> idSet = new HashSet<>(searchAttrIds);
        // 将所有的规格属性 通过 被检索的规格属性id 过滤出来我们想要的  sku的所有可以被检索的规格属性
        List<SkuEsModel.Attr> attrsList = baseAttrs.stream().filter(item -> {
            return idSet.contains(item.getAttrId());
        }).map(item -> {
            SkuEsModel.Attr attr = new SkuEsModel.Attr();
            BeanUtils.copyProperties(item, attr);
            return attr;
        }).collect(Collectors.toList());


        //TODO 1、发送远程调用,库存系统查询是否有库存
        Map<Long, Boolean> stockMap = null;
        try{
            R r = wareFeignService.getSkusHasStock(skuIdList);
            // 封装成map,下面只需要通过skuId查询是否有库存
            TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>() {
            };
            stockMap = r.getData(typeReference).stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
        }catch (Exception e) {
            log.error("库存服务查询异常:原因{}",e);
        }
        Map<Long, Boolean> finalStockMap = stockMap;


        // 2、封装每个sku的信息
        List<SkuEsModel> upProducts = skus.stream().map(sku -> {
            // 组装需要的数据
            SkuEsModel esModel = new SkuEsModel();
            BeanUtils.copyProperties(sku, esModel);

            esModel.setSkuPrice(sku.getPrice());
            esModel.setSkuImg(sku.getSkuDefaultImg());

            // 1、发送远程调用,库存系统查询是否有库存
            if (finalStockMap == null) {
                esModel.setHasStock(true);
            } else {
                esModel.setHasStock(finalStockMap.get(sku.getSkuId()));
            }


            //TODO 2、热度评分。刚上架:0
            esModel.setHotScore(0L);

            //TODO 3、品牌和分类的名字信息,品牌的logo
            BrandEntity brand = brandService.getById(sku.getBrandId());
            esModel.setBrandName(brand.getName());
            esModel.setBrandImg(brand.getLogo());
            CategoryEntity category = categoryService.getById(sku.getCatalogId());
            esModel.setCatalogName(category.getName());


            // 4、查询当前sku的所有可以被检索的规格属性(方法见TODO4)
            esModel.setAttrs(attrsList);

            return esModel;
        }).collect(Collectors.toList());

        //TODO 5、将数据发送给es进行保存 gulimall-search

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
第六步、远程上架接口

  1. 在 gulimall-search 服务中创建 ElasticSearchSaveController 类
@Slf4j
@RequestMapping("/search/save")
@RestController
public class ElasticSaveController {
    @Autowired
    ProductSaveService productSaveService;

    /**
     * 上架商品
     * @param skuEsModels
     * @return
     */
    @PostMapping("/product")
    public R productStatsUp(@RequestBody List<SkuEsModel> skuEsModels) {
        boolean b = false;
        try{
            b = productSaveService.productStatsUp(skuEsModels);
        }catch (Exception e){
            log.error("ElasticSaveController商品上架错误: {}",e);
            return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(),BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());
        }

        if (!b) {
            return R.ok();
        } else {
            return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(),BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  1. 编写其Service层的实现方法
@Slf4j
@Service
public class ProductSaveServiceImpl implements ProductSaveService {

    @Autowired
    RestHighLevelClient restHighLevelClient;

    @Override
    public boolean productStatsUp(List<SkuEsModel> skuEsModels) throws IOException {
        // 保存到es

        // 1、给es中建立索引并建立好映射关系(我们通过Kibana完成)
        // 2、给es中保存这些数据
        // BulkRequest bulkRequest, RequestOptions options
        BulkRequest bulkRequest = new BulkRequest();
        for (SkuEsModel model : skuEsModels) {
            // 构造保存请求
            IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
            indexRequest.id(model.getSkuId().toString());
            String modelJson = JSON.toJSONString(model);
            indexRequest.source(modelJson, XContentType.JSON);
            bulkRequest.add(indexRequest);
        }
        // 发起请求
        BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);

        //TODO 如果批量错误
        boolean b = bulk.hasFailures(); // true:有错误  false:无错误
        List<String> collect = Arrays.stream(bulk.getItems()).map(item -> {
            return item.getId();
        }).collect(Collectors.toList());
        log.info("商品上架完成:{},返回数据:{}",collect,bulk.toString());

        return b;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  1. 在 gulimall-product 服务中创建远程调用Feigin接口
@FeignClient("gulimall-search")
public interface SearchFeignService {

    // 上架商品
    @PostMapping("/search/save/product")
    R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  1. 在商品上架方法中完成 将数据发送给es进行保存 功能
//TODO 5、将数据发送给es进行保存 gulimall-search
R r = searchFeignService.productStatusUp(upProducts);
if (r.getCode() == 0){
  // 远程调用成功
  // TODO 6、远程调用成功 修改当前SPU的状态为上架
  baseMapper.updateSpuStatus(spuId, ProductConstant.StatusEnum.SPU_UP.getCode());
} else {
  // 远程调用失败
  // TODO 7、重复调用?接口幂等性;重试机制?
  log.error("商品远程es保存失败");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  1. 编写 远程调用成功 修改当前SPU的状态为上架的方法
@Mapper
public interface SpuInfoDao extends BaseMapper<SpuInfoEntity> {

    void updateSpuStatus(@Param("spuId") Long spuId, @Param("code") int code);

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

SpuInfoDao.xml:

<update id="updateSpuStatus">
    UPDATE pms_spu_info SET publish_status=#{code},update_time=NOW() WHERE id=#{spuId}
</update>
  • 1
  • 2
  • 3

在这里插入图片描述

此时查看ES中也存入了数据 !



二、首页


在这里插入图片描述
自此写到哪个功能时,再导入相应的服务前端代码

这里对 gulimall-product 服务目录进行了调整:

  • 将原来的 controller包名改成了 app ,后台管理的操作
  • 创建一个 web 包,用来写前台的Controller

2.1、整合 thymeleaf 渲染首页

第一步、导入依赖

在 gulimall-product 服务中导入:

  • thymeleaf 依赖
  • devtools 依赖实现 页面修改时不重启服务器实时更新,热部署
    1. 引入 devtools依赖
    2. 修改完页面后 使用快捷键 command+shift+F9 即可重新自动编译页面
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--页面修改时不重启服务器实时更新,热部署-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <optional>true</optional>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

第二步、将前端页面复制进项目

将资料中的html>首页资源下的内容复制到 gulimall-product 服务中

  1. index文件夹复制到 src/main/resources/static路径下 (静态资源都放在 static 文件下就可以按照路径直接访问)
  2. index.html 文件复制到 src/main/resources/templates 路径下

第三步、配置文件关闭缓存

spring:
  thymeleaf:
    cache: false  # 关闭缓存
  • 1
  • 2
  • 3

2.2、整合dev-tools 渲染一集分类数据


需求:访问 http://127.0.0.1:10000/index.html 来到首页,来到首页时并渲染一集分类

2.2.1、接口编写

  1. Controller 层编写
    web 目录下创建并编写 IndexController
@Controller
public class IndexController {

    @Autowired
    CategoryService categoryService;

    @GetMapping({"/","/index.html"})
    public String indexPage(Model model){
        // TODO 1、查找所有的一级分类
        List<CategoryEntity> categoryEntities = categoryService.getLevel1Categorys();
        // 视图解析器进行拼串
        // 前缀:"classpath:/templates/"  后缀:".html"
        model.addAttribute("categorys", categoryEntities);
        return "index";
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  1. Service层编写
    在 CategoryServiceImpl 实现类下编写查找所有的一集分类方法
@Override
public List<CategoryEntity> getLevel1Categorys() {
    List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
    return categoryEntities;
}
  • 1
  • 2
  • 3
  • 4
  • 5
2.2.2、前端修改

  1. 引入 thymeleaf 名称空间
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  • 1
  1. 修改渲染
    修改 header_main >header_banner>header_main_left盒子下的ul内容,通过Thymeleaf语法进行渲染
<ul>
  <li th:each="category : ${categorys}">
    <a href="#" class="header_main_left_a" th:attr="ctg-data=${category.catId}">
      <b th:text="${category.name}"></b>
    </a>
  </li>
</ul>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

效果展示:
在这里插入图片描述

2.3、渲染二级、三级分类数据


需求:当鼠标滑到一级菜单的时候显示二级、三级分类数据

2.3.1、后端接口

  1. 第一步、编写 二级分类、三级分类VO

com/atguigu/gulimall/product/vo目录下:

@NoArgsConstructor
@AllArgsConstructor
@Data
public class Catelog2Vo {
    private String catalog1Id;      // 父分类的id(一级分类)
    private List<Catelog3Vo> catalog3List;  // 三级子分类
    private String id;
    private String name;

    /**
     * 三级分类 Vo
     */
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public static class Catelog3Vo{
        private String catalog2Id;      // 父分类的id(二级分类)
        private String id;
        private String name;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  1. 第二步、编写Controller层方法
@Controller
public class IndexController {

    @Autowired
    CategoryService categoryService;

    /**
     * 渲染二级、三级分类
     * @return
     */
    @ResponseBody
    @GetMapping({"/index/catalog.json"})
    public Map<String, List<Catelog2Vo>> getCatalogJson(){
        Map<String, List<Catelog2Vo>> catalogJson = categoryService.getCatalogJson();
        return catalogJson;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  1. 第三步、编写 CategoryService 的实现方法
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
    // 1、查出所有分类
    List<CategoryEntity> level1Categorys = this.getLevel1Categorys();

    // 2、封装数据
    Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
        // 2.1、每一个一集分类,查到这个一集分类的所有二级分类
        List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
        // 2.2、封装上面的结果
        List<Catelog2Vo> catelog2Vos = null;

        if (categoryEntities != null) {
            catelog2Vos = categoryEntities.stream().map(l2 -> {
                Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());

                // 2.3、找当前二级分类的三级分类封装成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 -> {
                        // 封装成指定格式
                        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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
2.3.2、前端修改

修改静态资源:src/main/resources/static/index/js 路径下的 catalogLoader.js 文件的请求路径

$(function(){
    $.getJSON("index/catalog.json",function (data) {

        var ctgall=data;
  • 1
  • 2
  • 3
  • 4

测试成功:

在这里插入图片描述



三、Nginx 搭建域名访问

域名映射效果:

  • 请求接口:gulimall.com
  • 请求页面:gulimall.com

nginx 直接代理给网关,网关进行判断

  • 如果是/api/***,转交给对应的服务器
  • 如果是 满足域名,转交给对应的服务

在这里插入图片描述

3.1、Nginx搭建域名反向代理


由于本人使用的是腾讯云服务器,Nginx跑在云服务器上,这里通过使用花生壳内网穿透 + 腾讯云 Nginx 来实现:

  1. 修改本地 /etc/hosts
# h
124.222.223.222 gulimall.com
  • 1
  • 2

在这里插入图片描述

  1. 本地项目启动,通过花生壳进行内网穿透将 本地的1000:端口映射出去

在这里插入图片描述

  1. 修改Nginx配置文件:
  • 复制 /mydata/nginx/conf/conf.d 路径下的 default.conf 文件,并重命名为 gulimall.conf

  • 修改 /mydata/nginx/conf/conf.d 路径下的 gulimall.conf

  • **

server {
    listen       80;
    server_name  gulimall.com;

    #charset koi8-r;
    #access_log  /var/log/nginx/log/host.access.log  main;

    location / {
        proxy_pass http://m374k82881.qicp.vip:80;
    }
		# ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在这里插入图片描述

  1. 重启 nginx容器
[root@hgwtencent conf.d]# docker restart nginx
  • 1

3.2、Nginx 搭建域名访问环境


3.2.1、启动本地服务,将 gulimall-gate 内网穿透出去

在这里插入图片描述

3.2.2、配置上游服务器

修改 /mydata/nginx/conf 路径下的 nginx.conf 配置文件

upstream gulimall{
		server m374k82881.qicp.vip:16059;
}
  • 1
  • 2
  • 3

在这里插入图片描述

诊断域名:m374k82881.qicp.vip 域名IP地址指向:103.46.128.53 转发服务器IP103.46.128.53 域名已激活内网穿透功能,并与转发服务器IP指向一致 连接转发服务器成功   映射:m374k82881.qicp.vip:16059 局域网服务器:127.0.0.1:88 本机内网IP192.168.3.222 局域网服务器连接成功
  • 1
3.2.3、配置域名代理

修改 /mydata/nginx/conf/conf.d 路径下的 gulimall.conf 配置文件

server {
    listen       80;
    server_name  gulimall.com;

    #charset koi8-r;
    #access_log  /var/log/nginx/log/host.access.log  main;

    location / {
        proxy_set_header Host $host;
        proxy_pass http://gulimall;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在这里插入图片描述

修改完配置文件进行重启nginx容器服务:

docker restart nginx
  • 1
3.3.3、配置网关

配置 gulimall-gateway 服务下的 application.yml 配置文件,加上以下网关配置

- id: gulimall_host_route
  uri: lb://gulimall-product
  predicates:
    - Host=**.gulimall.com
  • 1
  • 2
  • 3
  • 4

测试成功

在这里插入图片描述
本人在最后还是选择了 在本地小鲸鱼中跑了个Nginx来模拟域名访问。



四、性能压测

压力测试笔记

影响性能考虑包括:

  • 数据库、应用程序、中间件、网络和操作系统等方面

首先考虑自己的应用属于CPU密集型还是IO密集型

4.1、堆内存与垃圾回收

在这里插入图片描述

4.2、性能监控

jdk 的两个小工具 jconsole、jvisualvm(升级版的 jconsole);通过命令启动,可监控本地和远程应用。远程应用需要配置

  • jconsole
  • jvisualvm

运行状态:

  • 运行:正在运行
  • 休眠:sleep
  • 等待:wait
  • 驻留:线程池里面的空闲线程
  • 监视:阻塞的线程,正在等待锁

要监控GC,安装插件:工具-插件。可用插件-检查最新版本 报错的时候百度“插件中心”,改个JVM对应的插件中心url.xml.z

安装visual GC

4.3、监控指标

  • SQL 耗时越小越好,一般情况下微妙级别
  • 命中率越高越好,一般情况下不能低于 95%
  • 锁等待次数越低越好,等待时间越短越好

根据视频做的测试:

压测内容压测线程数吞吐量/s90%响应时间99%响应时间
Nginx5063827746
Gateway5043,381211
简单服务5037,67424
首页一级菜单渲染(关闭缓存)50130(db,thymeleaf)426721
首页一级菜单渲染(开启缓存、优化数据库、关日志)5098322346
三级分类数据获取5045/min(db)94,42894,641
首页全量数据获取501.6(静态资源)3001230023
Nginx+Gateway50
Gateway+简单服务5014,493535
全链路5055474100
  • 中间件越多,性能损失越大,大多都损失在网络交互了
  • 业务
    • Db (MySQL 优化)
    • 模板的渲染速度 (缓存)
    • 静态资源
4.4、Nginx动静分离 【优化】

在这里插入图片描述

  1. 以后将所有项目的静态资源放在Nginx里面
  2. 规则:/static/** 所有请求都由Nginx直接返回

第一步、给静态资源搬家

  1. 在服务器的 /mydata/nginx/html 路径下创建一个 static 文件夹,用来存放我们的静态资源
  2. 将项目中 gulimall-product 服务中 src/main/resources/static目录下的 index文件夹下所有内容 剪切到 服务器的 /mydata/nginx/html/static 目录下
    在这里插入图片描述

**第二步、**替换本地的index.html中静态资源的请求路径全部加上 /static/

  • href=" 替换 href="/static/

在这里插入图片描述

  • <script src=" 替换 <script src="/static/
    在这里插入图片描述

  • <img src=" 替换 <img src="/static/

在这里插入图片描述

第三步、配置Nginx

    location /static/ {
        root /usr/share/nginx/html;
    }
  • 1
  • 2
  • 3

在这里插入图片描述

测试,进去感觉明显快了

在这里插入图片描述

4.5、配置内存和新生代【优化】

-Xmx1024m -Xms1024m -Xmn512m
<img src="/static/index/img/5a154759N5385d5d6.jpg" />
  • 1
  • 2

在这里插入图片描述

4.6、优化三级分类数据获取【优化】

修改实现类 CategoryServiceImpl 的方法,将数据库的多次查询变为一次

/**
 * 封装一级、二级、三级分类
 * @return
 */
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
    /**
     * 1、将数据库的多次查询变为一次
     */
    List<CategoryEntity> selectList = baseMapper.selectList(null);

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

    // 2、封装数据
    Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
        // 2.1、每一个一集分类,查到这个一集分类的所有二级分类
        List<CategoryEntity> categoryEntities = getParent_cid(selectList,v.getCatId());
        // 2.2、封装上面的结果
        List<Catelog2Vo> catelog2Vos = null;

        if (categoryEntities != null) {
            catelog2Vos = categoryEntities.stream().map(l2 -> {
                Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());

                // 2.3、找当前二级分类的三级分类封装成vo
                List<CategoryEntity> level3Catelog = getParent_cid(selectList,l2.getCatId());
                if (level3Catelog!=null) {
                    List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                        // 封装成指定格式
                        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;
}

private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList, Long parent_cid) {
    List<CategoryEntity> collect = selectList.stream().filter(item -> item.getParentCid() == parent_cid).collect(Collectors.toList());
    return collect;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49


五、分布式与缓存


业务笔记写到了别处:谷粒商城-分布式高级篇-分布式锁与缓存

Redis 笔记:Redis



六、商城业务


七、Sentinel服务流控、熔断和降级

此处笔记写到别处: Sentinel服务流控、熔断和降级


八、Sleuth+ZipKin 服务链路追踪

8.1、基本概念


8.1.1、为什么用?

微服务架构是一个分布式架构,它按业务划分服务单元,一个分布式系统往往有很多介服务单元。由于服务单元数量众多,业务的复杂性,如果出现了错误和异常,很难去定位。主要体现在,一个请求可能需要调用很多个服务,而内部服务的调用复杂性,决定了问题难以定位。所以微服务架构中,必须实现分布式链路追踪,去跟进一个请求到底有 哪些服务参与,参与的顺序又是怎样的,从而达到每个请求的步骤清哳可见,出了问題,很快定位。

链路追踪組件有 Google 的 Dapper, Twitter 的Zpkin,以及阿里的 Eagleeve(鹰眼)等,它们都是非常优秀的链路追踪开源组件。

8.1.2、基本术语
  • Span:基本工作单元,例如,在一个新建的span中发送一个RPC等同于发送一个回应请求给RPC,span通过一个64位ID唯一标识,trace以另一个64位ID表示,span还有其他数据信息,比如摘要、时间戳事件、关键值注释(tags)、span的ID、以及进度ID(通常是IP地址)
    span在不断的启动和停止,同时记录了时间信息,当你创建了一个span,你必须在未来的某个时刻停止它。
  • Trace:一系列spans组成的一个树状结构,例如,如果你正在跑一个分布式工程,你可能需要创建一个trace。
  • Annotation:用来及时记录一个事件的存在,一些核心annotations用来定义一个请求的开始和结束
  • cs - Client Sent -客户端发起一个请求,这个annotion描述了这个span的开始
  • sr - Server Received -服务端获得请求并准备开始处理它,如果将其sr减去cs时间戳便可得到网络延迟
  • ss - Server Sent -注解表明请求处理的完成(当请求返回客户端),如果ss减去sr时间戳便可得到服务端需要的处理请求时间
  • cr - Client Received -表明span的结束,客户端成功接收到服务端的回复,如果cr减去cs时间戳便可得到客户端从服务端获取回复的所有所需时间

8.2、SpringBoot 整合 Sleuth+ZipKin 服务链路追踪


第一步、在gulimall-common服务中导入依赖

  1. 导入SpringCloud的版本依赖
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Greenwich.SR3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  1. 导入 spring-cloud-starter-sleuth
<!--链路追踪-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5

第二步、Docker安装 zipkin 服务器

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

第三步、引入zipkin依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4

因为 zipkin 同时包含了sleuth,所以将上面sleuth的依赖删掉

第四步、为每个服务都配置依赖

# zipkin服务器的地址
spring.zipkin.base-url=http://127.0.0.1:9411/
# 关闭服务器发现,否则SpringCloud会把 zipkin 的 url当作服务器名称
spring.zipkin.discovery-client-enabled=false
# 设置使用 http 的方式传输数据
spring.zipkin.sender.type=web
# 设置抽样采集率为100%,默认为0.1%,即10%
spring.sleuth.sampler.probability=1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

进行一次购物活动,查看 http://localhost:9411/

在这里插入图片描述

好啦,就陪大家到这里了!刷题背题去咯~

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小惠珠哦/article/detail/871217
推荐阅读
相关标签
  

闽ICP备14008679号