谷粒商城笔记+踩坑(9)——上架商品spu到ES索引库

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
简介: ES回顾、【查询模块】保存ES文档、【库存模块】库存量查询、【商品模块】上架单个spu

 导航:

谷粒商城笔记+踩坑汇总篇

Java笔记汇总:

【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析-CSDN博客

目录

1、ES回顾

2、ES整合商品上架

2.1、分析

2.2、创建sku的es索引库

2.2.1、两种索引库设计方案分析

2.2.2、最终选用的索引库方案,nested类型

2.3、SkuEsModel模型类

2.4、【库存模块】库存量查询

2.5、【查询模块】保存ES文档

2.5.1、常量类

2.5.2、controller

2.5.3、service

2.6、【商品模块】上架单个spu

2.6.1、controller

2.6.2、远程调用库存模块

2.6.3、【公共模块】商品常量类,添加上传成功或失败的状态码

2.6.4、service

2.6.5、测试

2.6.6、修改结果类R


1、ES回顾

elasticsearch基础1——索引、文档_elasticsearch索引 文档_vincewm的博客-CSDN博客

elasticsearch基础2——DSL查询文档,黑马旅游项目查询功能_elasticsearch查询文档_vincewm的博客-CSDN博客elasticsearch基础3——聚合、补全、集群_vincewm的博客-CSDN博客elasticsearch基础2——DSL查询文档,黑马旅游项目查询功能_elasticsearch查询文档_vincewm的博客-CSDN博客

2、ES整合商品上架

2.1、分析

es在整个项目中的应用:

1.对商品的全文检索功能

2.对日志的全文检索功能

image.gif

image.gif

为什么不用mysql?

es比mysql检索功能强大,并且对于庞大的检索数据,es性能更好。

因为mysql是存在磁盘中的,磁盘IO效率是很差的,尽管MySQL底层用的是B+树,在精确检索并且命中索引时IO次数可以稳定在4次以内,但如果没命中索引,则需要全表扫描,每次分段加载数据页到内存中进行读写,效率是非常差的。

ES是基于倒排索引,根据文档id获取文档并根据相关度进行排序,在海量数据查询时效率高。

倒排索引流程:

  1. 分词:将每一个文档的数据利用算法分词,得到一个个词条;
  2. 映射关系表:创建分词和文档id的映射关系表;
  3. 词条-->id-->文档:搜索词条时,根据映射关系表找到它对应的所有文档id,然后根据文档id正向索引查到文档。

另外,在数据量大的场景下,可以使用ES集群,将索引库从逻辑上拆分为N个分片,并且每个节点可以存储副本分片,提高可用性。

image.gif

需求:

  • 在后台选择上架的商品才能在网站展示
  • 上架的商品也可以被ES检索

image.gif

2.2、创建sku的es索引库

2.2.1、两种索引库设计方案分析

  • 检索的时候输入名字,这样就是需要用sku的名称去全文检索
  • 检索的时候选择商品规格,规格是spu的公共属性,每个spu是一样的

索引库设计方案1(推荐,空间换时间):规格参数放在sku里

缺点:如果每个sku都存储规格参数(如尺寸),会有冗余存储,因为每个spu下面的sku规格参数都一样。例如spu“华为14Pro”下的sku“红色华为14Pro”,以及spu“华为手机”下的sku“蓝色华为14Pro”,这两个sku的规格参数“CPU:A14”相等。

{
    skuId:1
    spuId:11
    skyTitile:华为xx
    price:999
    saleCount:99
    attr:[
        {尺寸:5},
        {CPU:高通945},
        {分辨率:全高清}
  ]
}

image.gif

索引库设计方案2(不推荐,传输的数据量大):规格参数和sku分离

sku索引
{
    spuId:1
    skuId:11
}
attr索引
{
    skuId:11
    attr:[
        {尺寸:5},
        {CPU:高通945},
        {分辨率:全高清}
  ]
}

image.gif

结论:如果将规格参数单独建立索引,会出现检索时出现大量数据传输的问题,会引起网络故障。

所以我们选方案一,用空间换时间

2.2.2、最终选用的索引库方案,nested类型

{ “type”: “keyword” }, 保持数据精度问题,可以检索,但不分词

“analyzer”: “ik_smart” 中文分词器

“index”: false, 不可被检索,不生成index

“doc_values”: false 默认为true,不可被聚合,es就不会维护一些聚合的信息

