2、product-es准备
ES在内存中,所以在检索中优于mysql。ES也支持集群,数据分片存储。
-
需求:
- 上架的商品才可以在网站展示。
- 上架的商品需要可以被检索。(上架的商品就加入到es去被检索)
- 分析sku在es中如何存储 商品mapping
-
分析: 商品上架在es中是存sku还是spu?
- 1)检索的时候输入名字,是需要按照sku的title进行全文检索
- 2)检素使用商品规格,规格是spu的公共属性,每个spu是一样的
- 3)按照分类id进去的都是直接列出spu的,还可以切换。
- 4〕我们如果将sku的全量信息保存到es中(包括spu属性〕就太多字段了
-
处理方案
-
方案1:
存储的到一个索引记录中,每个都带有对应sku具体的attr属性
-
{ skuId:1 spuId:11 skyTitile:华为xx price:999 saleCount:99 attr:[ {尺寸:5}, {CPU:高通945}, {分辨率:全高清} ] -
缺点:
- 如果每个sku都存储规格参数(如尺寸),会有冗余存储,因为每个spu对应的sku的规格参数都一样
-
-
方案2:
存储到两个索引记录中,一个存储对应的spu/sku的id,然后去检索查询另一个记录中的数据,就可以解决上面的冗余存储的问题
-
#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
- 所以会出现如果有大量并发的请求,就会出现一刹那32MB,我们就加两个0,就会有3g多的数据在网络传输
- 这样子就会出现网络问题
-
-
结论
如果将规格参数单独建立索引,会出现检索时出现大量数据传输的问题,会引起网络网络
因此选用方案1,以空间换时间
-
-
建立product索引
索引数据模型
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, # 不可被检索,不生成index "doc_values": false # 不可被聚合 }, "catalogName": {"type": "keyword" }, "attrs": { "type": "nested", "properties": { "attrId": {"type": "long" }, "attrName": { "type": "keyword", "index": false, "doc_values": false }, "attrValue": {"type": "keyword" } } } } } }其中
- “type”: “keyword” 保持数据精度问题,可以检索,但不分词
- “index”:false 代表不可被检索
- “doc_values”: false 不可被聚合,es就不会维护一些聚合的信息
- 冗余存储的字段: 不用来检索,也不用来分析,节省空间
- 库存是bool。
- 检索品牌id,但是不检索品牌名字、图片 用skuTitle检索
-
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
3、正式编码
- controller
//POST /product/spuinfo/{spuId}/up
//上架
@PostMapping("/{spuId}/up")
public R spuUp(@PathVariable("spuId")Long spuId){
spuInfoService.up(spuId);
return R.ok();
}
- 传输to对象
因为这里的search服务也需要使用到这个to对象,所以我们新建在common中
商品上架需要在es中保存spu信息并更新spu的状态信息,由于SpuInfoEntity与索引的数据模型并不对应,所以我们要建立专门的vo进行数据传输
com.achang.common.to.es.SkuEsModel
@Data
public class SkuEsModel implements Serializable {
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;
@Data
public static class Attrs{
private Long attrId;
private String attrName;
private String attrValue;
}
}
- service
com.achang.achangmall.product.service.impl.SpuInfoServiceImpl
//商品上架
@Override
public void up(Long spuId) {
//1、查询当前spuId对应的所有sku信息,品牌的名字
List skuInfoEntities = skuInfoService.getSkusBySpuId(spuId);
List skuIdList = skuInfoEntities.stream().map(item -> {
return item.getSkuId();
}).collect(Collectors.toList());
//todo 查询当前sku可以被检索的规格属性
List baseAttrList = productAttrValueService.baseAttrList(spuId);
List attrIdList = baseAttrList.stream().map(item -> {
return item.getAttrId();
}).collect(Collectors.toList());
List searchAttrIds = attrService.selectSearchAttrIds(attrIdList);
Set idSet = new HashSet<>(searchAttrIds);
List attrsList = baseAttrList.stream().filter(item -> {
return idSet.contains(item.getAttrId());
}).map(s -> {
SkuEsModel.Attrs attrsEs = new SkuEsModel.Attrs();
BeanUtils.copyProperties(s, attrsEs);
return attrsEs;
}).collect(Collectors.toList());
Map stockMap = null;
try {
//发送远程调用,库存系统查询是否有库存
R r = wareFeignService.getSkuHasStock(skuIdList);
TypeReference> typeReference = new TypeReference>() {};
stockMap = r.getData(typeReference).stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, SkuHasStockVo::getHasStock));
}catch (Exception e){
log.error("库存服务查询出现问题:原因【{}】",e);//如果远程调用失败
}
//2、封装每个sku的信息
Map finalStockMap = stockMap;
List collect = skuInfoEntities.stream().map(i -> {
SkuEsModel model = new SkuEsModel();
BeanUtils.copyProperties(i,model);
model.setSkuPrice(i.getPrice());
model.setSkuImg(i.getSkuDefaultImg());
model.setHasStock( (finalStockMap != null)? finalStockMap.get(i.getSkuId()):true );//设置库存信息
//热度评分 0
model.setHotScore(0L);
//查询品牌 和 分类名字信息
BrandEntity brandEntity = brandService.getById(model.getBrandId());
model.setBrandName(brandEntity.getName());
model.setBrandImg(brandEntity.getLogo());
CategoryEntity categoryEntity = categoryService.getById(model.getCatalogId());
model.setCatalogName(categoryEntity.getName());
//设置检索属性
model.setAttrs(attrsList);
return model;
}).collect(Collectors.toList());
//发给es进行保存,远程调用
R r = esFeignService.productStatusUp(collect);
String code = (String) r.get("code");
if (Integer.parseInt(code) == 0){
//调用成功
//todo 修改spu商品状态
this.baseMapper.updateSpuStatus(spuId, ProductConsatnt.StatusEnum.SPU_UP.getCode());
}else {
//todo 调用失败,重复调用,接口幂等性;重试机制
}
}
- 在指定的所有属性集合中,筛选中检索属性
com.achang.achangmall.product.service.impl.AttrServiceImpl
//在指定的所有属性集合中,筛选中检索属性 @Override public ListselectSearchAttrIds(List attrIdList) { List attrEntities = this.baseMapper.selectList(new QueryWrapper () .eq("search_type", 1) .in("attr_id", attrIdList).select("attr_id")); List searchAttrIdList = attrEntities.stream().map(item -> { return item.getAttrId(); }).collect(Collectors.toList()); return searchAttrIdList; }
- 远程调用接口—库存服务
com.achang.achangmall.ware.controller.WareSkuController
//查询sku是否有库存
@PostMapping("/hasstock")
public R getSkuHasStock(@RequestBody List skuIdList){
List vos = wareSkuService.getSkuHasStock(skuIdList);
return R.ok().put("data",vos);
}
com.achang.achangmall.ware.vo.SkuHasStockVo
@Data
public class SkuHasStockVo {
private Long skuId;
private Boolean hasStock;
}
com.achang.achangmall.ware.controller.WareSkuController
//查询sku是否有库存
@PostMapping("/hasstock")
public R getSkuHasStock(@RequestBody List skuIdList){
List vos = wareSkuService.getSkuHasStock(skuIdList);
return R.ok().setData(vos);
}
com.achang.achangmall.ware.service.impl.WareSkuServiceImpl
@Override public ListgetSkuHasStock(List skuIdList) { List collect = skuIdList.stream().map(item -> { SkuHasStockVo vo = new SkuHasStockVo(); Long count = baseMapper.getSkuStock(item); vo.setSkuId(item); vo.setHasStock(count == null?false:count>0); return vo; }).collect(Collectors.toList()); return collect; }
achangmall-ware/src/main/resources/mapper/ware/WareSkuDao.xml
远程调用com.achang.achangmall.product.feign.WareFeignService
@FeignClient("achangmall-ware")
public interface WareFeignService {
@PostMapping("/ware/waresku/hasstock")
public R getSkuHasStock(@RequestBody List skuIdList);
}
远程调用ES添加索引功能
com.achang.achangmall.search.controller.ESSaveController
@RequestMapping("/search")
@RestController
@Slf4j
public class ESSaveController {
@Autowired
private ProductSaveService productSaveService;
//上架商品
@PostMapping("/save/product")
public R productStatusUp(@RequestBody List esModels) {
Boolean flag = false;
try {
flag = productSaveService.productStatusUp(esModels);
} catch (IOException e) {
e.printStackTrace();
log.error("ESSaveController,商品上架错误:{}",e);
return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(),BizCodeEnum.PRODUCT_UP_EXCEPTION.getMsg());
}
if (!flag){
return R.ok();
}else {
return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(),BizCodeEnum.PRODUCT_UP_EXCEPTION.getMsg());
}
}
}
com.achang.achangmall.search.constant.ESConstant
public class ESConstant {
public static final String PRODUCT_INDEX = "product";//sku数据在es中的索引
}
com.achang.achangmall.search.service.impl.ProductSaveServiceImpl
@Slf4j
@Service
public class ProductSaveServiceImpl implements ProductSaveService {
@Autowired
private RestHighLevelClient restHighLevelClient;
//上架商品
@Override
public Boolean productStatusUp(List esModels) throws IOException {
//给es中建立索引
//给es保存数据
BulkRequest bulkRequest = new BulkRequest();
esModels.forEach(item->{
//构造保存请求
IndexRequest indexRequest = new IndexRequest(ESConstant.PRODUCT_INDEX);
String json = JSON.toJSONString(item);
indexRequest.source(json, XContentType.JSON);
indexRequest.id(item.getSkuId().toString());
bulkRequest.add(indexRequest);
});
BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, ElasticsearchConfig.COMMON_OPTIONS);
//todo 批量错误
boolean b = bulk.hasFailures();
List collect = Arrays.stream(bulk.getItems()).map(item -> {
return item.getId();
}).collect(Collectors.toList());
log.info("商品上架完成:【{}】",collect);
return b;
}
}
com.achang.achangmall.product.feign.ESFeignService
@FeignClient("achangmall-search")
public interface ESFeignService {
@PostMapping("/search/save/product")
public R productStatusUp(@RequestBody List esModels);
}
achangmall-product/src/main/resources/mapper/product/SpuInfoDao.xml
update `pms_spu_info` set publish_status =#{code},update_time = NOW() where id = #{spuId}
二、商城业务
明天继续



