目录
1、ES回顾
2、ES整合商品上架
2.1、分析
2.2、创建sku的es索引库
2.2.1、两种索引库设计方案分析
2.2.2、最终选用的索引库方案,nested类型
2.3、SkuEsModel模型类
2.4、【库存模块】库存量查询
2.5、【查询模块】保存ES文档
2.5.1、常量类
2.5.2、controller
2.5.3、service
2.6、【商品模块】上架单个spu
2.6.1、controller
2.6.2、远程调用库存模块
2.6.3、【公共模块】商品常量类,添加上传成功或失败的状态码
2.6.4、service
2.6.5、测试
2.6.6、修改结果类R
elasticsearch基础1——索引、文档_elasticsearch索引 文档_vincewm的博客-CSDN博客
elasticsearch基础2——DSL查询文档,黑马旅游项目查询功能_elasticsearch查询文档_vincewm的博客-CSDN博客elasticsearch基础3——聚合、补全、集群_vincewm的博客-CSDN博客elasticsearch基础2——DSL查询文档,黑马旅游项目查询功能_elasticsearch查询文档_vincewm的博客-CSDN博客
es在整个项目中的应用:
1.对商品的全文检索功能
2.对日志的全文检索功能
为什么不用mysql?
es比mysql检索功能强大,并且对于庞大的检索数据,es性能更好,因为mysql是存在内存中的,而es可以分片存储。
需求:
索引库设计方案1(推荐,空间换时间):规格参数放在sku里
缺点:如果每个sku都存储规格参数(如尺寸),会有冗余存储,因为每个spu下面的sku规格参数都一样。例如spu“华为14Pro”下的sku“红色华为14Pro”,以及spu“华为手机”下的sku“蓝色华为14Pro”,这两个sku的规格参数“CPU:A14”相等。
{
skuId:1
spuId:11
skyTitile:华为xx
price:999
saleCount:99
attr:[
{尺寸:5},
{CPU:高通945},
{分辨率:全高清}
]
}
索引库设计方案2(不推荐,传输的数据量大):规格参数和sku分离
sku索引
{
spuId:1
skuId:11
}
attr索引
{
skuId:11
attr:[
{尺寸:5},
{CPU:高通945},
{分辨率:全高清}
]
}
结论:如果将规格参数单独建立索引,会出现检索时出现大量数据传输的问题,会引起网络故障。
所以我们选方案一,用空间换时间
{ “type”: “keyword” }, 保持数据精度问题,可以检索,但不分词
“analyzer”: “ik_smart” 中文分词器
“index”: false, 不可被检索,不生成index
“doc_values”: false 默认为true,不可被聚合,es就不会维护一些聚合的信息
这个数据模型要先在es中建立
注意:
为了防止对象数组扁平化,商品属性字段类型设为nested类型。
es数组的扁平化处理:es存储对象数组时,它会将数组扁平化,也就是说将对象数组的每个属性抽取出来,作为一个数组。因此会出现查询紊乱的问题。
示例:下面user字段是对象数组类型,因为数组扁平化处理,下面结果跟期望查询结果不符:
PUT product
{
"mappings":{
"properties": {
"skuId":{ "type": "long" }, #商品sku
"spuId":{ "type": "keyword" }, #当前sku所属的spu。
"skuTitle": {
"type": "text",
"analyzer": "ik_smart" #只有sku的标题需要被分词
},
"skuPrice": { "type": "keyword" },
"skuImg" : { "type": "keyword" },
"saleCount":{ "type":"long" },
"hasStock": { "type": "boolean" }, #是否有库存。在库存模块添加此商品库存后,此字段更为true
"hotScore": { "type": "long" },
"brandId": { "type": "long" },
"catalogId": { "type": "long" },
"brandName": {"type": "keyword"},
"brandImg":{
"type": "keyword",
"index": false, #不可被检索
"doc_values": false #不可被聚合。doc_values默认为true
},
"catalogName": {"type": "keyword" },
"attrs": {
"type": "nested", #对象数组防止扁平化,不能用object类型
"properties": {
"attrId": {"type": "long" },
"attrName": {
"type": "keyword",
"index": false, #在后面“商城业务-检索服务”开发时这里要去掉
"doc_values": false #在后面“商城业务-检索服务”开发时这里要去掉
},
"attrValue": {"type": "keyword" }
}
}
}
}
}
商品上架需要在es中保存spu信息并更新spu状态信息,所以我们就建立专门的vo来接收
SkuEsModel
写在common模块
@Data
public class SkuEsModel { //common中
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 Attr{
private Long attrId;
private String attrName;
private String attrValue;
}
}
查询sku列表是否有库存:
上架的话需要确定库存,所以调用ware微服务来检测是否有库存
@RestController
@RequestMapping("ware/waresku")
public class WareSkuController {
@Autowired
private WareSkuService wareSkuService;
@PostMapping(value = "/hasStock")
public R getSkuHasStock(@RequestBody List skuIds) {
List vos = wareSkuService.getSkuHasStock(skuIds);
return R.ok().setData(vos);
}
}
实现也比较好理解,就是先用自定义的mapper查有没有库存
有的话,给库存赋值,并收集成集合
@Override
public List getSkuHasStock(List skuIds) {
List skuHasStockVos = skuIds.stream().map(item -> {
//根据sku_id查库存,要写mapper,主要为了真实库存减去锁定库存
Long count = this.baseMapper.getSkuStock(item);
SkuHasStockVo skuHasStockVo = new SkuHasStockVo();
skuHasStockVo.setSkuId(item);
skuHasStockVo.setHasStock(count == null ? false : count > 0);
return skuHasStockVo;
}).collect(Collectors.toList());
return skuHasStockVos;
}
自定义mapper
这里的库存并不是简单查一下库存表,需要自定义一个简单的sql。用库存减去锁定的库存即可得出!
public class EsConstant {
//在es中的索引
public static final String PRODUCT_INDEX = "gulimall_product";
public static final Integer PRODUCT_PAGESIZE = 16;
}
ElasticSaveController
package com.xxx.gulimall.search.controller;
@Slf4j
@RequestMapping(value = "/search/save")
@RestController
public class ElasticSaveController {
@Autowired
private ProductSaveService productSaveService;
/**
* 上架商品
* @param skuEsModels
* @return
*/
@PostMapping(value = "/product")
public R productStatusUp(@RequestBody List skuEsModels) {
boolean status=false;
try {
status = productSaveService.productStatusUp(skuEsModels);
} catch (IOException e) {
//log.error("商品上架错误{}",e);
return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(),BizCodeEnum.PRODUCT_UP_EXCEPTION.getMessage());
}
if(status){
return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(),BizCodeEnum.PRODUCT_UP_EXCEPTION.getMessage());
}else {
return R.ok();
}
}
}
使用BulkRequest ,批量保存sku_es模型类列表到索引库
@Slf4j
@Service("productSaveService")
public class ProductSaveServiceImpl implements ProductSaveService {
@Autowired
private RestHighLevelClient esRestClient;
@Override
public boolean productStatusUp(List skuEsModels) throws IOException {
//1.在es中建立索引,建立号映射关系(doc/json/product-mapping.json)[kibana中执行product-mapping.txt,需要ES安装IK分词器]
//2. 在ES中保存这些数据
BulkRequest bulkRequest = new BulkRequest();
for (SkuEsModel skuEsModel : skuEsModels) {
//构造保存请求
IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
indexRequest.id(skuEsModel.getSkuId().toString());
String jsonString = JSON.toJSONString(skuEsModel);
indexRequest.source(jsonString, XContentType.JSON);
bulkRequest.add(indexRequest);
}
BulkResponse bulk = esRestClient.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
//TODO 如果批量错误
boolean hasFailures = bulk.hasFailures();
List collect = Arrays.asList(bulk.getItems()).stream().map(item -> {
return item.getId();
}).collect(Collectors.toList());
log.info("商品上架完成:{}",collect);
return hasFailures;
}
}
SpuInfoController上架
/**
* 商品上架
*/
@PostMapping("/{spuId}/up")
public R spuUp(@PathVariable("spuId") Long spuId){
spuInfoService.up(spuId);
return R.ok();
}
在商品模块的feign包下:
@FeignClient("gulimall-ware")
public interface WareFeignService {
@PostMapping(value = "/ware/waresku/hasStock")
R getSkuHasStock(@RequestBody List skuIds);
}
商品模块启动类:
@EnableFeignClients(basePackages = "com.xunqi.gulimall.product.feign")
然后service里就能直接@Autowired注入了。
public class ProductConstant {
public enum AttrEnum {
ATTR_TYPE_BASE(1,"基本属性"),
ATTR_TYPE_SALE(0,"销售属性");
private int code;
private String msg;
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
AttrEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
public enum ProductStatusEnum {
NEW_SPU(0,"新建"),
SPU_UP(1,"商品上架"),
SPU_DOWN(2,"商品下架"),
;
private int code;
private String msg;
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
ProductStatusEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
}
业务流程:
SpuInfoServiceImpl
@Override
public void up(Long spuId) {
//1.获得spu对应的sku集合
List skuInfoEntities = skuInfoService.getSkusBySpuId(spuId);
//2.获得spu的基础属性实体集合
List baseAttrs = productAttrValueService.baseAttrListforSpu(spuId);
//3.获得基本属性中可搜索的属性id
//3.1获得spu基础属性实体集合中的属性id集合
List attrIds = baseAttrs.stream().map(attr -> {
return attr.getAttrId();
}).collect(Collectors.toList());
//3.2获得可搜索属性实体类对象
List searchAttrIds = attrService.selectSearchAttrs(attrIds);
//3.3将它们转化为set集合
Set idSet = searchAttrIds.stream().collect(Collectors.toSet());
//3.4对所有基础属性实体过滤,第一步是只保留可搜索属性实体类对象,第二步是给这些对象中的Attrs对象赋值,最后收集为attrsList
List attrsList = baseAttrs.stream().filter(item -> {
return idSet.contains(item.getAttrId());
}).map(item -> {
SkuEsModel.Attrs attrs = new SkuEsModel.Attrs();
BeanUtils.copyProperties(item, attrs);
return attrs;
}).collect(Collectors.toList());
//收集所有skuId的集合
List skuIdList = skuInfoEntities.stream()
.map(SkuInfoEntity::getSkuId)
.collect(Collectors.toList());
//TODO 1、发送远程调用,库存系统查询是否有库存
Map stockMap = null;
try {
R skuHasStock = wareFeignService.getSkuHasStock(skuIdList);
TypeReference> typeReference = new TypeReference>() {};
stockMap = skuHasStock.getData(typeReference).stream()
.collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
} catch (Exception e) {
log.error("库存服务查询异常:原因{}",e);
}
//2、封装每个sku的信息
Map finalStockMap = stockMap;
List collect = skuInfoEntities.stream().map(sku -> {
//组装需要的数据
SkuEsModel esModel = new SkuEsModel();
esModel.setSkuPrice(sku.getPrice());
esModel.setSkuImg(sku.getSkuDefaultImg());
//设置库存信息
if (finalStockMap == null) {
esModel.setHasStock(true);
} else {
esModel.setHasStock(finalStockMap.get(sku.getSkuId()));
}
//TODO 2、热度评分。0
esModel.setHotScore(0L);
//TODO 3、查询品牌和分类的名字信息
BrandEntity brandEntity = brandService.getById(sku.getBrandId());
esModel.setBrandName(brandEntity.getName());
esModel.setBrandId(brandEntity.getBrandId());
esModel.setBrandImg(brandEntity.getLogo());
CategoryEntity categoryEntity = categoryService.getById(sku.getCatalogId());
esModel.setCatalogId(categoryEntity.getCatId());
esModel.setCatalogName(categoryEntity.getName());
//设置检索属性
esModel.setAttrs(attrsList);
BeanUtils.copyProperties(sku,esModel);
return esModel;
}).collect(Collectors.toList());
//TODO 5、将数据发给es进行保存:mall-search
R r = searchFeignService.productStatusUp(collect);
if (r.getCode() == 0) {
//远程调用成功
//TODO 6、修改当前spu的状态,具体代码看代码块后面SpuInfoDao.xml
this.baseMapper.updaSpuStatus(spuId, ProductConstant.ProductStatusEnum.SPU_UP.getCode());
} else {
//远程调用失败
//TODO 7、重复调用?接口幂等性:重试机制
}
}
SpuInfoDao更新spu状态
UPDATE pms_spu_info SET publish_status = #{code} ,update_time = NOW() WHERE id = #{spuId}
商品上架用到了三个微服务,分别是product、ware、search
那我们分别debug启动它们,然后在这些微服务中使用的方法中打上断点,查看调用流程
获得spu对应的sku集合
获得spu的基础属性实体集合
基础属性如下:
给SkuEsModel.Attrs对象赋值
测试
public T getData(String key, TypeReference typeReference) {
Object data = get(key);// 默认是map类型,springmvc做的
String jsonStr = JSON.toJSONString(data);
T t = JSON.parseObject(jsonStr, typeReference);
return t;
}
// 利用fastJson进行逆转
// 这里要声明泛型,这个泛型只跟方法有关,跟类无关。
// 例如类上有个泛型,这里可以使用类上的泛型,就不用声明
public T getData(TypeReference typeReference) {
Object data = get("data");// 默认是map类型,springmvc做的
String jsonStr = JSON.toJSONString(data);
T t = JSON.parseObject(jsonStr, typeReference);
return t;
}
public R setData(Object data) {
put("data", data);
return this;
}