这个数据模型要先在es中建立

注意:

为了防止对象数组扁平化,商品属性字段类型设为nested类型。

es数组的扁平化处理:es存储对象数组时,它会将数组扁平化,也就是说将对象数组的每个属性抽取出来,作为一个数组。因此会出现查询紊乱的问题。

示例:下面user字段是对象数组类型,因为数组扁平化处理,下面结果跟期望查询结果不符:

image.gif

PUT product
{
    "mappings":{
        "properties": {
            "skuId":{ "type": "long" },    #商品sku
            "spuId":{ "type": "keyword" },  #当前sku所属的spu。
            "skuTitle": {
                "type": "text",
                "analyzer": "ik_smart"      #只有sku的标题需要被分词
            },
            "skuPrice": { "type": "keyword" },  
            "skuImg"  : { "type": "keyword" },  
            "saleCount":{ "type":"long" },
            "hasStock": { "type": "boolean" },    #是否有库存。在库存模块添加此商品库存后,此字段更为true
            "hotScore": { "type": "long"  },
            "brandId":  { "type": "long" },
            "catalogId": { "type": "long"  },
            "brandName": {"type": "keyword"}, 
            "brandImg":{
                "type": "keyword",
                "index": false,          #不可被检索
                "doc_values": false     #不可被聚合。doc_values默认为true
            },
            "catalogName": {"type": "keyword" }, 
            "attrs": {
                "type": "nested",    #对象数组防止扁平化,不能用object类型
                "properties": {
                    "attrId": {"type": "long"  },
                    "attrName": {
                        "type": "keyword",
                        "index": false,        #在后面“商城业务-检索服务”开发时这里要去掉
                        "doc_values": false    #在后面“商城业务-检索服务”开发时这里要去掉
                    },
                    "attrValue": {"type": "keyword" }
                }
            }
        }
    }
}

image.gif

image.gif

2.3、SkuEsModel模型类

商品上架需要在es中保存spu信息并更新spu状态信息,所以我们就建立专门的vo来接收

SkuEsModel

写在common模块

@Data
public class SkuEsModel { //common中
    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;
    private Long catalogId;
    private String brandName;
    private String brandImg;
    private String catalogName;
    private List<Attr> attrs;
    @Data
    public static class Attr{
        private Long attrId;
        private String attrName;
        private String attrValue;
    }
}

image.gif

2.4、【库存模块】库存量查询

查询sku列表是否有库存:

上架的话需要确定库存,所以调用ware微服务来检测是否有库存

@RestController
@RequestMapping("ware/waresku")
public class WareSkuController {
    @Autowired
    private WareSkuService wareSkuService;
    
    @PostMapping(value = "/hasStock")
    public R getSkuHasStock(@RequestBody List<Long> skuIds) {
        List<SkuHasStockVo> vos = wareSkuService.getSkuHasStock(skuIds);
        return R.ok().setData(vos);
    }
}

image.gif

实现也比较好理解,就是先用自定义的mapper查有没有库存

有的话,给库存赋值,并收集成集合

@Override
public List<SkuHasStockVo> getSkuHasStock(List<Long> skuIds) {
    List<SkuHasStockVo> skuHasStockVos = skuIds.stream().map(item -> {
//根据sku_id查库存,要写mapper,主要为了真实库存减去锁定库存
        Long count = this.baseMapper.getSkuStock(item);    
        SkuHasStockVo skuHasStockVo = new SkuHasStockVo();
        skuHasStockVo.setSkuId(item);
        skuHasStockVo.setHasStock(count == null ? false : count > 0);
        return skuHasStockVo;
    }).collect(Collectors.toList());
    return skuHasStockVos;
}

image.gif

自定义mapper

这里的库存并不是简单查一下库存表,需要自定义一个简单的sql。用库存减去锁定的库存即可得出!

<select id="getSkuStock" resultType="java.lang.Long">
    SELECT SUM(stock - stock_locked) FROM wms_ware_sku WHERE sku_id = #{skuId}
</select>

image.gif

2.5、【查询模块】保存ES文档

2.5.1、常量类

public class EsConstant {
    //在es中的索引
    public static final String PRODUCT_INDEX = "gulimall_product";
    public static final Integer PRODUCT_PAGESIZE = 16;
}

image.gif

2.5.2、controller

ElasticSaveController

