谷粒商城分布式高级(三)—— 商城业务(商品上架)
一、商品上架
上架的商品才可以在網(wǎng)站展示。
上架的商品需要可以被檢索。
1、商品 Mapping
分析:商品上架在 es 中是存 sku 還是 spu? (1)檢索的時候輸入名字,是需要按照 sku 的 title 進行全文檢索的 (2)檢索使用商品規(guī)格,規(guī)格是 spu 的公共屬性,每個 spu 是一樣的 (3)按照分類 id 進去的都是直接列出 spu 的,還可以切換 (4)我們?nèi)绻麑?sku 的全量信息保存到 es 中(包括 spu 屬性)就太多量字段了 (5)我們?nèi)绻麑?spu 以及他包含的 sku 信息保存到 es 中,也可以方便檢索,但是 sku 屬于 spu 的級聯(lián)動,在 es 中需要 nested 模型,這種性能差點 (6)但是存儲與檢索我們必須性能折中 (7)如果我們分拆存儲,spu 和 attr 一個索引,sku 單獨一個所以可能涉及的問題:檢索商品的名字,如“手機”,對應(yīng)的 spu 有很多,我們要分析出這些 spu 的所有級聯(lián)屬性,再做一次查詢,就必須將所有 spu_id 都發(fā)出去。假設(shè)有 1 萬個數(shù)據(jù),數(shù)據(jù)傳輸一次就 10000*4=4MB;并發(fā)情況下假設(shè) 1000 檢索請求,那就是 4GB 的數(shù)據(jù),傳輸阻塞時間會很長,業(yè)務(wù)更加無法繼續(xù)。
方案1:
{
skuId: 1,
spuId: 11,
skyTitile: 華為xx,
price: 999,
saleCount: 99,
attr: [{
尺寸: 5
}, {
CPU: 高通945
}, {
分辨率: 全高清
}]
}
缺點:如果每個sku都存儲規(guī)格參數(shù)(如尺寸),會有冗余存儲,因為每個 spu 對應(yīng)的 sku 的規(guī)格參數(shù)都一樣
方案2:
sku索引
{
spuId: 1,
skuId: 11
}
attr索引
{
skuId: 11,
attr: [{
尺寸: 5
}, {
CPU: 高通945
}, {
分辨率: 全高清
}]
}
先找到4000個符合要求的spu,再根據(jù)4000個spu查詢對應(yīng)的屬性,封裝了4000個id,long 8B*4000=32000B=32KB 1K個人檢索,就是32MB
結(jié)論:如果將規(guī)格參數(shù)單獨建立索引,會出現(xiàn)檢索時出現(xiàn)大量數(shù)據(jù)傳輸?shù)膯栴},會引起網(wǎng)絡(luò)網(wǎng)絡(luò) 因此選用方案1,以空間換時間
所以,我們?nèi)缦略O(shè)計,這樣才是文檔區(qū)別于關(guān)系型數(shù)據(jù)庫的地方,寬表設(shè)計,不能去考慮數(shù)據(jù)庫范式。
建立product索引
最終選用的數(shù)據(jù)模型:
(1)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,
"doc_values": false
},
"catalogName": {
"type": "keyword"
},
"attrs": {
"type": "nested",
"properties": {
"attrId": {
"type": "long"
},
"attrName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"attrValue": {
"type": "keyword"
}
}
}
}
}
}
其中
(1)“type”: “keyword” 保持數(shù)據(jù)精度問題,可以檢索,但不分詞
(2)“index”:false 代表不可被檢索
(3)“doc_values”: false 不可被聚合,es就不會維護一些聚合的信息 冗余存儲的字段:不用來檢索,也不用來分析,節(jié)省空間
庫存是bool。 檢索品牌id,但是不檢索品牌名字、圖片 用skuTitle檢索
(2)nested 數(shù)據(jù)類型場景
屬性是"type": “nested”,因為是內(nèi)部的屬性進行檢索
數(shù)組類型的對象會被扁平化處理(對象的每個屬性會分別存儲到一起)
user.name=["aaa","bbb"]
user.addr=["ccc","ddd"]
這種存儲方式,可能會發(fā)生如下錯誤:
錯誤檢索到{aaa,ddd},這個組合是不存在的
數(shù)組的扁平化處理會使檢索能檢索到本身不存在的,為了解決這個問題,就采用了嵌入式屬性,數(shù)組里是對象時用嵌入式屬性(不是對象無需用嵌入式屬性)
nested閱讀:https://blog.csdn.net/weixin_40341116/article/details/80778599
使用聚合:https://blog.csdn.net/kabike/article/details/101460578
2、上架細節(jié)
上架是將后臺的商品放在 es 中可以提供檢索和查詢功能
(1)hasStock:代表是否有庫存。默認上架的商品都有庫存。如果庫存無貨的時候才需要更新一下 es
(2)庫存補上以后,也需要重新更新一下 es
(3)hotScore 是熱度值,我們只模擬使用點擊率更新熱度。點擊率增加到一定程度才更新熱度值
(4)下架就是從 es 中移除檢索項,以及修改 mysql 狀態(tài)
商品上架步驟:
(1)先在 es 中按照之前的 mapping 信息,建立 product 索引。
(2)點擊上架,查詢出所有 sku 的信息,保存到 es 中
(3)es 保存成功返回,更新數(shù)據(jù)庫的上架狀態(tài)信息
3、數(shù)據(jù)一致性
(1)商品無庫存的時候需要更新 es 的庫存信息 (2)商品有庫存也要更新 es 的信息
4、上架流程
product里組裝好,search里上架
(1)gulimall-common 更新文件 com.atguigu.common.utils.R
將R設(shè)置成泛型的
package com.atguigu.common.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.apache.http.HttpStatus;
import java.util.HashMap;
import java.util.Map;
public class R extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
public R setData(Object data) {
put("data",data);
return this;
}
//利用fastjson進行反序列化
public <T> T getData(TypeReference<T> typeReference) {
Object data = get("data"); //默認是map
String jsonString = JSON.toJSONString(data);
T t = JSON.parseObject(jsonString, typeReference);
return t;
}
//利用fastjson進行反序列化
public <T> T getData(String key,TypeReference<T> typeReference) {
Object data = get(key); //默認是map
String jsonString = JSON.toJSONString(data);
T t = JSON.parseObject(jsonString, typeReference);
return t;
}
public R() {
put("code", 0);
put("msg", "success");
}
public static R error() {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知異常,請聯(lián)系管理員");
}
public static R error(String msg) {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
}
public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R ok(String msg) {
R r = new R();
r.put("msg", msg);
return r;
}
public static R ok(Map<String, Object> map) {
R r = new R();
r.putAll(map);
return r;
}
public static R ok() {
return new R();
}
public R put(String key, Object value) {
super.put(key, value);
return this;
}
public Integer getCode() {
return (Integer) this.get("code");
}
}
(2)gulimall-common 新增文件 com.atguigu.common.to.SkuHasStockVo
package com.atguigu.common.to;
import lombok.Data;
@Data
public class SkuHasStockVo {
private Long skuId;
private Boolean hasStock;
}
(3)gulimall-common 新增文件 com.atguigu.common.to.es.SkuEsModel
商品上架需要在 es 中保存spu信息并更新spu的狀態(tài)信息,由于SpuinfoEntity與索引的數(shù)據(jù)模型并不對應(yīng),所以我們要建立專門的vo進行數(shù)據(jù)傳輸
package com.atguigu.common.to.es;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@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;
}
}
(4)gulimall-common 更新文件 com.atguigu.common.constant.ProductConstant
package com.atguigu.common.constant;
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 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;
}
}
}
(5)gulimall-product 的 com.atguigu.gulimall.product.controller.SpuInfoController 新增上架方法 upSpuForSearch
/**
* 商品上架
* @param spuId
* @return
*/
///product/spuinfo/{spuId}/up
@PostMapping("{spuId}/up")
public R upSpuForSearch(@PathVariable("spuId") Long spuId){
spuInfoService.upSpuForSearch(spuId);
return R.ok();
}
(6)gulimall-product 的 com.atguigu.gulimall.product.service.impl.SpuInfoServiceImpl 新增接口和實現(xiàn)類方法 upSpuForSearch
/**
* 商品上架
* @param spuId
*/
@Override
public void upSpuForSearch(Long spuId) {
//1、查出當前spuId對應(yīng)的所有sku信息,品牌的名字
List<SkuInfoEntity> skuInfoEntities = skuInfoService.getSkusBySpuId(spuId);
//TODO 4、查出當前sku的所有可以被用來檢索的規(guī)格屬性
List<ProductAttrValueEntity> baseAttrs = attrValueService.baseAttrlistforspu(spuId);
List<Long> attrIds = baseAttrs.stream().map(attr -> {
return attr.getAttrId();
}).collect(Collectors.toList());
List<Long> searchAttrIds = attrService.selectSearchAttrsIds(attrIds);
//轉(zhuǎn)換為Set集合
Set<Long> idSet = searchAttrIds.stream().collect(Collectors.toSet());
List<SkuEsModel.Attrs> 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());
List<Long> skuIdList = skuInfoEntities.stream()
.map(SkuInfoEntity::getSkuId)
.collect(Collectors.toList());
//TODO 1、發(fā)送遠程調(diào)用,庫存系統(tǒng)查詢是否有庫存
Map<Long, Boolean> stockMap = null;
try {
R skuHasStock = wareFeignService.getSkuHasStocks(skuIdList);
//
TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>() {};
stockMap = skuHasStock.getData(typeReference).stream()
.collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
} catch (Exception e) {
log.error("庫存服務(wù)查詢異常:原因{}",e);
}
//2、封裝每個sku的信息
Map<Long, Boolean> finalStockMap = stockMap;
List<SkuEsModel> collect = skuInfoEntities.stream().map(sku -> {
//組裝需要的數(shù)據(jù)
SkuEsModel esModel = new SkuEsModel();
esModel.setSkuPrice(sku.getPrice());
esModel.setSkuImg(sku.getSkuDefaultImg());
//設(shè)置庫存信息
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());
//設(shè)置檢索屬性
esModel.setAttrs(attrsList);
BeanUtils.copyProperties(sku,esModel);
return esModel;
}).collect(Collectors.toList());
//TODO 5、將數(shù)據(jù)發(fā)給es進行保存:gulimall-search
R r = searchFeignService.saveProductAsIndices(collect);
if (r.getCode() == 0) {
//遠程調(diào)用成功
//TODO 6、修改當前spu的狀態(tài)
this.baseMapper.updateSpuStatus(spuId, ProductConstant.ProductStatusEnum.SPU_UP.getCode());
} else {
//遠程調(diào)用失敗
//TODO 7、重復調(diào)用?接口冪等性:重試機制
}
}
(7)gulimall-product 的 com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl 新增接口和實現(xiàn)類方法 getSkusBySpuId
/**
* 查出當前spuid對應(yīng)的所有sku信息
* @param spuId
* @return
*/
@Override
public List<SkuInfoEntity> getSkusBySpuId(Long spuId) {
List<SkuInfoEntity> skus = this.list(new QueryWrapper<SkuInfoEntity>().eq("spu_id", spuId));
return skus;
}
(8)gulimall-product 的 com.atguigu.gulimall.product.service.impl.AttrServiceImpl 新增接口和實現(xiàn)類方法 selectSearchAttrsIds
/**
* 在指定的所有屬性集合里面,挑出檢索屬性
* @param attrIds
* @return
*/
@Override
public List<Long> selectSearchAttrsIds(List<Long> attrIds) {
/**
* SELECT attr_id FROM pms_attr WHERE attr_id in (?) AND search_type=1
*/
return baseMapper.selectSearchAttrsIds(attrIds);
}
(9)gulimall-product 的 com.atguigu.gulimall.product.dao.AttrDao 新增接口 selectSearchAttrsIds
/**
* 在指定的所有屬性集合里面,挑出檢索屬性
* @param attrIds
* @return
*/
List<Long> selectSearchAttrsIds(@Param("attrIds") List<Long> attrIds);
(10)gulimall-product 的 AttrDao.xml 新增方法 selectSearchAttrsIds
<!--在指定的所有屬性集合里面,挑出檢索屬性-->
<select id="selectSearchAttrsIds" resultType="java.lang.Long">
SELECT attr_id FROM pms_attr WHERE attr_id in
<foreach collection="attrIds" item="id" separator="," open="(" close=")">
#{id}
</foreach>
AND search_type=1
</select>
(11)gulimall-product 創(chuàng)建遠程調(diào)用文件 com.atguigu.gulimall.product.feign.WareFeignService
package com.atguigu.gulimall.product.feign;
import com.atguigu.common.to.SkuHasStockVo;
import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
@Component
@FeignClient("gulimall-ware")
public interface WareFeignService {
/**
* 1、R設(shè)計的時候可以加上泛型
* 2、直接返回我們想要的結(jié)果
* 3、自己封裝解析結(jié)果
* @return
*/
@PostMapping("/ware/waresku/hasStock")
R getSkuHasStocks(@RequestBody List<Long> skuIds);
}
(12)gulimall-ware 編寫遠程服務(wù)查詢庫存接口
com.atguigu.gulimall.ware.controller.WareSkuController 新增方法 getSkuHasStocks
/**
* 查詢sku是否有庫存
* 返回skuId 和 stock庫存量
*/
@PostMapping("/hasStock")
public R getSkuHasStocks(@RequestBody List<Long> SkuIds){
List<SkuHasStockVo> vos = wareSkuService.getSkuHasStock(SkuIds);
return R.ok().setData(vos);
}
com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl 新增接口和實現(xiàn)方法
/**
* 查詢sku是否有庫存
* @param skuIds
* @return
*/
@Override
public List<SkuHasStockVo> getSkuHasStock(List<Long> skuIds) {
List<SkuHasStockVo> collect = skuIds.stream().map(skuId -> {
SkuHasStockVo vo = new SkuHasStockVo();
//查看當前sku的總庫存量
//SELECT SUM(stock-stock_locked) FROM wms_ware_sku WHERE sku_id=1
Long count = baseMapper.getSkuStock(skuId);
vo.setHasStock(count==null?false:count>0);
vo.setSkuId(skuId);
return vo;
}).collect(Collectors.toList());
System.out.println(collect);
return collect;
}
com.atguigu.gulimall.ware.dao.WareSkuDao 新增方法 getSkuStock
/**
* 查看當前sku的總庫存量
* @param skuId
* @return
*/
Long getSkuStock(@Param("skuId") Long skuId);
WareSkuDao.xml 新增xml實現(xiàn)方法
<!--查看當前sku的總庫存量-->
<select id="getSkuStock" resultType="java.lang.Long">
SELECT SUM(stock-stock_locked) FROM wms_ware_sku WHERE sku_id=#{skuId}
</select>
(13)gulimall-product 創(chuàng)建遠程調(diào)用文件 com.atguigu.gulimall.product.feign.SearchFeignService
package com.atguigu.gulimall.product.feign;
import com.atguigu.common.to.es.SkuEsModel;
import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
@Component
@FeignClient("gulimall-search")
public interface SearchFeignService {
@PostMapping("/search/save/product")
R saveProductAsIndices(@RequestBody List<SkuEsModel> skuEsModels);
}
(14)gulimall-search 編寫遠程服務(wù)es上架商品接口
新增文件 com.atguigu.gulimall.search.controller.ElasticSaveController
package com.atguigu.gulimall.search.controller;
import com.atguigu.common.exception.BizCodeEnum;
import com.atguigu.common.to.es.SkuEsModel;
import com.atguigu.common.utils.R;
import com.atguigu.gulimall.search.service.ProductSaveService;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.jni.BIOCallback;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Slf4j
@RequestMapping("/search/save")
@RestController
public class ElasticSaveController {
@Autowired
ProductSaveService productSaveService;
//上架商品
@PostMapping("/product")
public R saveProductAsIndices(@RequestBody List<SkuEsModel> skuEsModels){
boolean b = false;
try {
System.out.println(skuEsModels);
b = productSaveService.productStatusUp(skuEsModels);
System.out.println(b);
}catch (Exception e){
e.printStackTrace();
log.error("ElasticSaveController商品上架錯誤:{}",e);
return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UP_EXCEPTION.getMessage());
}
if(!b){
return R.ok();
}else{
return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UP_EXCEPTION.getMessage());
}
}
}
新增接口和實現(xiàn)類文件 com.atguigu.gulimall.search.service.impl.ProductSaveServiceImpl
package com.atguigu.gulimall.search.service.impl;
import com.alibaba.fastjson.JSON;
import com.atguigu.common.to.es.SkuEsModel;
import com.atguigu.gulimall.search.config.GulimallElasticSearchConfig;
import com.atguigu.gulimall.search.constant.EsConstant;
import com.atguigu.gulimall.search.service.ProductSaveService;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.bulk.BulkItemResponse;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
public class ProductSaveServiceImpl implements ProductSaveService {
@Resource
private RestHighLevelClient restHighLevelClient;
/**
* 上架商品
* @param skuEsModels
*/
@Override
public boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {
// 1.給ES建立一個索引 product
BulkRequest bulkRequest = new BulkRequest();
// 2.構(gòu)造保存請求
for (SkuEsModel esModel : skuEsModels) {
// 設(shè)置索引
IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
// 設(shè)置索引id
indexRequest.id(esModel.getSkuId().toString());
String jsonString = JSON.toJSONString(esModel);
indexRequest.source(jsonString, XContentType.JSON);
// add
bulkRequest.add(indexRequest);
}
// bulk批量保存
BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
// TODO 是否擁有錯誤
boolean hasFailures = bulk.hasFailures();
List<String> collect = Arrays.stream(bulk.getItems()).map(item -> item.getId()).collect(Collectors.toList());
log.info("商品上架完成:{},返回數(shù)據(jù)",collect,bulk.toString());
return hasFailures;
}
}
總結(jié)
以上是生活随笔為你收集整理的谷粒商城分布式高级(三)—— 商城业务(商品上架)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。