一、商品上架
ELK在项目架构中的作用:日志存储和文本检索。上架的商品信息保存到ES方便在网站展示及被检索。
项目整体架构图如下:
1.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.2 商品信息保存到es
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"
}
}
}
}
}
}
index:
默认true,如果为false,表示该字段不会被索引,但是检索结果里面有,但字段本身不能当做检索条件。
doc_values:
默认true,设置为false,表示不可以做排序、聚合以及脚本操作,这样更节省磁盘空间。
还可以通过设定doc_values为true,index为false来让字段不能被搜索但可以用于排序、聚合以及脚本操作。
1.3 es数组的扁平化处理
只需要修改 user 的类型即可:
PUT my-index
{
"mappings": {
"properties": {
"user": {
"type": "nested"
}
}
}
}
执行查询操作,返回0条数据。
1.4 构造基本数据
商品上架接口: POST /product/spuinfo/{spuId}/up
SpuInfoController 层代码
/**
* 商品上架
* @param spuId
* @return
*/
@PostMapping("/{spuId}/up")
public R spuUp(@PathVariable Long spuId){
spuInfoService.productUp(spuId);
return R.ok();
}
sku 在 es 存储的数据
@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;
}
}
业务层代码
@Override
public void productUp(Long spuId) {
//1.查出当前spuId对应的所有sku信息,品牌的名字
List<SkuInfoEntity> skus = skuInfoService.getSkusBySpuId(spuId);
List<Long> skuIdList = skus.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
//4.查询当前sku的所有可以被检索的规格属性
List<ProductAttrValueEntity> baseAttrs = attrValueService.baseAttrListForSpu(spuId);
List<Long> attrIds = baseAttrs.stream().map(attrValue -> {
return attrValue.getAttrId();
}).collect(Collectors.toList());
List<Long> searchAttrIds = attrService.selectSearchAttrs(attrIds);
Set<Long> ids = new HashSet<>(searchAttrIds);
List<SkuEsModel.Attrs> attrsList = baseAttrs.stream().filter(item -> {
return ids.contains(item.getAttrId());
}).map(item -> {
SkuEsModel.Attrs attr = new SkuEsModel.Attrs();
BeanUtils.copyProperties(item, attr);
return attr;
}).collect(Collectors.toList());
//1.发送远程调用,库存系统是否有库存
Map<Long, Boolean> stockMap = null; //先为库存null
try {
//会得到库存数
R r = wareFeignService.getSkusHasStock(skuIdList);
TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>() {
};
stockMap = r.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> upProducts = skus.stream().map(sku -> {
// 组装存储到es的数据
SkuEsModel skuEsModel = new SkuEsModel();
BeanUtils.copyProperties(sku, skuEsModel);
skuEsModel.setSkuPrice(sku.getPrice());
skuEsModel.setSkuImg(sku.getSkuDefaultImg());
//对比SkuInfoEntity SkuEsModel缺少的属性hasStock,hotScore
//设置库存信息
if (finalStockMap == null){
skuEsModel.setHasStock(true);
}else {
skuEsModel.setHasStock(finalStockMap.get(sku.getSkuId()));
}
//2.热度评分 0
skuEsModel.setHotScore(0L);
//3.查询品牌和分类信息
BrandEntity brand = brandService.getById(skuEsModel.getBrandId());
skuEsModel.setBrandName(brand.getName());
skuEsModel.setBrandImg(brand.getLogo());
CategoryEntity category = categoryService.getById(skuEsModel.getCatalogId());
skuEsModel.setCatalogName(category.getName());
//设置检索属性
skuEsModel.setAttrs(attrsList);
return skuEsModel;
}).collect(Collectors.toList());
//5.将数据发送给es进行保存 gulimall-search
R r = searchFeignService.productStatusUp(upProducts);
if (r.getCode() == 0) {
//远程调用成功
//6.修改当前spu的状态
baseMapper.updateSpuStatus(spuId, ProductConstant.StatusEnum.SPU_UP.getCode());
} else {
//远程调用失败
//7.重复调用?接口幂等性;重试机制?
//Feign调用流程
/**
* 1、构造请求数据,将对象转为json;
* RequestTemplate template = buildTemplateFromArgs.create(argv);
* 2、发送请求进行执行
* executeAndDecode(template);
* 3、执行请求会有重试机制
* while(true){
* try{
* executeAndDecode(template);
* }catch(){
* try{retryer.continueOrPropagate(e);}catch(){throw ex;}
* continue;
* }
* }
*/
}
}
查询可以被检索的商品属性
@Override
public List<Long> selectSearchAttrs(List<Long> attrIds) {
// SELECT * FROM pms_attr WHERE attr_id IN (attrIds) AND search_type = 1;
return baseMapper.selectByAttrIds(attrIds);
}
<select id="selectByAttrIds" resultType="java.lang.Long">
SELECT * FROM pms_attr WHERE attr_id IN
<foreach collection="attrIds" item="id" separator="," open="(" close=")">
#{id}
</foreach>>
AND search_type = 1;
</select>
WareSkuController SKU库存控制层
/**
* 查询sku是否有库存
* @param skuIds
* @return
*/
@PostMapping("/hasstock")
public R getSkuHasStock(@RequestBody List<Long> skuIds) {
//sku_id stock
List<SkuHasStockVO> list = wareSkuService.getSkuHasStock(skuIds);
return R.ok().put("data", list);
}
// 注意stock字段为null情况处理,防止空指针情况
@Override
public List<SkuHasStockVO> getSkuHasStock(List<Long> skuIds) {
List<SkuHasStockVO> stockVOList = skuIds.stream().map(sku -> {
// 可以使用分组直接批量查询sku_id 避免多次单次查询,提高查询效率
// SELECT SUM(ifnull(stock,0)-ifnull(stock_locked,0)), sku_id FROM `wms_ware_sku` WHERE sku_id IN(27,28,29) GROUP BY sku_id
SkuHasStockVO vo = new SkuHasStockVO();
Long count = baseMapper.getSkuStock(sku);
vo.setSkuId(sku);
vo.setStock(count > 0);
return vo;
}).collect(Collectors.toList());
return stockVOList;
}
远程查询 sku 的库存信息
@FeignClient("gulimall-ware")
public interface WareFeignService {
/**
* 远程调用返回的数据,解析成我们想要的结果方案:
* 1、R设计的时候可以加上泛型
* 2、直接返回我们想要的结果
* 3、自己封装解析结果
* @param skuIds
* @return
*/
@PostMapping("/ware/waresku/hasstock")
R getSkusHasStock(@RequestBody List<Long> skuIds);
}
保存数据到ES接口
@Slf4j
@RestController
@RequestMapping("/search/save")
public class ElasticSaveController {
@Autowired
ProductSaveService productSaveService;
/**
* 上架商品
* @param skuEsModels
* @return
*/
@PostMapping("/product")
public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels){
boolean result = false;
try {
result = productSaveService.productStatusUp(skuEsModels);
}catch (Exception e){
log.error("ElasticSaveController商品上架错误:{}",e);
return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(),BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());
}
if (!result){
return R.ok();
}else {
return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(),BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());
}
}
}
商品保存到 es 方法
@Slf4j
@Service
public class ProductSaveServiceImpl implements ProductSaveService {
@Resource
private RestHighLevelClient restHighLevelClient;
@Override
public boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {
//保存到es
//1、给es中建立索引。product 建立好映射关系。
//2、给es中保存这些数据
//BulkRequest bulkRequest, RequestOptions options
BulkRequest bulkRequest = new BulkRequest();
for (SkuEsModel model : skuEsModels) {
//1、构造保存请求
IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);//传入sku数据在es中的索引
indexRequest.id(model.getSkuId().toString());
String s = JSON.toJSONString(model);
indexRequest.source(s, XContentType.JSON);
bulkRequest.add(indexRequest);
}
BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, ElasticsearchConfig.COMMON_OPTIONS);
//TODO 1、如果批量错误
boolean result = bulk.hasFailures();
List<String> collect = Arrays.stream(bulk.getItems()).map(item -> {
return item.getId();
}).collect(Collectors.toList());
log.info("商品上架完成:{},返回数据:{}",collect,bulk.toString());
return result;
}
}