package com.xxx.gulimall.search.controller;
@Slf4j
@RequestMapping(value = "/search/save")
@RestController
public class ElasticSaveController {
    @Autowired
    private ProductSaveService productSaveService;
    /**
     * 上架商品
     * @param skuEsModels
     * @return
     */
    @PostMapping(value = "/product")
    public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels) {
        boolean status=false;
        try {
            status = productSaveService.productStatusUp(skuEsModels);
        } catch (IOException e) {
            //log.error("商品上架错误{}",e);
            return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(),BizCodeEnum.PRODUCT_UP_EXCEPTION.getMessage());
        }
        if(status){
            return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(),BizCodeEnum.PRODUCT_UP_EXCEPTION.getMessage());
        }else {
            return R.ok();
        }
    }
}

image.gif

2.5.3、service

使用BulkRequest ,批量保存sku_es模型类列表到索引库

@Slf4j
@Service("productSaveService")
public class ProductSaveServiceImpl implements ProductSaveService {
    @Autowired
    private RestHighLevelClient esRestClient;
    @Override
    public boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {
        //1.在es中建立索引,建立号映射关系(doc/json/product-mapping.json)[kibana中执行product-mapping.txt,需要ES安装IK分词器]
        //2. 在ES中保存这些数据
        BulkRequest bulkRequest = new BulkRequest();
        for (SkuEsModel skuEsModel : skuEsModels) {
            //构造保存请求
            IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
            indexRequest.id(skuEsModel.getSkuId().toString());
            String jsonString = JSON.toJSONString(skuEsModel);
            indexRequest.source(jsonString, XContentType.JSON);
            bulkRequest.add(indexRequest);
        }
        BulkResponse bulk = esRestClient.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
        //TODO 如果批量错误
        boolean hasFailures = bulk.hasFailures();
        List<String> collect = Arrays.asList(bulk.getItems()).stream().map(item -> {
            return item.getId();
        }).collect(Collectors.toList());
        log.info("商品上架完成:{}",collect);
        return hasFailures;
    }
}

image.gif

2.6、【商品模块】上架单个spu

image.gif

2.6.1、controller

SpuInfoController上架

/**
 * 商品上架
 */
@PostMapping("/{spuId}/up")
public R spuUp(@PathVariable("spuId") Long spuId){
    spuInfoService.up(spuId);
    return R.ok();
}

image.gif

2.6.2、远程调用库存模块

在商品模块的feign包下:

@FeignClient("gulimall-ware")
public interface WareFeignService {
    @PostMapping(value = "/ware/waresku/hasStock")
    R getSkuHasStock(@RequestBody List<Long> skuIds);
}

image.gif

商品模块启动类:

@EnableFeignClients(basePackages = "com.xunqi.gulimall.product.feign")

image.gif

然后service里就能直接@Autowired注入了。

2.6.3、【公共模块】商品常量类,添加上传成功或失败的状态码

public class ProductConstant {
    public enum AttrEnum {
        ATTR_TYPE_BASE(1,"基本属性"),
        ATTR_TYPE_SALE(0,"销售属性");
        private int code;
        private String msg;
        public int getCode() {
            return code;
        }
        public String getMsg() {
            return msg;
        }
        AttrEnum(int code, String msg) {
            this.code = code;
            this.msg = msg;
        }
    }
    public enum ProductStatusEnum {
        NEW_SPU(0,"新建"),
        SPU_UP(1,"商品上架"),
        SPU_DOWN(2,"商品下架"),
        ;
        private int code;
        private String msg;
        public int getCode() {
            return code;
        }
        public String getMsg() {
            return msg;
        }
        ProductStatusEnum(int code, String msg) {
            this.code = code;
            this.msg = msg;
        }
    }
}

image.gif

2.6.4、service

业务流程:

  1. 查询当前spu下的sku列表;
  2. 将这个sku列表封装成sku_es模型类列表;
  1. 给每个sku加上属性规格列表;
  2. 查询每个sku是否有库存,要远程调用库存模块;
  3. 给每个sku加上热度、所属品牌、所属分类名、所有可被检索的规格等属性;
  1. 将收集的sku_es模型类列表发给es保存,要远程调用查询模块。

SpuInfoServiceImpl

