- 1 sku在es中的存储模型分析
- 1.1 商品Mapping
- 2 nested数据类型
- 3 商品上架服务
- 3.1 远程调用查询库存服务
- 3.2 实现es存储业务
- 3.2.1 远程调用接口
- 3.3 商品上传业务逻辑
- 3.4 测试
商品上架
上架的商品才可以在网站展示
上架的商品需要可以被检索
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的数据,传输阻塞时间会很长,业务更加无法继续。
所以,我们如下设计,这样才是文档区别于关系型数据库的地方,宽表设计,不能去考虑数据库范式。
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
},
"attr": {
"type": "nested",
"properties": {
"attrId": {
"type": "long"
},
"attrName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"attrValue": {
"type": "keyword"
}
}
}
}
}
}
2 nested数据类型
Es会将存入的数组进行扁平化处理
例如:存入数组
PUT my_index/_doc/1
{
"group": "fans",
"user": [
{
"first": "John",
"last": "Smith"
},
{
"first": "Alice",
"last": "White"
}
]
}
将会被扁平化为:
{
"group": "fans",
"user.first": ["Alice", "John"],
"user.last": ["Smith", "White"]
}
设置为nested类型可以防止扁平化处理
PUT my_index
{
"mappings": {
"properties": {
"user": {
"type": "nested"
}
}
}
}
3 商品上架服务
3.1 远程调用查询库存服务
1、创建SkuHasStockVo,路径com/atguigu/common/to/es/SkuHasStockVo.java
@Data
public class SkuHasStockVo {
private Long skuId;
private Boolean hasStock;
}
2、创建getSkuHasStock,路径:com/atguigu/gulimall/ware/controller/WareSkuController.java
// 查询sku是否有库存
@RequestMapping("/hasstock")
public R getSkuHasStock(@RequestBody List skuIds){
List vos = wareSkuService.getSkuHasStock(skuIds);
return R.ok().put("data", vos);
}
3、实现getSkuHasStock方法
@Override public ListgetSkuHasStock(List skuIds) { List collect = skuIds.stream().map(skuId -> { SkuHasStockVo vo = new SkuHasStockVo(); // 查询当前sku的总库存量 Long count = baseMapper.getSkuStock(skuId); vo.setSkuId(skuId); vo.setHasStock(count != null && count > 0); return vo; }).collect(Collectors.toList()); return collect; }
4、实现getSkuStock方法,接口上记得加上@Param注解
SELECT SUM(stock-stock_locked) FROM `wms_ware_sku` WHERe sku_id=#{skuId}
5、创建WareFeignService远程调用接口,路径:com/atguigu/gulimall/product/feign/WareFeignService.java
@FeignClient("gulimall-ware")
public interface WareFeignService {
@PostMapping("/ware/waresku/hasstock")
R getSkuHasStock(@RequestBody List skuIds);
}
3.2 实现es存储业务
1、创建ProductSaveService,路径:com/atguigu/gulimall/search/service/ProductSaveService.java
2、建立常量类EsConstant,路径:com/atguigu/gulimall/search/constant/EsConstant.java
public class EsConstant {
public static final String PRODUCT_INDEX = "product"; // sku数据在es中的索引
}
3、添加异常枚举,路径:com/atguigu/common/exception/BizCodeEnume.java
public enum BizCodeEnume {
UNKNOW_EXCEPTION(10000,"系统未知异常"),
VAILD_EXCEPTION(10001,"参数格式校验失败"),
PRODUCT_UP_EXCEPTION(11000, "商品上架异常");
private int code;
private String msg;
BizCodeEnume(int code,String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
4、创建ElasticSaveController,添加productStatusUp方法,路径:com/atguigu/gulimall/search/controller/ElasticSaveController.java
@Slf4j
@RestController
@RequestMapping("/search/save")
public class ElasticSaveController {
@Autowired
ProductSaveService productSaveService;
@PostMapping("/product")
public R productStatusUp(@RequestBody List skuEsModels) {
boolean b = false;
try {
b = productSaveService.productStatusUp(skuEsModels);
} catch (Exception e) {
log.error("ElasticSaveController商品上架错误: {}", e);
return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());
}
if (b) {
return R.ok();
} else {
return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());
}
}
}
5、建立映射关系,在kibana中执行下列代码
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
},
"attr": {
"type": "nested",
"properties": {
"attrId": {
"type": "long"
},
"attrName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"attrValue": {
"type": "keyword"
}
}
}
}
}
}
6、实现productStatusUp方法,路径:com/atguigu/gulimall/search/service/impl/ProductSaveServiceImpl.java
@Autowired RestHighLevelClient restHighLevelClient; @Override public boolean productStatusUp(List3.2.1 远程调用接口skuEsModels) throws IOException { // 1. 给es中建立索引,product,建立好映射关系 // 2. 保存数据 BulkRequest bulkRequest = new BulkRequest(); for (SkuEsModel model : skuEsModels) { // 构造保存请求 IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX); indexRequest.id(model.getSkuId().toString()); String s = JSON.toJSONString(model); indexRequest.source(s, XContentType.JSON); bulkRequest.add(indexRequest); } BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS); boolean b = bulk.hasFailures(); if (b) { bulk.buildFailureMessage(); } else { List collect = Arrays.stream(bulk.getItems()).map(BulkItemResponse::getId).collect(Collectors.toList()); log.info("商品上架完成: {}", collect); } return !b; }
1、创建SearchFeignService接口,路径:com/atguigu/gulimall/product/feign/SearchFeignService.java
@FeignClient("gulimall-search")
public interface SearchFeignService {
@PostMapping("/search/save/product")
R productStatusUp(@RequestBody List skuEsModels);
}
3.3 商品上传业务逻辑
1、添加状态码,路径:com/atguigu/common/constant/ProductConstant.java
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 StatusEnum{
NEW_SPU(0,"新建"),SPU_UP(1,"商品上架"),SPU_DOWN(2, "商品下架");
private int code;
private String msg;
StatusEnum(int code,String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
}
2、在product模块下的SpuInfoController里创建spuUp方法
@PostMapping("/{spuId}/up")
public R spuUp(@PathVariable("spuId") Long spuId){
spuInfoService.up(spuId);
return R.ok();
}
3、创建SkuEsModel,路径:com/atguigu/common/to/es/SkuEsModel.java
@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;
@Data
public static class Attrs{
private Long attrId;
private String attrName;
private String attrValue;
}
}
4、实现up方法
@Autowired
BrandService brandService;
@Autowired
CategoryService categoryService;
@Autowired
WareFeignService wareFeignService;
@Autowired
SearchFeignService searchFeignService;
@Override
@Transactional
public void up(Long spuId) {
// 1. 查出当前spuId对应的所有sku信息,品牌的名字
List skus = skuInfoService.getSkusBySpuId(spuId);
// 4. 查询当前sku的所有可以被用来检索的规格属性
List productAttrValueEntities = productAttrValueService.baseAttrListForSpu(spuId);
// 4.1. 获取所有对应SPUId的attrId
List attrIds = productAttrValueEntities.stream().map(ProductAttrValueEntity::getAttrId).collect(Collectors.toList());
// 4.2. 在表pms_attr查找所有对应attr_id的实体
Collection attrEntities = attrService.listByIds(attrIds);
// 4.3. 过滤掉所有search_type为0的实体,并封装为SkuEsModel.Attrs
List skuEsModelAttrs = attrEntities.stream().filter(item -> item.getSearchType() == 1).map(item -> {
SkuEsModel.Attrs attrs = new SkuEsModel.Attrs();
BeanUtils.copyProperties(item, attrs);
attrs.setAttrValue(item.getValueSelect());
return attrs;
}).collect(Collectors.toList());
// 获取哦skus中的所有skuId作为集合
List skuIdList = skus.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
// TODO 发送远程调用,库存系统查询是否有库存
Map stockMap = null;
try {
R r = wareFeignService.getSkuHasStock(skuIdList);
Object data = r.get("data");
// 需要导入fastjson包
String s = JSON.toJSONString(data);
TypeReference> typeReference = new TypeReference>() {};
// com.alibaba.fastjson.TypeReference;
List skuHasStockVos = JSON.parseObject(s, typeReference);
// 将skuHasStockVos映射为map,skuId为键,HasStock为值
stockMap = skuHasStockVos.stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, SkuHasStockVo::getHasStock));
} catch (Exception e) {
log.error("库存查询服务出现异常,原因{}", e);
}
// 2. 封装每个sku的信息
Map finalStockMap = stockMap;
List collect = skus.stream().map(sku -> {
// 组装需要的数据
SkuEsModel skuEsModel = new SkuEsModel();
BeanUtils.copyProperties(sku, skuEsModel);
// 手动赋值其他属性
// TODO 热度评分
skuEsModel.setHotScore(0L);
skuEsModel.setSkuPrice(sku.getPrice());
skuEsModel.setSkuImg(sku.getSkuDefaultImg());
// 设置库存
if (finalStockMap == null) {
skuEsModel.setHasStock(true);
} else {
skuEsModel.setHasStock(finalStockMap.get(sku.getSkuId()));
}
// TODO 查询品牌和分类的名字信息
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(skuEsModelAttrs);
return skuEsModel;
}).collect(Collectors.toList());
// TODO 将数据发送给es进行保存
R r = searchFeignService.productStatusUp(collect);
if (r.getCode() == 0) {
// 远程调用成功
// TODO 修改当前spu的状态
baseMapper.updateSpuStatus(spuId, ProductConstant.StatusEnum.SPU_UP.getCode());
} else {
// 远程调用失败
// TODO 重复调用,接口幂等性,重试机制
}
}
5、实现getSkusBySpuId方法
@Override public ListgetSkusBySpuId(Long spuId) { List list = this.list(new QueryWrapper ().eq("spu_id", spuId)); return list; }
6、实现updateSpuStatus方法,接口上记得加@Param注解
3.4 测试UPDATE `pms_spu_info` SET publish_status=#{code},update_time=NOW() WHERe id=#{spuId}
记得先将数据表pms_attr里的属性设置为可被检索
点击上架
到kibana页面查询索引
GET product/_search



