一.商品上架操作
将检索数据存入es,更改商品上架状态为已上架
二.业务设计
(1)设计检索数据
分析:商品上架在 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 的数据,,传输阻塞时间会很
长,业务更加无法继续。
所以,我们如下设计,这样才是文档区别于关系型数据库的地方,宽表设计,不能去考虑数
据库范式。
什么是sku和spu?
spu 例:华为 HUAWEI Mate 30 Pro
sku 例:华为 HUAWEI Mate 30 Pro 亮黑色 8GB+256GB麒麟990旗舰芯片OLED环幕屏双4000万徕卡电影四摄4G全网通手机
sku可以根据颜色、内存、芯片等各种规格参数来划分成不同的版本,sku为这款产品的最小单元,查询产品时主要查sku信息
从查询思路出发,查询商品信息是根据spu查sku,核心是sku,又因为spu与sku是一对多的关系,所有我们存储检索数据有两种方式
(1)spu与sku设计成一个索引,通过查询spu就能查询到每一条具体的sku数据,缺点是存入es数据量庞大,占用空间大(优先考虑)
(2)spu与sku分开,单独索引,查询时先查spu,然后再查出具体sku,虽然存入es数据量不大,但是查询效率低
检索字段主要包括:[spu的id,sku的id,sku的商品标题,sku部分商品参数,产品热度分数,是否有库存,产品部分属性参数],最后根据上面两种方式设计出数据的结构,这里选用第一种方式(空间换时间)宽表设计,不能去考虑数据库范式,将spu和sku信息整合在一起,下面是QUERY DSL新增语句:
PUT product
{
"mappings": {
"properties": {
"skuId": {
"type": "long"
},
"spuId": {
"type": "long"
},
"skuTitle": {
"type": "text",
"analyzer": "ik_smart"
},
"skuPrice": {
"type": "keyword"
},
"skuImg": {
"type": "keyword",
"index": false,
"doc_values": false
},
"saleCount": {
"type": "long"
},
"hosStock": {
"type": "boolean"
},
"hotScore": {
"type": "long"
},
"brandId": {
"type": "long"
},
"catelogId": {
"type": "long"
},
"brandName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"brandImg": {
"type": "keyword",
"index": false,
"doc_values": false
},
"catelogName": {
"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"
}
}
}
}
}
}
(2)设计后端业务
- 查询当前sku规格属性,筛选出可以检索的属性
- 远程调用仓储服务查询是否有库存
- 远程调用es服务保存数据
- 修改商品上架状态
三.代码
(1)商品服务查数据
//商品上架:上架是将后台的商品放在 es 中可以提供检索和查询功能
@Override
public void up(Long spuId) {
//根据spuid查出sku集合
List<SkuInfoEntity> skuInfoEntities = skuInfoService.querySkuBySpuId(spuId);
//TODO 4.查询当前sku规格属性,并判断是否可以检索 search_type
/**
* 根据Spuid查表`pms_product_attr_value`,取出attr_id,查这些属性idattr_id的属性
* 筛选出符合检索的search_type=1的attr
* 然后拿到 attr的attr_id=product_attr_value的attr_id 的数据插入SkuEsModel.Attrs
*/
List<ProductAttrValueEntity> productAttrValueList = productAttrValueService.baseAttrlistforspu(spuId);
//获取attrIds
List<Long> attrIds = productAttrValueList.stream().map(productAttrValue -> {
Long attrId = productAttrValue.getAttrId();
return attrId;
}).collect(Collectors.toList());
//筛选出search_type=1的attr
List<AttrEntity> list = attrDao.selectBatchIds(attrIds);
List<Long> productAttrIds = list.stream().filter(attr -> {
return attr.getSearchType() == 1;
}).map(attrEntity -> {
return attrEntity.getAttrId();
}).collect(Collectors.toList());
HashSet<Long> idSet = new HashSet<>(productAttrIds);
//拿到search_type=1的product_attr_value数据
List<SkuEsModel.Attrs> esAttrList = productAttrValueList.stream().filter(item -> {
//筛选
return idSet.contains(item.getAttrId());
}).map(attr -> {
//给SkuEsModel.Attrs赋值
SkuEsModel.Attrs esAttr = new SkuEsModel.Attrs();
BeanUtils.copyProperties(attr, esAttr);
return esAttr;
}).collect(Collectors.toList());
//TODO 1.发送远程调用 库存系统查询是否有库存
//注意调用时,传的参数有序列化问题
List<Long> skuIds = skuInfoEntities.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
Map<Long, Boolean> booleanMap = null;
try {
R<List<SkuHasStockVo>> r = wareFeignService.getSkuHasStock(skuIds);
Integer code = r.getCode();
List<SkuHasStockVo> data = r.getData(new TypeReference<List<SkuHasStockVo>>(){});
booleanMap = data.stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
} catch (Exception e) {
log.error("库存服务查询异常:原因{}",e);
}
//循环查数据库?
Map<Long, Boolean> finalBooleanMap = booleanMap;
List<SkuEsModel> collect = skuInfoEntities.stream().map(sku -> {
SkuEsModel esModel = new SkuEsModel();
BeanUtils.copyProperties(sku, esModel);
esModel.setSkuImg(sku.getSkuDefaultImg());
esModel.setSkuPrice(sku.getPrice());
esModel.setAttrs(esAttrList);
//拿到远程查出的库存状态
if (finalBooleanMap == null){
//为null表示调用库存服务失败,直接给一个默认库存
//有库存true 无库存false,默认有数据为true
esModel.setHasStock(true);
}else {
//调用库存服务成功,直接设置库存状态
esModel.setHasStock(finalBooleanMap.get(sku.getSkuId()));
}
//TODO 2.热度评分 0?
esModel.setHotScore(0L);
//TODO 3.查询品牌和分类的名字信息
BrandEntity brand = brandService.getById(esModel.getBrandId());
esModel.setBrandName(brand.getName());
esModel.setBrandImg(brand.getLogo());
CategoryEntity category = categoryService.getById(esModel.getCatalogId());
esModel.setCatalogName(category.getName());
return esModel;
}).collect(Collectors.toList());
//TODO 5.es服务保存数据
R r = searchFeignService.productStatusUp(collect);
if (r.getCode() == 0){
//TODO 6.修改商品上架状态 上架状态[0 - 下架,1 - 上架]
this.baseMapper.updataSpuStatus(spuId, ProductConstant.StatusEnum.SUP_UP.getCode());
}else {
//远程调用失败
//TODO 7.重复调用?接口幂等性:重试机制?
}
}
(2)仓储服务查库存
//查询是否有库存,参数传json
@Override
public List<SkuHasStockVo> getSkuHasStock(List<Long> skuIds) {
/**
* 库存 = 库存总合 - 锁定库存总和
* 查每个skuid的库存
*
* 循环查SELECT SUM(stock-stock_locked) AS stocksum FROM `wms_ware_sku` WHERE sku_id=?
* 分组查SELECT sku_id,SUM(stock-stock_locked) AS stocksum FROM `wms_ware_sku` GROUP BY sku_id
*/
List<SkuHasStockVo> collect = skuIds.stream().map(skuId -> {
Long count = baseMapper.selectSumStock(skuId);
SkuHasStockVo skuHasStockVo = new SkuHasStockVo();
skuHasStockVo.setSkuId(skuId);
skuHasStockVo.setHasStock(count==null?false:count>0);
return skuHasStockVo;
}).collect(Collectors.toList());
return collect;
}
<select id="selectSumStock" resultType="java.lang.Long">
SELECT SUM(stock-stock_locked) AS stocksum FROM `wms_ware_sku` WHERE sku_id=#{skuId}
</select>
(3)ES服务新增数据
//上架,保存es数据
@Override
public void productStatusUp(List<SkuEsModel> esModels) throws Exception {
BulkRequest bulkRequest = new BulkRequest();
for (SkuEsModel esModel : esModels) {
IndexRequest indexRequest = new IndexRequest();
indexRequest.index(EsConstant.PRODUCT_INDEX);
indexRequest.id(esModel.getSkuId().toString());
String string = JSON.toJSONString(esModel);
indexRequest.source(string, XContentType.JSON);
bulkRequest.add(indexRequest);
}
//批量保存
BulkResponse bulk = client.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
//如果批量插入错误,就抛异常
if (bulk.hasFailures()){//有问题返回true
throw new Exception("es批量保存异常");
}
//获取批量处理结果
BulkItemResponse[] items = bulk.getItems();
List<String> collect = Arrays.stream(items).map(item -> {
return item.getId();
}).collect(Collectors.toList());
log.info("商品上架成功id:{},返回数据:{}",collect,bulk.toString());
}
(4)解决公共返回类无法获取数据的问题
公共返回类set的参数无法获取,因为返回类继承HashMap,远程调用服务时序列化数据无法获取类的属性,只能从Map中拿数据
解决办法:直接将数据序列化操作(对象转json,json再转成需要的对象)
public <T> T getData(TypeReference<T> typeReference) {
Object data = get("data");
String s = JSON.toJSONString(data);
T t = JSON.parseObject(s, typeReference);
return t;
}
TypeReference内部类方式传参
List<SkuHasStockVo> data = r.getData(new TypeReference<List<SkuHasStockVo>>(){});