@Override
    public void up(Long spuId) {
        //1.获得spu对应的sku集合
        List<SkuInfoEntity> skuInfoEntities = skuInfoService.getSkusBySpuId(spuId);
        //2.获得spu的基础属性实体集合
        List<ProductAttrValueEntity> baseAttrs = productAttrValueService.baseAttrListforSpu(spuId);
        //3.获得基本属性中可搜索的属性id
        //3.1获得spu基础属性实体集合中的属性id集合
        List<Long> attrIds = baseAttrs.stream().map(attr -> {
            return attr.getAttrId();
        }).collect(Collectors.toList());
        //3.2获得可搜索属性实体类对象
        List<Long> searchAttrIds = attrService.selectSearchAttrs(attrIds);
        //3.3将它们转化为set集合
        Set<Long> idSet = searchAttrIds.stream().collect(Collectors.toSet());
        //3.4对所有基础属性实体过滤,第一步是只保留可搜索属性实体类对象,第二步是给这些对象中的Attrs对象赋值,最后收集为attrsList
        List<SkuEsModel.Attrs> attrsList = baseAttrs.stream().filter(item -> {
            return idSet.contains(item.getAttrId());
        }).map(item -> {
            SkuEsModel.Attrs attrs = new SkuEsModel.Attrs();
            BeanUtils.copyProperties(item, attrs);
            return attrs;
        }).collect(Collectors.toList());
        //收集所有skuId的集合
        List<Long> skuIdList = skuInfoEntities.stream()
                .map(SkuInfoEntity::getSkuId)
                .collect(Collectors.toList());
        //TODO 1、发送远程调用,库存系统查询是否有库存
        Map<Long, Boolean> stockMap = null;
        try {
            R skuHasStock = wareFeignService.getSkuHasStock(skuIdList);
            TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>() {};
            stockMap = skuHasStock.getData(typeReference).stream()
                    .collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
        } catch (Exception e) {
            log.error("库存服务查询异常:原因{}",e);
        }
//2、封装每个sku的信息
        Map<Long, Boolean> finalStockMap = stockMap;
        List<SkuEsModel> collect = skuInfoEntities.stream().map(sku -> {
            //组装需要的数据
            SkuEsModel esModel = new SkuEsModel();
            esModel.setSkuPrice(sku.getPrice());
            esModel.setSkuImg(sku.getSkuDefaultImg());
            //设置库存信息
            if (finalStockMap == null) {
                esModel.setHasStock(true);
            } else {
                esModel.setHasStock(finalStockMap.get(sku.getSkuId()));
            }
            //TODO 2、热度评分。0
            esModel.setHotScore(0L);
            //TODO 3、查询品牌和分类的名字信息
            BrandEntity brandEntity = brandService.getById(sku.getBrandId());
            esModel.setBrandName(brandEntity.getName());
            esModel.setBrandId(brandEntity.getBrandId());
            esModel.setBrandImg(brandEntity.getLogo());
            CategoryEntity categoryEntity = categoryService.getById(sku.getCatalogId());
            esModel.setCatalogId(categoryEntity.getCatId());
            esModel.setCatalogName(categoryEntity.getName());
            //设置检索属性
            esModel.setAttrs(attrsList);
            BeanUtils.copyProperties(sku,esModel);
            return esModel;
        }).collect(Collectors.toList());
        //TODO 5、将数据发给es进行保存:mall-search
        R r = searchFeignService.productStatusUp(collect);
        if (r.getCode() == 0) {
            //远程调用成功
            //TODO 6、修改当前spu的状态,具体代码看代码块后面SpuInfoDao.xml
            this.baseMapper.updaSpuStatus(spuId, ProductConstant.ProductStatusEnum.SPU_UP.getCode());
        } else {
            //远程调用失败
            //TODO 7、重复调用?接口幂等性:重试机制
        }
    }

image.gif

SpuInfoDao更新spu状态

<update id="updaSpuStatus">
        UPDATE pms_spu_info SET publish_status = #{code} ,update_time = NOW() WHERE id = #{spuId}
    </update>
image.gif

2.6.5、测试

商品上架用到了三个微服务,分别是product、ware、search

那我们分别debug启动它们,然后在这些微服务中使用的方法中打上断点,查看调用流程

获得spu对应的sku集合

image.gif

获得spu的基础属性实体集合

基础属性如下:

image.gif

给SkuEsModel.Attrs对象赋值

image.gif

测试

image.gif

2.6.6、修改结果类R

