一、商品上架
上架的商品才可以在网站展示。
上架的商品需要可以被检索。
1、商品 Mapping
分析:商品上架在 es 中是存 sku 还是 spu? (1)检索的时候输入名字,是需要按照 sku 的 title 进行全文检索的 (2)检索使用商品规格,规格是 spu 的公共属性,每个 spu 是一样的 (3)按照分类 id 进去的都是直接列出 spu 的,还可以切换 (4)我们如果将 sku 的全量信息保存到 es 中(包括 spu 属性)就太多量字段了 (5)我们如果将 spu 以及他包含的 sku 信息保存到 es 中,也可以方便检索,但是 sku 属于 spu 的级联动,在 es 中需要 nested 模型,这种性能差点 (6)但是存储与检索我们必须性能折中 (7)如果我们分拆存储,spu 和 attr 一个索引,sku 单独一个所以可能涉及的问题:检索商品的名字,如“手机”,对应的 spu 有很多,我们要分析出这些 spu 的所有级联属性,再做一次查询,就必须将所有 spu_id 都发出去。假设有 1 万个数据,数据传输一次就 10000*4=4MB;并发情况下假设 1000 检索请求,那就是 4GB 的数据,传输阻塞时间会很长,业务更加无法继续。
方案1:
{
skuId: 1,
spuId: 11,
skyTitile: 华为xx,
price: 999,
saleCount: 99,
attr: [{
尺寸: 5
}, {
CPU: 高通945
}, {
分辨率: 全高清
}]
}
缺点:如果每个sku都存储规格参数(如尺寸),会有冗余存储,因为每个 spu 对应的 sku 的规格参数都一样
方案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,以空间换时间
所以,我们如下设计,这样才是文档区别于关系型数据库的地方,宽表设计,不能去考虑数据库范式。
建立product索引
最终选用的数据模型:
(1)PUT product
{
”mappings”: {
”properties”: {
”skuId”: {
”type”: “long”
},
”spuId”: {
”type”: “keyword”
},
”skuTitle”: {
”type”: “text”,
”analyzer”: “ik_smart”
},
”skuPrice”: {
”type”: “keyword”
},
”skuImg”: {
”type”: “keyword”
},
”saleCount”: {
”type”: “long”
},
”hasStock”: {
”type”: “boolean”
},
”hotScore”: {
”type”: “long”
},
”brandId”: {
”type”: “long”
},
”catalogId”: {
”type”: “long”
},
”brandName”: {
”type”: “keyword”
},
”brandImg”: {
”type”: “keyword”,
”index”: false,
”doc_values”: false
},
”catalogName”: {
”type”: “keyword”
},
”attrs”: {
”type”: “nested”,
”properties”: {
”attrId”: {
”type”: “long”
},
”attrName”: {
”type”: “keyword”,
”index”: false,
”doc_values”: false
},
”attrValue”: {
”type”: “keyword”
}
}
}
}
}
}
其中
(1)“type”: “keyword” 保持数据精度问题,可以检索,但不分词
(2)“index”:false 代表不可被检索
(3)“doc_values”: false 不可被聚合,es就不会维护一些聚合的信息 冗余存储的字段:不用来检索,也不用来分析,节省空间
库存是bool。 检索品牌id,但是不检索品牌名字、图片 用skuTitle检索
(2)nested 数据类型场景
属性是”type”: “nested”,因为是内部的属性进行检索
数组类型的对象会被扁平化处理(对象的每个属性会分别存储到一起)
user.name=[“aaa”,”bbb”]
user.addr=[“ccc”,”ddd”]
这种存储方式,可能会发生如下错误:
错误检索到{aaa,ddd},这个组合是不存在的
数组的扁平化处理会使检索能检索到本身不存在的,为了解决这个问题,就采用了嵌入式属性,数组里是对象时用嵌入式属性(不是对象无需用嵌入式属性)
nested阅读:https://blog.csdn.net/weixin_40341116/article/details/80778599
使用聚合:https://blog.csdn.net/kabike/article/details/101460578
2、上架细节
上架是将后台的商品放在 es 中可以提供检索和查询功能
(1)hasStock:代表是否有库存。默认上架的商品都有库存。如果库存无货的时候才需要更新一下 es
(2)库存补上以后,也需要重新更新一下 es
(3)hotScore 是热度值,我们只模拟使用点击率更新热度。点击率增加到一定程度才更新热度值
(4)下架就是从 es 中移除检索项,以及修改 mysql 状态
商品上架步骤:
(1)先在 es 中按照之前的 mapping 信息,建立 product 索引。
(2)点击上架,查询出所有 sku 的信息,保存到 es 中
(3)es 保存成功返回,更新数据库的上架状态信息
3、数据一致性
(1)商品无库存的时候需要更新 es 的库存信息 (2)商品有库存也要更新 es 的信息
4、上架流程
product里组装好,search里上架
(1)gulimall-common 更新文件 com.atguigu.common.utils.R
将R设置成泛型的
package com.atguigu.common.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.apache.http.HttpStatus;
import java.util.HashMap;
import java.util.Map;
public class R extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
public R setData(Object data) {
put("data",data);
return this;
}
//利用fastjson进行反序列化
public <T> T getData(TypeReference<T> typeReference) {
Object data = get("data"); //默认是map
String jsonString = JSON.toJSONString(data);
T t = JSON.parseObject(jsonString, typeReference);
return t;
}
//利用fastjson进行反序列化
public <T> T getData(String key,TypeReference<T> typeReference) {
Object data = get(key); //默认是map
String jsonString = JSON.toJSONString(data);
T t = JSON.parseObject(jsonString, typeReference);
return t;
}
public R() {
put("code", 0);
put("msg", "success");
}
public static R error() {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
}
public static R error(String msg) {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
}
public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R ok(String msg) {
R r = new R();
r.put("msg", msg);
return r;
}
public static R ok(Map<String, Object> map) {
R r = new R();
r.putAll(map);
return r;
}
public static R ok() {
return new R();
}
public R put(String key, Object value) {
super.put(key, value);
return this;
}
public Integer getCode() {
return (Integer) this.get("code");
}
}
(2)gulimall-common 新增文件 com.atguigu.common.to.SkuHasStockVo
package com.atguigu.common.to;
import lombok.Data;
@Data
public class SkuHasStockVo {
private Long skuId;
private Boolean hasStock;
}
(3)gulimall-common 新增文件 com.atguigu.common.to.es.SkuEsModel
商品上架需要在 es 中保存spu信息并更新spu的状态信息,由于SpuinfoEntity与索引的数据模型并不对应,所以我们要建立专门的vo进行数据传输
package com.atguigu.common.to.es;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@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;
private Long catalogId;
private String brandName;
private String brandImg;
private String catalogName;
private List<Attrs> attrs;
@Data
public static class Attrs {
private Long attrId;
private String attrName;
private String attrValue;
}
}
(4)gulimall-common 更新文件 com.atguigu.common.constant.ProductConstant
package com.atguigu.common.constant;
public class ProductConstant {
public enum AttrEnum{
ATTR_TYPE_BASE(1,"基本属性"),ATTR_TYPE_SALE(0,"销售属性");
private int code;
private String msg;
AttrEnum(int code, String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return 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;
}
}
}
(5)gulimall-product 的 com.atguigu.gulimall.product.controller.SpuInfoController 新增上架方法 upSpuForSearch
/**
* 商品上架
* @param spuId
* @return
*/
///product/spuinfo/{spuId}/up
@PostMapping("{spuId}/up")
public R upSpuForSearch(@PathVariable("spuId") Long spuId){
spuInfoService.upSpuForSearch(spuId);
return R.ok();
}
(6)gulimall-product 的 com.atguigu.gulimall.product.service.impl.SpuInfoServiceImpl 新增接口和实现类方法 upSpuForSearch
/**
* 商品上架
* @param spuId
*/
@Override
public void upSpuForSearch(Long spuId) {
//1、查出当前spuId对应的所有sku信息,品牌的名字
List<SkuInfoEntity> skuInfoEntities = skuInfoService.getSkusBySpuId(spuId);
//TODO 4、查出当前sku的所有可以被用来检索的规格属性
List<ProductAttrValueEntity> baseAttrs = attrValueService.baseAttrlistforspu(spuId);
List<Long> attrIds = baseAttrs.stream().map(attr -> {
return attr.getAttrId();
}).collect(Collectors.toList());
List<Long> searchAttrIds = attrService.selectSearchAttrsIds(attrIds);
//转换为Set集合
Set<Long> idSet = searchAttrIds.stream().collect(Collectors.toSet());
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());
List<Long> skuIdList = skuInfoEntities.stream()
.map(SkuInfoEntity::getSkuId)
.collect(Collectors.toList());
//TODO 1、发送远程调用,库存系统查询是否有库存
Map<Long, Boolean> stockMap = null;
try {
R skuHasStock = wareFeignService.getSkuHasStocks(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进行保存:gulimall-search
R r = searchFeignService.saveProductAsIndices(collect);
if (r.getCode() == 0) {
//远程调用成功
//TODO 6、修改当前spu的状态
this.baseMapper.updateSpuStatus(spuId, ProductConstant.ProductStatusEnum.SPU_UP.getCode());
} else {
//远程调用失败
//TODO 7、重复调用?接口幂等性:重试机制
}
}
(7)gulimall-product 的 com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl 新增接口和实现类方法 getSkusBySpuId
/**
* 查出当前spuid对应的所有sku信息
* @param spuId
* @return
*/
@Override
public List<SkuInfoEntity> getSkusBySpuId(Long spuId) {
List<SkuInfoEntity> skus = this.list(new QueryWrapper<SkuInfoEntity>().eq("spu_id", spuId));
return skus;
}
(8)gulimall-product 的 com.atguigu.gulimall.product.service.impl.AttrServiceImpl 新增接口和实现类方法 selectSearchAttrsIds
/**
* 在指定的所有属性集合里面,挑出检索属性
* @param attrIds
* @return
*/
@Override
public List<Long> selectSearchAttrsIds(List<Long> attrIds) {
/**
* SELECT attr_id FROM pms_attr WHERE attr_id in (?) AND search_type=1
*/
return baseMapper.selectSearchAttrsIds(attrIds);
}
(9)gulimall-product 的 com.atguigu.gulimall.product.dao.AttrDao 新增接口 selectSearchAttrsIds
/**
* 在指定的所有属性集合里面,挑出检索属性
* @param attrIds
* @return
*/
List<Long> selectSearchAttrsIds(@Param("attrIds") List<Long> attrIds);
(10)gulimall-product 的 AttrDao.xml 新增方法 selectSearchAttrsIds
<!--在指定的所有属性集合里面,挑出检索属性-->
<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>
(11)gulimall-product 创建远程调用文件 com.atguigu.gulimall.product.feign.WareFeignService
package com.atguigu.gulimall.product.feign;
import com.atguigu.common.to.SkuHasStockVo;
import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
@Component
@FeignClient("gulimall-ware")
public interface WareFeignService {
/**
* 1、R设计的时候可以加上泛型
* 2、直接返回我们想要的结果
* 3、自己封装解析结果
* @return
*/
@PostMapping("/ware/waresku/hasStock")
R getSkuHasStocks(@RequestBody List<Long> skuIds);
}
(12)gulimall-ware 编写远程服务查询库存接口
com.atguigu.gulimall.ware.controller.WareSkuController 新增方法 getSkuHasStocks
/**
* 查询sku是否有库存
* 返回skuId 和 stock库存量
*/
@PostMapping("/hasStock")
public R getSkuHasStocks(@RequestBody List<Long> SkuIds){
List<SkuHasStockVo> vos = wareSkuService.getSkuHasStock(SkuIds);
return R.ok().setData(vos);
}
com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl 新增接口和实现方法
/**
* 查询sku是否有库存
* @param skuIds
* @return
*/
@Override
public List<SkuHasStockVo> getSkuHasStock(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.setHasStock(count==null?false:count>0);
vo.setSkuId(skuId);
return vo;
}).collect(Collectors.toList());
System.out.println(collect);
return collect;
}
com.atguigu.gulimall.ware.dao.WareSkuDao 新增方法 getSkuStock
/**
* 查看当前sku的总库存量
* @param skuId
* @return
*/
Long getSkuStock(@Param("skuId") Long skuId);
WareSkuDao.xml 新增xml实现方法
<!--查看当前sku的总库存量-->
<select id="getSkuStock" resultType="java.lang.Long">
SELECT SUM(stock-stock_locked) FROM wms_ware_sku WHERE sku_id=#{skuId}
</select>
(13)gulimall-product 创建远程调用文件 com.atguigu.gulimall.product.feign.SearchFeignService
package com.atguigu.gulimall.product.feign;
import com.atguigu.common.to.es.SkuEsModel;
import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
@Component
@FeignClient("gulimall-search")
public interface SearchFeignService {
@PostMapping("/search/save/product")
R saveProductAsIndices(@RequestBody List<SkuEsModel> skuEsModels);
}
(14)gulimall-search 编写远程服务es上架商品接口
新增文件 com.atguigu.gulimall.search.controller.ElasticSaveController
package com.atguigu.gulimall.search.controller;
import com.atguigu.common.exception.BizCodeEnum;
import com.atguigu.common.to.es.SkuEsModel;
import com.atguigu.common.utils.R;
import com.atguigu.gulimall.search.service.ProductSaveService;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.jni.BIOCallback;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Slf4j
@RequestMapping("/search/save")
@RestController
public class ElasticSaveController {
@Autowired
ProductSaveService productSaveService;
//上架商品
@PostMapping("/product")
public R saveProductAsIndices(@RequestBody List<SkuEsModel> skuEsModels){
boolean b = false;
try {
System.out.println(skuEsModels);
b = productSaveService.productStatusUp(skuEsModels);
System.out.println(b);
}catch (Exception e){
e.printStackTrace();
log.error("ElasticSaveController商品上架错误:{}",e);
return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UP_EXCEPTION.getMessage());
}
if(!b){
return R.ok();
}else{
return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UP_EXCEPTION.getMessage());
}
}
}
新增接口和实现类文件 com.atguigu.gulimall.search.service.impl.ProductSaveServiceImpl
package com.atguigu.gulimall.search.service.impl;
import com.alibaba.fastjson.JSON;
import com.atguigu.common.to.es.SkuEsModel;
import com.atguigu.gulimall.search.config.GulimallElasticSearchConfig;
import com.atguigu.gulimall.search.constant.EsConstant;
import com.atguigu.gulimall.search.service.ProductSaveService;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.bulk.BulkItemResponse;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
public class ProductSaveServiceImpl implements ProductSaveService {
@Resource
private RestHighLevelClient restHighLevelClient;
/**
* 上架商品
* @param skuEsModels
*/
@Override
public boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {
// 1.给ES建立一个索引 product
BulkRequest bulkRequest = new BulkRequest();
// 2.构造保存请求
for (SkuEsModel esModel : skuEsModels) {
// 设置索引
IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
// 设置索引id
indexRequest.id(esModel.getSkuId().toString());
String jsonString = JSON.toJSONString(esModel);
indexRequest.source(jsonString, XContentType.JSON);
// add
bulkRequest.add(indexRequest);
}
// bulk批量保存
BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
// TODO 是否拥有错误
boolean hasFailures = bulk.hasFailures();
List<String> collect = Arrays.stream(bulk.getItems()).map(item -> item.getId()).collect(Collectors.toList());
log.info("商品上架完成:{},返回数据",collect,bulk.toString());
return hasFailures;
}
}