一、product-es准备
P128
ES在内存中,所以在检索中优于mysql。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都存储规格参数(如尺寸),会有冗余存储,因为每个sku对应的spu的规格参数都一样
方案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索引
最终选用的数据模型:
- { “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" }, # 视频中有false
"saleCount":{ "type":"long" },
"hasStock": { "type": "boolean" },
"hotScore": { "type": "long" },
"brandId": { "type": "long" },
"catalogId": { "type": "long" },
"brandName": {"type": "keyword"}, # 视频中有false
"brandImg":{
"type": "keyword",
"index": false, # 不可被检索,不生成index,只用做页面使用
"doc_values": false # 不可被聚合,默认为true
},
"catalogName": {"type": "keyword" }, # 视频里有false
"attrs": {
"type": "nested",
"properties": {
"attrId": {"type": "long" },
"attrName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"attrValue": {"type": "keyword" }
}
}
}
}
}
冗余存储的字段:不用来检索,也不用来分析,节省空间
二、nested嵌入式对象
属性是"type": “nested”,因为是内部的属性进行检索
数组类型的对象会被扁平化处理(对象的每个属性会分别存储到一起)
user.name=["aaa","bbb"]
user.addr=["ccc","ddd"]
这种存储方式,可能会发生如下错误:
错误检索到{aaa,ddd},这个组合是不存在的
数组的扁平化处理会使检索能检索到本身不存在的,为了解决这个问题,就采用了嵌入式属性,数组里是对象时用嵌入式属性(不是对象无需用嵌入式属性)
三、商品上架(ES保存)
@Override // SpuInfoServiceImpl
public void up(Long spuId) {
// 1 组装数据 查出当前spuId对应的所有sku信息
List<SkuInfoEntity> skus = skuInfoService.getSkusBySpuId(spuId);
// 查询这些sku是否有库存
List<Long> skuids = skus.stream().map(sku -> sku.getSkuId()).collect(Collectors.toList());
// 2 封装每个sku的信息
// 3.查询当前sku所有可以被用来检索的规格属性
List<ProductAttrValueEntity> baseAttrs = attrValueService.baseAttrListForSpu(spuId);
// 得到基本属性id
List<Long> attrIds = baseAttrs.stream().map(attr -> attr.getAttrId()).collect(Collectors.toList());
// 过滤出可被检索的基本属性id,即search_type = 1 [数据库中目前 4、5、6、11不可检索]
Set<Long> ids = new HashSet<>(attrService.selectSearchAttrIds(attrIds));
// 可被检索的属性封装到SkuEsModel.Attrs中
List<SkuEsModel.Attrs> attrs = baseAttrs.stream()
.filter(item -> ids.contains(item.getAttrId()))
.map(item -> {
SkuEsModel.Attrs attr = new SkuEsModel.Attrs();
BeanUtils.copyProperties(item, attr);
return attr;
}).collect(Collectors.toList());
// 每件skuId是否有库存
Map<Long, Boolean> stockMap = null;
try {
// 3.1 远程调用库存系统 查询该sku是否有库存
R hasStock = wareFeignService.getSkuHasStock(skuids);
// 构造器受保护 所以写成内部类对象
stockMap = hasStock.getData(new TypeReference<List<SkuHasStockVo>>() {})
.stream()
.collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
log.warn("服务调用成功" + hasStock);
} catch (Exception e) {
log.error("库存服务调用失败: 原因{}", e);
}
Map<Long, Boolean> finalStockMap = stockMap;//防止lambda中改变
// 开始封装es
List<SkuEsModel> skuEsModels = skus.stream().map(sku -> {
SkuEsModel esModel = new SkuEsModel();
BeanUtils.copyProperties(sku, esModel);
esModel.setSkuPrice(sku.getPrice());
esModel.setSkuImg(sku.getSkuDefaultImg());
// 4 设置库存,只查是否有库存,不查有多少
if (finalStockMap == null) {
esModel.setHasStock(true);
} else {
esModel.setHasStock(finalStockMap.get(sku.getSkuId()));
}
// TODO 1.热度评分 刚上架是0
esModel.setHotScore(0L);
// 设置品牌信息
BrandEntity brandEntity = brandService.getById(esModel.getBrandId());
esModel.setBrandName(brandEntity.getName());
esModel.setBrandImg(brandEntity.getLogo());
// 查询分类信息
CategoryEntity categoryEntity = categoryService.getById(esModel.getCatalogId());
esModel.setCatalogName(categoryEntity.getName());
// 保存商品的属性, 查询当前sku的所有可以被用来检索的规格属性,同一spu都一样,在外面查一遍即可
esModel.setAttrs(attrs);
return esModel;
}).collect(Collectors.toList());
// 5.发给ES进行保存 gulimall-search
R r = searchFeignService.productStatusUp(skuEsModels);
if (r.getCode() == 0) {
// 远程调用成功
baseMapper.updateSpuStatus(spuId, ProductConstant.StatusEnum.SPU_UP.getCode());
} else {
// 远程调用失败 TODO 接口幂等性 重试机制
/**
* Feign 的调用流程 Feign有自动重试机制
* 1. 发送请求执行
* 2.
*/
}
}
@Slf4j
@Service
public class ProductSaveServiceImpl implements ProductSaveService {
@Resource
private RestHighLevelClient client;
/**
* 将数据保存到ES
* 用bulk代替index,进行批量保存
* BulkRequest bulkRequest, RequestOptions options
*/
@Override // ProductSaveServiceImpl
public boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {
// 1. 批量保存
BulkRequest bulkRequest = new BulkRequest();
// 2.构造保存请求
for (SkuEsModel esModel : skuEsModels) {
// 设置es索引 gulimall_product
IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
// 设置索引id
indexRequest.id(esModel.getSkuId().toString());
// json格式
String jsonString = JSON.toJSONString(esModel);
indexRequest.source(jsonString, XContentType.JSON);
// 添加到文档
bulkRequest.add(indexRequest);
}
// bulk批量保存
BulkResponse bulk = client.bulk(bulkRequest, GuliESConfig.COMMON_OPTIONS);
// TODO 是否拥有错误
boolean hasFailures = bulk.hasFailures();
if(hasFailures){
List<String> collect = Arrays.stream(bulk.getItems()).map(item -> item.getId()).collect(Collectors.toList());
log.error("商品上架错误:{}",collect);
}
return hasFailures;
}
}
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":{
"type": "boolean"
},
"hotScore":{
"type": "long"
},
"brandId":{
"type": "long"
},
"catalogId":{
"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":{
"type":"long"
},
"attrName":{
"type":"keyword",
"index":false,
"doc_values": false
},
"attrValue":{
"type":"keyword"
}
}
}
}
}
}