public <T> T getData(String key, TypeReference<T> typeReference) {
    Object data = get(key);// 默认是map类型,springmvc做的
    String jsonStr = JSON.toJSONString(data);
    T t = JSON.parseObject(jsonStr, typeReference);
    return t;
  }
  // 利用fastJson进行逆转
  // 这里要声明泛型<T>,这个泛型只跟方法有关,跟类无关。
  // 例如类上有个泛型,这里可以使用类上的泛型,就不用声明
  public <T> T getData(TypeReference<T> typeReference) {
    Object data = get("data");// 默认是map类型,springmvc做的
    String jsonStr = JSON.toJSONString(data);
    T t = JSON.parseObject(jsonStr, typeReference);
    return t;
  }
  public R setData(Object data) {
    put("data", data);
    return this;
  }

image.gif


相关实践学习
使用阿里云Elasticsearch体验信息检索加速
通过创建登录阿里云Elasticsearch集群,使用DataWorks将MySQL数据同步至Elasticsearch,体验多条件检索效果,简单展示数据同步和信息检索加速的过程和操作。
ElasticSearch 入门精讲
ElasticSearch是一个开源的、基于Lucene的、分布式、高扩展、高实时的搜索与数据分析引擎。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr(也是基于Lucene)。 ElasticSearch的实现原理主要分为以下几个步骤: 用户将数据提交到Elastic Search 数据库中 通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据 当用户搜索数据时候,再根据权重将结果排名、打分 将返回结果呈现给用户 Elasticsearch可以用于搜索各种文档。它提供可扩展的搜索,具有接近实时的搜索,并支持多租户。
相关文章
|
4天前
|
SQL 设计模式 开发框架
谷粒商城笔记+踩坑(6)——商品服务-属性及其关联分组
商品规格属性和销售属性的增删改查、属性分组中查询新增关联的属性
谷粒商城笔记+踩坑(6)——商品服务-属性及其关联分组
|
4天前
|
JSON Java API
谷粒商城笔记+踩坑(7)——新增商品,请求参数转vo类
效果展示、配置、启动会员模块、获取当前分类关联的品牌(不用分页)、获取当前分类下的分组及其关联的属性、新增商品、添加复合配置、限制内存、报错loadbalancer解决
谷粒商城笔记+踩坑(7)——新增商品,请求参数转vo类
|
4天前
|
XML 前端开发 Java
谷粒商城笔记+踩坑(5)——商品服务-属性分组、品牌关联分类,spu+sku+分页拦截器
SPU和SKU、属性分组的增删改查、QueryWrapper的and和or用法、获取当前品牌关联的所有分类
谷粒商城笔记+踩坑(5)——商品服务-属性分组、品牌关联分类,spu+sku+分页拦截器
|
3月前
|
前端开发 JavaScript Python
Django项目之电商购物商城 -- 新增收货地址
Django项目之电商购物商城 -- 新增收货地址
|
4天前
|
存储 前端开发 Java
谷粒商城笔记+踩坑(19)——订单模块构建、登录拦截器
首先搭建页面环境,然后介绍整合Spring Session的相关内容,并将用户信息放到session里,多线程优化,完成订单模块的功能、登录拦截等功能的实现
谷粒商城笔记+踩坑(19)——订单模块构建、登录拦截器
|
4天前
|
SQL 前端开发 Java
谷粒商城笔记+踩坑(15)——商品详情搭建+异步编排
查询 pms_spu_info_desc@Autowired// 4、获取 spu 的介绍 pms_spu_info_desc获取线程池的属性值这里直接调用与配置文件相对应的属性配置类@Bean。
谷粒商城笔记+踩坑(15)——商品详情搭建+异步编排
|
4天前
|
前端开发 JavaScript Java
谷粒商城笔记+踩坑(3)——商品服务-三级分类、网关跨域
商品服务-三级分类增删改查、跨域问题、逻辑删除
|
4月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue的免税商品优选购物商城系统附带文章和源代码
基于SpringBoot+Vue的免税商品优选购物商城系统附带文章和源代码
38 1
|
3月前
|
JSON 安全 API
电商开发者必读:微店商品详情API接口全解析
微店商品详情API让开发者能通过商品ID获取包括名称、价格、库存、描述和图片在内的详细信息。开发者需注册账号、获取API密钥和访问权限,并熟悉HTTP请求。请求示例为GET方法,响应数据以JSON格式返回。注意错误处理、保密性、频率限制和数据验证,以确保安全和高效使用。
|
3月前
|
缓存 前端开发 数据库
Django项目之电商购物商城 -- 创建收货地址
Django项目之电商购物商城 -- 创建收货地址