--仅列出典型的业务逻辑
三级分类
组装属性结构
目标:查出所有的三级分类,并且要组装成父子结构
表的结构如下,其中parent_id表示父分类的id,如果此值为0,意思是当前行是最大的分类(也就是1级分类);同样也可通过cat_level来查找,此值表示当前分类的等级
具体实现方法如下:
对于方法listWithTree:
baseMapper对象就是当前ServiceImpl的Service接口
selectList的时候传入null值就是查询所有的值
stream流式编码的过程如下:
对于方法 findChildren:
其传入的参数为父节点(在递归的第一层是一级分类)和所有的数据
过程如下:
//controller
@RequestMapping("/list/tree")
public R list(){
List<CategoryEntity> entities=categoryService.listWithTree();
return R.ok().put("data",entities);
}
//serviceimpl
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
@Override
public List<CategoryEntity> listWithTree() {
//获取全部的list
//不使用autoWired引入接口,因为这里继承了ServiceImpl,baseMapper属性就是service
List<CategoryEntity> entities = baseMapper.selectList(null);
//先查出所有的一级属性,这里因为只有一行,filter里的大括号小括号和return都可以省略的
List<CategoryEntity> level1 = entities.stream().filter((menu) -> {
return menu.getParentCid() == 0;//为0表示是一级分类
}).peek(menu -> {
menu.setChildren(findChildren(menu, entities));//给每个孩子字段设置值
//升序
}).sorted((menu1, menu2) -> {
return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
}).collect(Collectors.toList());//集合处理
return level1;
}
//递归查找所有的子菜单
private List<CategoryEntity> findChildren(CategoryEntity root, List<CategoryEntity> all) {
List<CategoryEntity> children = all.stream().filter((menu) -> {
return menu.getParentCid().equals(root.getCatId());
}).peek((menu) -> {
menu.setChildren(findChildren(menu, all));//递归查找所有
}).sorted((menu1, menu2) -> {
return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
}).collect(Collectors.toList());
return children;
}
}
增删改查三级分类
这里都可以使用人人开源直接生成的代码,主要是前端的工作
@PathVariable注解用于接受uri中的参数
@RequestBody注解用于接受前端传入的数据(请求体里的内容Json)
/**
* 信息
*/
@RequestMapping("/info/{catId}")
public R info(@PathVariable("catId") Long catId){
CategoryEntity category = categoryService.getById(catId);
return R.ok().put("data", category);
}
/**
* 保存
*/
@RequestMapping("/save")
public R save(@RequestBody CategoryEntity category){
categoryService.save(category);
return R.ok();
}
/**
* 修改
*/
@RequestMapping("/update")
public R update(@RequestBody CategoryEntity category){
categoryService.updateCascade(category);
return R.ok();
}
@RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds){
//检查当前删除的菜单是否被别的地方引用了
categoryService.removeMenuByIds(Arrays.asList(catIds));
return R.ok();
}
获取分类属性分组
前端的效果是拿到点击数据的三级分类或者查找关键字,可以显示该分类下的分组信息:比如手机下的分组为信息,基本信息,芯片,如果关键字为信息就只能搜索到前两条信息;
请求参数如下:
需要返回的参数如下:
重写AttrGroupServiceImpl实现类的queryPage方法,该方法的逻辑如下:
//分页查询,需要匹配key的模糊查询
@Override
public PageUtils queryPage(Map<String, Object> params, Long catlogId) {
//如果没有三级分类的id传过来,那么catlogId等于0,那就传所有的(前端规定)
if(catlogId==0){
IPage<AttrGroupEntity> page =
this.page(new Query<AttrGroupEntity>().getPage(params), new QueryWrapper<AttrGroupEntity>());//没有任何条件的情况下就是查所有
return new PageUtils(page);//利用封装好的工具类进行解析
}else{
//需要的sql语句
//select * from pms_attr_group where catelog_id=? and (attr_group_name=key or attr_group_id like %key%)
//key就是检索框输入的东西
String key= (String) params.get("key");
QueryWrapper<AttrGroupEntity> wrapper = new QueryWrapper<AttrGroupEntity>().eq("catelog_id",catlogId);//三级分类id相等
if(!StringUtils.isEmpty(key)){
wrapper.and((obj)->{
obj.eq("attr_group_id",key).or().like("attr_group_name",key);
});
}
IPage<AttrGroupEntity> page =
this.page(new Query<AttrGroupEntity>().getPage(params), wrapper);
return new PageUtils(page);
}
}
在解决此问题的途中出现了一个问题,因为我们在编写三级分类的时候运用了递归查找,最后一级分类虽然没有子分类了,但是还是带了一个空集合,这导致前端没有办法输入我们可以使用@JsonInclude注解对此属性进行限定
完整查找路径
需要将分类的完整cid路径传给前端,例如手机:[2,25,225]
@Override
public Long[] findCatelogPath(Long catlogId) {
List<Long> path=new ArrayList<>();
findParentPath(catlogId,path);
//集合逆序,添加的时候是反着添加的
Collections.reverse(path);
return path.toArray(new Long[path.size()]);//转化成数组的时候要指定数组
}
//找到父亲id的方法
private List<Long> findParentPath(Long catlogId,List<Long> path){
//收集当前id
path.add(catlogId);
CategoryEntity entity = this.getById(catlogId);
//获得父节点id--递归
Long parentCid = entity.getParentCid();
if(parentCid!=0){
//如果父节点id不为0就一直找
findParentPath(parentCid,path);
}
return path;
}
新增关联分类,级联更新
获取关联分类
目标是把品牌id关联到对应的分类表中,我们使用category_brand_relation表记录了这样的对应信息,此表只有两个属性brand_id和category_id
/**
* 列表
*/
@GetMapping("/catelog/list")
public R catelogList(@RequestParam Map<String, Object> params){
String brandId= (String) params.get("brandId");
List<CategoryBrandRelationEntity> data = categoryBrandRelationService.
list(new QueryWrapper<CategoryBrandRelationEntity>().eq("brand_id", brandId));
return R.ok().put("data", data);
}
新增关联分类
在大规模的电商系统来说,关联很耗内存,如果我们能增加两个冗余字段就能避免联表查询,在category_brand_relation表属性的基础上,添加brandName和categoryName两个表属性;然后重写其save方法
@Override
public void saveDetail(CategoryBrandRelationEntity categoryBrandRelation) {
Long brandId = categoryBrandRelation.getBrandId();
Long catelogId = categoryBrandRelation.getCatelogId();
//分别查询品牌名和分类名
BrandEntity brandEntity = brandDao.selectById(brandId);
String brandEntityName = brandEntity.getName();
String cateEntityName=categoryDao.selectById(catelogId).getName();
categoryBrandRelation.setBrandName(brandEntityName);
categoryBrandRelation.setCatelogName(cateEntityName);//保存
this.save(categoryBrandRelation);//调用自己的save方法保存数据
}
虽然这节省了连表查询的内存,但是这也带来了新的问题,当你对与冗余字段相关的表进行修改的时候,也需要对冗余字段进行修改,比如我将品牌名修改为了华为,那么关联表中的分类对应品牌名也应该修改为华为;这就叫做级联更新
下面是两种方法更新字段,第一种是使用queryWrapper第二种是使用sql
更新品牌相关字段
@Autowired
private CategoryBrandRelationService categoryBrandRelationService;
@Override
public void updateDetail(BrandEntity brand) {
//保证冗余字段的数据一致
this.updateById(brand);
if(!StringUtils.isEmpty(brand.getName())){
//同步更新其他关联表的冗余字段
categoryBrandRelationService.updateBrand(brand.getBrandId(),brand.getName());
//TODO 更新其他的关联信息
}
}
@Override
public void updateBrand(Long brandId, String name) {
CategoryBrandRelationEntity entity = new CategoryBrandRelationEntity();
entity.setBrandId(brandId);//id
entity.setBrandName(name);//只有名字是冗余字段,只更新名字就行
this.update(entity,new UpdateWrapper<CategoryBrandRelationEntity>().eq("brand_id",brandId));//updateWrapper
}
更新分类相关字段:(并且要注意的是这样的级联更新一定是原子性的所以在方法上加@Transactional,并且需要在主类上开启事务功能加上@EnableTransactionalManagement)
/**
* 修改
*/
@RequestMapping("/update")
public R update(@RequestBody CategoryEntity category){
categoryService.updateCascade(category);
return R.ok();
}
@Override
@Transactional//事务
public void updateCascade(CategoryEntity category) {
this.updateById(category);
categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
}
@Override
public void updateCategory(Long catId, String name) {
this.baseMapper.updateCategory(catId,name);
}
@Mapper
public interface CategoryBrandRelationDao extends BaseMapper<CategoryBrandRelationEntity> {
void updateCategory(@Param("catId") Long catId,@Param("name") String name);
}
<update id="updateCategory">
update `pms_category_brand_relation` set catelog_name=#{name} where catelog_id=#{catId}
</update>
获取规格参数列表
给一个catelog_id要求查询出如下的规格参数,其中param还可能带有模糊查询条件,除了AttrEntity有的内容外还需要额外查询两个其他的字段catelogName和groupName
attr_attrgroup关系表
该方法逻辑如下
@Autowired
AttrAttrgroupRelationService relationService;//最好是注入service
@Autowired
AttrGroupService attrGroupService;
@Autowired
CategoryService categoryService;
@Override
public PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId) {
QueryWrapper<AttrEntity> wrapper = new QueryWrapper<>();
if(catelogId!=0){//不为0=存在,就添加eq条件
wrapper.eq("catelog_Id",catelogId);
}
//由于上面有可能添加了eq所以我们下面要使用and
String key= (String) params.get("key");
if(!StringUtils.isEmpty(key)){
wrapper.and((qwrapper)->{
qwrapper.eq("attr_id",key).or().like("attr_name",key);
});
}
IPage<AttrEntity> page = this.page(new Query<AttrEntity>().getPage(params), wrapper);//得到分页
//增加的操作
PageUtils pageUtils = new PageUtils(page);
List<AttrEntity> attrEntities=page.getRecords();
List<AttrRespVo> collect = attrEntities.stream().map((attrEntity) -> {
AttrRespVo attrRespVo = new AttrRespVo();
BeanUtils.copyProperties(attrEntity, attrRespVo);//拷贝属性
//获得当前关联的对象
AttrAttrgroupRelationEntity attrId = relationService.getOne(new QueryWrapper<AttrAttrgroupRelationEntity>()
.eq("attr_id", attrEntity.getAttrId()));//通过属性id相等查到关联对象
if (attrId != null) {//分组关联信息有可能是空的
//分组
AttrGroupEntity attrGroupEntity = attrGroupService.getById(attrId.getAttrGroupId());
attrRespVo.setCatelogName(attrGroupEntity.getAttrGroupName());
}
//分类
CategoryEntity categoryEntity = categoryService.getById(attrEntity.getCatelogId());
if (categoryEntity != null) {
attrRespVo.setGroupName(categoryEntity.getName());
}
return attrRespVo;
}).collect(Collectors.toList());
pageUtils.setList(collect);
return pageUtils;
}
编写批量删除功能
@Autowired
AttrAttrgroupRelationDao relationDao;
@Override
public void deleteRelation(AttrGroupRelationVo[] vos) {
//我们希望只发一次删除请求完成批量删除而不是用relationService.remove(new QueryWrapper<>().eq())
List<AttrAttrgroupRelationEntity> collect = Arrays.asList(vos).stream().map((item) -> {
AttrAttrgroupRelationEntity entity = new AttrAttrgroupRelationEntity();
BeanUtils.copyProperties(item, entity);
return entity;
}).collect(Collectors.toList());
relationDao.deleteBatchRelation(collect);
}
}
@Mapper
public interface AttrAttrgroupRelationDao extends BaseMapper<AttrAttrgroupRelationEntity> {
void deleteBatchRelation(@Param("collect") List<AttrAttrgroupRelationEntity> collect);
}
要注意这里sql语句的写法, 这里的or前后必须加上空格
<delete id="deleteBatchRelation">
delete from pms_attr_attrgroup_relation where
<foreach collection="collect" item="item" separator=" OR ">
(attr_id=#{item.attrId} and attr_group_id=#{item.attrGroupId})
</foreach>
</delete>
获取属性分组里没有关联的其他属性
@RequestParam注解用于取出请求头里的信息,一般后面紧跟一个Map<String,Object>
@RequestBody注解用于取出请求体里的信息,因为get请求没有请求体,所以只用于post请求
/**
* 新增关联列表显示
*/
@GetMapping("/{attrgroupId}/attr/relation")
public R attrNoRelation(@RequestParam Map<String, Object> params,@PathVariable("attrgroupId") Long attrGroupId){
PageUtils page= attrService.getNoRelationAttr(params,attrGroupId);
return R.ok().put("data",page);
}
代码逻辑如下:
@Override
public PageUtils getNoRelationAttr(Map<String, Object> params, Long attrGroupId) {
//1.当前分组只能关联自己所属分类里面的所有属性
AttrGroupEntity attrGroupEntity = attrGroupService.getById(attrGroupId);
Long catelogId = attrGroupEntity.getCatelogId();
//2.当前分组只能关联别的分组没有引用的属性
//2.1找到当前分类下的所有分组--》自己也要查,后续会连同自己的属性一起收集到collect里后进行剔除
List<AttrGroupEntity> attrGroupEntityList = attrGroupService.list(new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId));
List<Long> collect = attrGroupEntityList.stream().map((item) -> {
return item.getAttrGroupId();//我们只需要属性组id就行了
}).collect(Collectors.toList());
//2.2这些分组关联的属性-->获取所有attr_group_id字段与collect集合里一致的对象
List<AttrAttrgroupRelationEntity> relationEntities = relationService.list(new QueryWrapper<AttrAttrgroupRelationEntity>().in("attr_group_id", collect));
List<Long> collect1 = relationEntities.stream().map((item) -> {
return item.getAttrId();//只需要属性id就可以了
}).collect(Collectors.toList());
//2.3从当前分类的所有属性中筛选掉这些属性,并且不查销售属性(销售属性没有关联关系)
QueryWrapper<AttrEntity> attrEntityQueryWrapper = new QueryWrapper<AttrEntity>().eq("catelog_id", catelogId).eq("attr_type",ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode());
if(collect1!=null&&collect1.size()>0){
attrEntityQueryWrapper.notIn("attr_id", collect1);//不为空并且大于0才进行判断
}
//模糊查询
String key=(String) params.get("key");
if(!StringUtils.isEmpty(key)){
attrEntityQueryWrapper.and((w)->{
w.eq("attr_id",key).or().like("attr_name",key);
});
}
IPage<AttrEntity> page = this.page(new Query<AttrEntity>().getPage(params), attrEntityQueryWrapper);
PageUtils pageUtils = new PageUtils(page);
return pageUtils;
}
spu检索
前端申请的参数:
方法逻辑:
@Override
public PageUtils queryPageByCondition(Map<String, Object> params) {
QueryWrapper<SpuInfoEntity> wrapper = new QueryWrapper<>();
String key = (String) params.get("key");
//因为这里有or所以必须添加一个and让他变成status=1 and (id=1 or spu_name like...)
if(!StringUtils.isEmpty(key)){
wrapper.and((w)->{
w.eq("id",key).or().like("spu_name",key);
});
}
String status = (String) params.get("status");
if(!StringUtils.isEmpty(status)){
wrapper.eq("publish_status",status);
}
String brandId = (String) params.get("brandId");
if(!StringUtils.isEmpty(brandId)){
wrapper.eq("brand_id",brandId);
;
}
//注意这里数据库的字段
String catelogId = (String) params.get("catalogId");
if(!StringUtils.isEmpty(catelogId)){
wrapper.eq("catalog_id",catelogId);
}
IPage<SpuInfoEntity> page = this.page(
new Query<SpuInfoEntity>().getPage(params),wrapper
);
return new PageUtils(page);
}
商品上架
上架的商品需要进行es检索,该功能需要的to属性如下:
@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<Attr> attrs;
@Data
public static class Attr {
private Long attrId;
private String attrName;
private String attrValue;
}
}
远程库存查询
@Override
public List<SkuHasStockVo> getSkuHasStock(List<Long> skuIds) {
//获取sku总库存数stock-stock_locked
List<WareSkuEntity> wareSkuEntityList = this.list(new QueryWrapper<WareSkuEntity>().in("sku_id", skuIds));
List<SkuHasStockVo> collect = wareSkuEntityList.stream().filter(w->{
return !(w==null);//排除为空的值
}).map(w -> {
SkuHasStockVo skuHasStockVo = new SkuHasStockVo();
skuHasStockVo.setSkuId(w.getSkuId());
if(w.getStock()==null||w.getStockLocked()==null){
skuHasStockVo.setHasStock(true);
}else{
skuHasStockVo.setHasStock((w.getStock() - w.getStockLocked()) > 0);
}
return skuHasStockVo;
}).collect(Collectors.toList());
return collect;
}
ES模块创建相关的索引
为了规范性,我们直接枚举索引
public class EsConstant {
public static final String PRODUCT_INDEX="product";//sku数据在product中的索引
}
创建es时做的配置
@Configuration
public class ESConfig {
public static final RequestOptions COMMON_OPTIONS;
//默认规则
static {
RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
COMMON_OPTIONS = builder.build();
}
@Bean
public RestHighLevelClient esRestClient() {
RestClientBuilder builder = null;
// 可以指定多个es
builder = RestClient.builder(new HttpHost("192.168.116.128", 9200, "http"));
RestHighLevelClient client = new RestHighLevelClient(builder);
return client;
}
}
对于如下创建索引的方法,如果我们创建成功返回true否则返回false
@Slf4j
@Service
public class ProductSaveServiceImpl implements ProductSaveService {
@Autowired
RestHighLevelClient restHighLevelClient;
@Override
public boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {
//保存到es
//1.建立一个索引,product映射关系
//2.给es中保存这些数据
BulkRequest bulkRequest=new BulkRequest();//批量保存数据
for (SkuEsModel skuEsModel : skuEsModels) {
//1.构造保存请求
IndexRequest indexRequest=new IndexRequest(EsConstant.PRODUCT_INDEX);
indexRequest.id(skuEsModel.getSkuId().toString());
String s = JSON.toJSONString(skuEsModel);
indexRequest.source(s, XContentType.JSON);
bulkRequest.add(indexRequest);
}
BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, ESConfig.COMMON_OPTIONS);
List<String> collect = Arrays.stream(bulk.getItems()).map(i -> {
return i.getId();
}).collect(Collectors.toList());
log.info("商品上架成功",collect);
boolean b = bulk.hasFailures();
return !b;
}
}
上架
@Transactional
@Override
public void up(Long spuId) {
//对于skuInfoEntity和ESModel对象
//属性有但是字段不同:skuPrice skuImg
//没有属性需要单独处理:hasStock(需要远程调用库存系统,返回一个bool值),hotScore(热度评分,默认放一个0)
//需要调用本地方法查询:brandName,brandImg,catalogName,List<attrs>(attrId,attrName,attrValue)
List<SkuInfoEntity> skuInfoEntityList = skuInfoService.list(new QueryWrapper<SkuInfoEntity>().eq("spu_id", spuId));
//避免循环查表,先查询brandName和brandImg根据brandId
SpuInfoEntity byId = this.getById(spuId);
Long brandId = byId.getBrandId();
BrandEntity brandEntity = brandService.getById(brandId);
String brandEntityName = brandEntity.getName();
String logo = brandEntity.getLogo();
//查找catlogName
Long catalogId = byId.getCatalogId();
CategoryEntity categoryEntity = categoryService.getById(catalogId);
String categoryEntityName = categoryEntity.getName();
//查找List<attr>
List<ProductAttrValueEntity> list = productAttrValueService.baseAttrListForSpu(spuId);//获得spu相关的所有pav属性
List<Long> attrIds = list.stream().map(item -> {
return item.getAttrId();
}).collect(Collectors.toList());
//在指定的索引集合里挑出检索类型search_type为1的值
List<Long> longList=attrService.selectSearchAttrIds(attrIds);
Set<Long> idSet=new HashSet<>(longList);//存储进一个临时集合
List<SkuEsModel.Attr> attrList = list.stream().filter(item -> {
return idSet.contains(item.getAttrId());//如果在我们收集到的集合里有就返回true
}).map(item -> {
SkuEsModel.Attr attr = new SkuEsModel.Attr();
BeanUtils.copyProperties(item, attr);
return attr;
}).collect(Collectors.toList());
//编写库存查询--远程查询
List<Long> skuIds = skuInfoEntityList.stream().map(w -> {
return w.getSkuId();
}).collect(Collectors.toList());
Map<Long, Boolean> map=null;
try{
R r=wareFeignService.getSkuHasStock(skuIds);
TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>() {};//构造器受保护我们拿不到,只能携程一个匿名类对象
//根据skuid和bool值组合成了一个map
List<SkuHasStockVo> data = r.getData(typeReference);
data.stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
}catch (Exception e){
log.error("库存服务查询异常:原因{}",e);
}
Map<Long, Boolean> finalMap = map;//map要使用只能经过一次赋值
List<SkuEsModel> collect = skuInfoEntityList.stream().map(item -> {
SkuEsModel skuEsModel = new SkuEsModel();
BeanUtils.copyProperties(item,skuEsModel);//复制属性
skuEsModel.setSkuImg(item.getSkuDefaultImg());
skuEsModel.setSkuPrice(item.getPrice());
skuEsModel.setBrandName(brandEntityName);
skuEsModel.setBrandImg(logo);
skuEsModel.setCatalogName(categoryEntityName);
skuEsModel.setAttrs(attrList);
skuEsModel.setHotScore(0L);
if (finalMap == null) {
skuEsModel.setHasStock(true);
} else {
skuEsModel.setHasStock(finalMap.get(item.getSkuId()));
}
return skuEsModel;
}).collect(Collectors.toList());
//数据发给es进行保存
R r = searchFeignService.productStatusUp(collect);
if(r.getCode()==0){
//修改当前spu的状态
this.baseMapper.updateSpuStatus(spuId, ProductConstant.StatusEnum.STU_UP.getCode());
}else{
//远程调用失败
//TODo 重复调用,接口幂等性,重试机制
}
}
使用redis缓存
在分布式情况下无法使用synchronized本地锁解决方案,先采用redis的setIfAbsent来实现(这种情况无法解决redis集群环境)
//redis锁
public Map<String, List<Catalog2Vo>> getCatelogJsonFromDBWithRedis() {
String uuid = UUID.randomUUID().toString();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
Boolean lock = ops.setIfAbsent("lock", uuid,500, TimeUnit.SECONDS);
if (lock) {
Map<String, List<Catalog2Vo>> categoriesDb = getDataFromDB();
String lockValue = ops.get("lock");
// get和delete原子操作
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
stringRedisTemplate.execute(
new DefaultRedisScript<Long>(script, Long.class), // 脚本和返回类型
Arrays.asList("lock"), // 参数
lockValue); // 参数值,锁的值
return categoriesDb;
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 睡眠0.1s后,重新调用 //自旋
return getCatelogJsonFromDBWithRedis();
}
}
使用Redisson锁来解决此问题
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDBWithRedission() {
RLock lock = redissonClient.getLock("catalogJson-lock");//锁的粒度:防止锁重名
lock.lock();
Map<String, List<Catalog2Vo>> dataFromDB;
try{
dataFromDB=getDataFromDB();
}finally {
lock.unlock();
}
return dataFromDB;
}
由于我们查询数据库的方法过于复杂,希望除了第一个人是在数据库里查询结果,其他人都是在redis中拿到缓存的结果
private Map<String, List<Catalog2Vo>> getDataFromDB() {
String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson");
//得到锁以后,应该再去缓存中确定一次,如果没有才继续查询
if (!StringUtils.isEmpty(catalogJson)) {
Map<String, List<Catalog2Vo>> parseObject = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {
});
return parseObject;
}
List<CategoryEntity> entityList = baseMapper.selectList(null);
// 查询所有一级分类
List<CategoryEntity> level1 = getByParentCid(entityList, 0L);
Map<String, List<Catalog2Vo>> parent_cid = level1.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
// 拿到每一个一级分类 然后查询他们的二级分类
List<CategoryEntity> entities = getByParentCid(entityList, v.getCatId());
List<Catalog2Vo> catelog2Vos = null;
if (entities != null) {
catelog2Vos = entities.stream().map(l2 -> {
Catalog2Vo catelog2Vo = new Catalog2Vo(v.getCatId().toString(), l2.getName(), l2.getCatId().toString(), null);
// 找当前二级分类的三级分类
List<CategoryEntity> level3 = getByParentCid(entityList, l2.getCatId());
// 三级分类有数据的情况下
if (level3 != null) {
List<Catalog3Vo> catalog3Vos = level3.stream().map(l3 -> new Catalog3Vo(l3.getCatId().toString(), l3.getName(), l2.getCatId().toString())).collect(Collectors.toList());
catelog2Vo.setCatalog3List(catalog3Vos);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
//查到的数据放入缓存
//缓存中存的所有对象都应该是json字符串,因为他是跨语言跨平台的兼容的--序列化过程
String s = JSON.toJSONString(parent_cid);
stringRedisTemplate.opsForValue().set("catalogJson", s, 1, TimeUnit.DAYS);
return parent_cid;
}
使用ES做检索服务
整个search分为三个阶段,构建检索请求,执行检索请求和分析响应数据
@Autowired
private RestHighLevelClient client;
public SearchResult search(SearchPara searchPara) {
//构建检索请求
SearchRequest request = buildSearchRequest(searchPara);
SearchResult result =null;
try {
//执行检索请求
SearchResponse search = client.search(request, ESConfig.COMMON_OPTIONS);
//分析响应数据封装成我们需要的格式
result = buildSearchResult(search,searchPara);
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
在ES中插入目标索引
PUT gulimall_product
{
"mappings":{
"properties": {
"skuId":{ "type": "long" },
"spuId":{ "type": "keyword" },
"skuTitle": {
//text属性类似于string,该字段会被全文搜索,生成倒排索引,字符串会被分词器拆分成一个一个的排序
"type": "text",
"analyzer": "ik_smart"
},
//keyword属性的字段只能通过精确值搜索到,无法拆分
"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"
},
"catalogName": {"type": "keyword" },
"attrs": {
//nested表示当前属性时一个嵌套的属性
"type": "nested",
"properties": {
"attrId": {"type": "long" },
"attrName": {
"type": "keyword"
},
"attrValue": {"type": "keyword" }
}
}
}
}
}
我们在es中需要构建的查询条件:
GET gulimall_product/_search
{
//构建查询条件
"query": {
"bool": {
"must": [ {"match": { "skuTitle": "华为" }} ], # 检索出华为
"filter": [ # 过滤,关闭评分,提高查询效率
{ "term": { "catalogId": "225" } },
{ "terms": {"brandId": [ "2"] } },
{ "term": { "hasStock": "false"} },
{
"range": {
"skuPrice": { # 价格1K~7K
"gte": 1000,
"lte": 7000
}
}
},
{
"nested": {
"path": "attrs", # 聚合名字
"query": {
"bool": {
"must": [
{
"term": { "attrs.attrId": { "value": "6"} }
}
]
}
}
}
}
]
}
},
"sort": [ {"skuPrice": {"order": "desc" } } ],
"from": 0,
"size": 5,
"highlight": {
"fields": {"skuTitle": {}}, # 高亮的字段
"pre_tags": "<b style='color:red'>", # 前缀
"post_tags": "</b>"
},
"aggs": { # 查完后聚合
"brandAgg": {
"terms": {
"field": "brandId",
"size": 10
},
"aggs": { # 子聚合
"brandNameAgg": { # 每个商品id的品牌
"terms": {
"field": "brandName",
"size": 10
}
},
"brandImgAgg": {
"terms": {
"field": "brandImg",
"size": 10
}
}
}
},
"catalogAgg":{
"terms": {
"field": "catalogId",
"size": 10
},
"aggs": {
"catalogNameAgg": {
"terms": {
"field": "catalogName",
"size": 10
}
}
}
},
"attrs":{
"nested": {"path": "attrs" },
"aggs": {
"attrIdAgg": {
"terms": {
"field": "attrs.attrId",
"size": 10
},
"aggs": {
"attrNameAgg": {
"terms": {
"field": "attrs.attrName",
"size": 10
}
}
}
}
}
}
}
}
构建请求:
query阶段
排序,分页,高亮阶段
聚合阶段:
最后是用SearchRequest对sourceBuilder对象进行整体请求查询
private SearchRequest buildSearchRequest(SearchPara searchPara) {
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
//1.构建bool的query---》检索
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//1.1)must模糊匹配 对应语句 "must": [ {"match": { "skuTitle": "华为" }} ]
if (!StringUtils.isEmpty(searchPara.getKeyword())) {
boolQuery.must(QueryBuilders.matchQuery("skuTitle", searchPara.getKeyword()));
}
//1.2)filter 与must的区别是filter的项目不参与评分
//"filter": [ # 过滤{ "term": { "catalogId": "225" } },
if (searchPara.getCatalog3Id() != null) {
boolQuery.filter(QueryBuilders.termQuery("catalogId", searchPara.getCatalog3Id()));
}
//1.3) { "terms": {"brandId": [ "2"] } }, { "term": { "hasStock": "false"} }
if (searchPara.getBrandId() != null && searchPara.getBrandId().size() > 0) {
boolQuery.filter(QueryBuilders.termQuery("brandId", searchPara.getBrandId()));
}
boolQuery.filter(QueryBuilders.termQuery("hasStock", searchPara.getHasStock() == 1));
//1.4) "range": {"skuPrice": { # 价格1K~7K"gte": 1000, "lte": 7000}} 1_500表示大于1小于500
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("skuPrice");
if (!StringUtils.isEmpty(searchPara.getSkuPrice())) {
String[] split = searchPara.getSkuPrice().split("_");
if (split.length == 2) {//表示是一个区间
rangeQuery.gte(split[0]).lte(split[1]);
} else {
if (searchPara.getSkuPrice().startsWith("_")) {
rangeQuery.lte(split[0]);
} else {
rangeQuery.gte(split[0]);
}
}
boolQuery.filter(rangeQuery);
}
//传入的值 attrs=1_5寸:8存 attrs=2_16G:8G 前面的1_ 2_表示几号属性
//1.5) {"nested": {"path": "attrs", # 聚合名字"query": {"bool": {"must": [{"term": { "attrs.attrId": { "value": "6"
if (searchPara.getAttrs() != null && searchPara.getAttrs().size() > 0) {
for (String attr : searchPara.getAttrs()) {
BoolQueryBuilder boolQuery1 = QueryBuilders.boolQuery();
String[] s = attr.split("_");
String attrId = s[0];//检索的属性id
String[] attrValue = s[1].split(":");//检索的值
boolQuery1.must(QueryBuilders.termQuery("attrs.attrId", attrId));
boolQuery1.must(QueryBuilders.termsQuery("attrs.attrValue", attrValue));
NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", boolQuery1, ScoreMode.None);//none表示不参与评分
boolQuery.filter(nestedQuery);//每个属性都要生成一个嵌入式的查询条件
}
}
sourceBuilder.query(boolQuery);
//2.sort--》排序
//sort=hostScore_asc/desc
String sort = searchPara.getSort();
if (!StringUtils.isEmpty(sort)) {
String[] s = sort.split("_");
SortOrder order = s[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC;
sourceBuilder.sort(s[0], order);
}
//3.分页
//pageNum:1 from 0 size 5 [0,1,2,3,4]
//pageNum:2 from 5 size 5 [5,6,7,8,9]
//from=(pageNum-1)*size
if (searchPara.getPageNum() != null) {
sourceBuilder.from((searchPara.getPageNum() - 1) * EsConstant.PRODUCT_PAGESIZE);
sourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);
}
//4.高亮
/*
"highlight": {
"fields": {"skuTitle": {}}, # 高亮的字段
"pre_tags": "<b style='color:red'>", # 前缀
"post_tags": "</b>"
},
*/
if (!StringUtils.isEmpty(searchPara.getKeyword())) {
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("skuTitle");
highlightBuilder.preTags("<b style='color:red>");
highlightBuilder.postTags("</b>");
sourceBuilder.highlighter(highlightBuilder);
}
//5.聚合分析
//5.1)聚合品牌
/*
"aggs": { "brandAgg": {"terms": {"field": "brandId","size": 10
*/
TermsAggregationBuilder brandAgg = AggregationBuilders.terms("brand_agg");
brandAgg.field("brandId").size(10);
//5.2) 品牌子聚合
/*
"aggs": {
"brandNameAgg": {
"terms": {
"field": "brandName",
"size": 10
"brandImgAgg": {
"terms": {
"field": "brandImg",
"size": 10
*/
brandAgg.subAggregation(AggregationBuilders.terms("brandNameAgg").field("brandName").size(10));
brandAgg.subAggregation(AggregationBuilders.terms("brandImgAgg").field("brandImg").size(10));
sourceBuilder.aggregation(brandAgg);
//5.3)分类聚合
/*
"catalogAgg":{
"terms": {
"field": "catalogId",
"size": 10
},
"aggs": {
"catalogNameAgg": {
"terms": {
"field": "catalogName",
"size": 10
*/
TermsAggregationBuilder catalogAgg = AggregationBuilders.terms("catalogAgg").field("catalogId").size(10);
catalogAgg.subAggregation(AggregationBuilders.terms("catalogNameAgg").field("catalogName").size(1));
sourceBuilder.aggregation(catalogAgg);
//5.4)属性聚合
/*
"attrs":{
"nested": {"path": "attrs" },
"aggs": {
"attrIdAgg": {
"terms": {
"field": "attrs.attrId",
"size": 10
},
"aggs": {
"attrNameAgg": {
"terms": {
"field": "attrs.attrName",
"size": 10
"aggs": {
"attrValueAgg": {
"terms": {
"field": "attrs.attrValue",
"size": 10
*/
NestedAggregationBuilder attrsAgg = AggregationBuilders.nested("attr_agg", "attrs");
//聚合出id
TermsAggregationBuilder attrIdAgg = AggregationBuilders.terms("attrIdAgg").field("attrs.attrId").size(10);
//id里聚合出名字
attrIdAgg.subAggregation(AggregationBuilders.terms("attrNameAgg").field("attrs.attrName").size(10));
attrIdAgg.subAggregation(AggregationBuilders.terms("attrValueAgg").field("attrs.attrValue").size(10));
//小聚合放入大聚合
attrsAgg.subAggregation(attrIdAgg);
sourceBuilder.aggregation(attrsAgg);
//客户端打印
String s = sourceBuilder.toString();
System.out.println(s);
SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX}, sourceBuilder);
return searchRequest;
}
解析数据
SearchResult的结构
@Data
public class SearchResult {
/** * 查询到的所有商品信息*/
private List<SkuEsModel> products;
/*** 当前页码*/
private Integer pageNum;
/** 总记录数*/
private Long total;
/** * 总页码*/
private Integer totalPages;
/** 当前查询到的结果, 所有涉及到的品牌*/
private List<BrandVo> brands;
/*** 当前查询到的结果, 所有涉及到的分类*/
private List<CatalogVo> catalogs;
/** * 当前查询的结果 所有涉及到所有属性*/
private List<AttrVo> attrs;
/** 导航页 页码遍历结果集(分页) */
private List<Integer> pageNavs;
// ================以上是返回给页面的所有信息================
/** 导航数据*/
private List<NavVo> navs = new ArrayList<>();
/** 便于判断当前id是否被使用*/
private List<Long> attrIds = new ArrayList<>();
//面包屑导航
@Data
public static class NavVo {
private String name;
private String navValue;
private String link;
}
@AllArgsConstructor
@Data
public static class BrandVo {
private Long brandId;
private String brandName;
private String brandImg;
}
@Data
public static class CatalogVo {
private Long catalogId;
private String catalogName;
}
@AllArgsConstructor
@Data
public static class AttrVo {
private Long attrId;
private String attrName;
private List<String> attrValue;
}
}
方法逻辑如下:
构建面包屑 导航功能
private SearchResult buildSearchResult(SearchResponse response, SearchPara para) {
SearchResult result = new SearchResult();
SearchHits hits = response.getHits();
//1.返回查询到的所有商品
List<SkuEsModel> list = new ArrayList<>();
SearchHit[] searchHits = hits.getHits();
if (searchHits != null && searchHits.length > 0) {
for (SearchHit hit : searchHits) {
String sourceAsString = hit.getSourceAsString();
//解析数据
SkuEsModel esModel = JSON.parseObject(sourceAsString, SkuEsModel.class);
//如果有高亮的信息
if (!StringUtils.isEmpty(para.getKeyword())) {//按关键字检索了就设置高亮的字段
HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
String s = skuTitle.getFragments()[0].toString();
esModel.setSkuTitle(s);
}
list.add(esModel);
}
}
result.setProducts(list);
//2.商品分类信息
ParsedLongTerms catalogAgg = response.getAggregations().get("catalogAgg");//ParsedLongTerms就是Long类型的Term聚合
List<? extends Terms.Bucket> buckets = catalogAgg.getBuckets();
ArrayList<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
for (Terms.Bucket bucket : buckets) {
SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
catalogVo.setCatalogId(Long.parseLong(bucket.getKeyAsString()));
//名字在子聚合里面
ParsedStringTerms catalogNameAgg = bucket.getAggregations().get("catalogNameAgg");//ParsedStringTerms就是String类型的Term聚合
String catalogName = catalogAgg.getBuckets().get(0).getKeyAsString();
catalogVo.setCatalogName(catalogName);
catalogVos.add(catalogVo);
}
//3.当前商品的品牌信息
List<SearchResult.BrandVo> brandVos = new ArrayList<>();
Aggregations aggregations = response.getAggregations();
//ParsedLongTerms用于接收terms聚合的结果,并且可以把key转化为Long类型的数据
ParsedLongTerms brandAgg = aggregations.get("brand_agg");
for (Terms.Bucket bucket : brandAgg.getBuckets()) {
// 得到品牌id
Long brandId = bucket.getKeyAsNumber().longValue();
Aggregations subBrandAggs = bucket.getAggregations();
//得到品牌图片
ParsedStringTerms brandImgAgg = subBrandAggs.get("brandImgAgg");
String brandImg = brandImgAgg.getBuckets().get(0).getKeyAsString();
// 得到品牌名字
Terms brandNameAgg = subBrandAggs.get("brandNameAgg");
String brandName = brandNameAgg.getBuckets().get(0).getKeyAsString();
SearchResult.BrandVo brandVo = new SearchResult.BrandVo(brandId, brandName, brandImg);
brandVos.add(brandVo);
}
result.setBrands(brandVos);
//4.属性信息
List<SearchResult.AttrVo> attrVos = new ArrayList<>();
//ParsedNested用于接收内置属性的聚合
ParsedNested parsedNested = aggregations.get("attr_agg");
ParsedLongTerms attrIdAgg = parsedNested.getAggregations().get("attrIdAgg");
for (Terms.Bucket bucket : attrIdAgg.getBuckets()) {
//查询属性id
Long attrId = bucket.getKeyAsNumber().longValue();
Aggregations subAttrAgg = bucket.getAggregations();
//查询属性名
ParsedStringTerms attrNameAgg = subAttrAgg.get("attrNameAgg");
String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString();
//查询属性值
ParsedStringTerms attrValueAgg = subAttrAgg.get("attrValueAgg");
List<String> attrValues = new ArrayList<>();
for (Terms.Bucket attrValueAggBucket : attrValueAgg.getBuckets()) {
String attrValue = attrValueAggBucket.getKeyAsString();
attrValues.add(attrValue);
List<SearchResult.NavVo> navVos = new ArrayList<>();
}
SearchResult.AttrVo attrVo = new SearchResult.AttrVo(attrId, attrName, attrValues);
attrVos.add(attrVo);
}
result.setAttrs(attrVos);
//5.分页页码
result.setPageNum(para.getPageNum());
//6.总记录数,总页码
long totalRecord = hits.getTotalHits().value;
result.setTotal(totalRecord);
int total = (int) totalRecord;
int totalPages = total % EsConstant.PRODUCT_PAGESIZE == 0 ? total / EsConstant.PRODUCT_PAGESIZE : (total / EsConstant.PRODUCT_PAGESIZE) + 1;
result.setTotalPages(totalPages);
//7.构建面包屑 导航功能
if (para.getAttrs() != null && para.getAttrs().size() > 0) {
List<SearchResult.NavVo> navVos = para.getAttrs().stream().map(attr -> {
SearchResult.NavVo navVo = new SearchResult.NavVo();
//attrs=2_5寸:6寸
//分析每一个传过来的查询参数值
String[] s = attr.split("_");
navVo.setNavValue(s[1]);
R r = productFeignService.attrInfo(Long.parseLong(s[0]));
if (r.getCode() == 0) {
new TypeReference<SearchResult.AttrVo>() {
};
AttrResponseVo vo = r.getData("attr", new TypeReference<AttrResponseVo>() {
});
navVo.setNavValue(vo.getAttrName());//拿到属性的名字
} else {
navVo.setName(s[0]);
}
//取消了面包屑以后我们要跳转到哪--》将请求地址的url里面的当前置空
//拿到所有查询条件,去掉当前--》在controller里拿到值
String encode = "";
try {
encode = URLEncoder.encode(attr, "UTF-8");//前端的数据是编码过的
encode.replace("+", "%20");//空格会被浏览器解析为%20然后被java解析成+
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String replace = para.get_queryString().replace("&attrs=" + attr, "").replace("attrs=" + attr + "&", "").replace("attrs=" + attr, "");
navVo.setLink("http://search.gulimall.com/list.html?" + replace);
return navVo;
}).collect(Collectors.toList());
result.setNavs(navVos);
}
return result;
}
购物车功能
需求分析:购物车分为离线购物车和在线购物车;离线购物车可以购买商品,在用户登录之后,离线购物车会被清空,所有商品转移到在线购物车,即使浏览器关闭,离线购物车的数据还要在;由于其读多写多的特性,我们将数据放入redis中
preHandler帮我们在ThreadLocal里设置了一个用户信息,postHandler帮我们删除了此用户的信息,并且设置了用户的Cookie
添加购物车
获得分组属性例如颜色:蓝色 版本:256G
<select id="getSkuSaleAttrValuesAsStringList" resultType="java.lang.String">
select concat(attr_name,":",attr_value) from pms_sku_sale_attr_value where sku_id=#{skuId}
</select>
获取要操作的购物车
private BoundHashOperations<String, Object, Object> getCartOps() {
//操作购物车
UserInfoVo userInfoVo = CartInterceptor.threadLocal.get();
String key = "";
if (userInfoVo.getUserId() != null) {//使用登录后的购物车
key = CartConstant.CART_PREFIX + userInfoVo.getUserId();
} else {//使用离线购物车
key = CartConstant.CART_PREFIX + userInfoVo.getUserKey();
}
//绑定哈希操作
BoundHashOperations<String, Object, Object> hashOps = redisTemplate.boundHashOps(key);
return hashOps;
}
public CartItem addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
//1.远程查询当前要添加的商品信息
String res = (String) cartOps.get(skuId.toString());
if (StringUtils.isEmpty(res)) {
CartItem cartItem = new CartItem();
CompletableFuture<Void> skuInfoFuture = CompletableFuture.runAsync(() -> {
R info = productFeignService.info(skuId);
if (info.getCode() != 0) {
log.error("商品服务远程调用失败");
}
SkuInfoVo skuInfo = info.getData("skuInfo", new TypeReference<SkuInfoVo>() {
});
cartItem.setCheck(true);
cartItem.setCount(num);
cartItem.setImage(skuInfo.getSkuDefaultImg());
cartItem.setTitle(skuInfo.getSkuTitle());
cartItem.setSkuId(skuId);
cartItem.setPrice(skuInfo.getPrice());
}, threadPoolExecutor);//使用自己的线程池进行调度
CompletableFuture<Void> attrsFuture = CompletableFuture.runAsync(() -> {
//2.远程查询展示组合信息
List<String> skuAttrValues = productFeignService.getSkuAttrValues(skuId);
cartItem.setSkuAttr(skuAttrValues);
}, threadPoolExecutor);
CompletableFuture.allOf(skuInfoFuture, attrsFuture).get();//等待两个步骤都完成再封装
String s = JSON.toJSONString(cartItem);//转成Json给redis
cartOps.put(skuId.toString(), s);
return cartItem;
}else{
//如果以前已经有了那么就直接加数量就行
CartItem cartItem = JSON.parseObject(res, CartItem.class);
cartItem.setCount(cartItem.getCount()+num);
//更新一下redis
cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
return cartItem;
}
}
合并购物车
@Override
public Cart getCart() throws ExecutionException, InterruptedException {
Cart cart = new Cart();
UserInfoVo userInfoVo=CartInterceptor.threadLocal.get();
String tempKey = CartConstant.CART_PREFIX + userInfoVo.getUserKey();
String loginKey = CartConstant.CART_PREFIX + userInfoVo.getUserId();
if(userInfoVo.getUserId()!=null){
//用户登录
//如果临时购物车的数据还没有进行合并
List<CartItem> tempCartItems = getCartItems(tempKey);
if(tempCartItems!=null&&tempCartItems.size()>0){
for (CartItem item : tempCartItems) {
//todo 可优化部分
addToCart(item.getSkuId(), item.getCount());
}
//清空临时购物车
clearCart(tempKey);
}
//获取登录后的购物车数据,此时已经包含了临时购物车的数据
List<CartItem> cartItems = getCartItems(loginKey);
cart.setItems(cartItems);
}else{
List<CartItem> cartItems = getCartItems(tempKey);
cart.setItems(cartItems);
}
return cart;
}
提交订单业务
//@GlobalTransactional//全局事务注解,高并发模式下并不适用
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo orderSubmitVo) {
threadLocal.set(orderSubmitVo);
SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
//下单:去创建订单,检验令牌,检验价格,锁定库存
//1.验证令牌
String orderToken = orderSubmitVo.getOrderToken();//页面传递过来的值
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
String key = OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId();//redis里存的key
//lua脚本保证原子性 返回1代表删除成功,0代表删除失败
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Long res = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(key), orderToken);
if(res==1){
//验证成功--下单创建订单,检验令牌,检验价格,锁库存
OrderCreateVo order = createOrder();
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = orderSubmitVo.getPayPrice();
if(Math.abs(payAmount.subtract(payPrice).doubleValue())<0.01){
//金额对比成功
//保存信息
saveOrder(order);
//锁定库存,只要有异常就回滚订单数据
WareSkuLockVo wareSkuLockVo = new WareSkuLockVo();
wareSkuLockVo.setOrderSn(order.getOrder().getOrderSn());
List<OrderItemVo> locks=order.getOrderItems().stream().map(item->{
OrderItemVo orderItemVo = new OrderItemVo();
orderItemVo.setSkuId(item.getSkuId());
orderItemVo.setCount(item.getSkuQuantity());
orderItemVo.setTitle(item.getSkuName());
return orderItemVo;
}).collect(Collectors.toList());
wareSkuLockVo.setLocks(locks);
//远程锁库存操作
R r = wareFeignService.orderLockStock(wareSkuLockVo);
if(r.getCode()==0){
//锁定成功了
responseVo.setOrder(order.getOrder());
responseVo.setCode(0);
//到这里下面的步骤都不可能失败了,所以可以发送订单创建成功的消息
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());
return responseVo;
}else{
//锁定失败了
responseVo.setCode(3);
throw new NoStockException();
}
}else{
responseVo.setCode(2);
return responseVo;
}
}else{
//验证失败
return responseVo;
}
}
最关键的方法是远程锁库存的逻辑orderLockStock方法--此时还没有引入RabbitMQ
方法逻辑:
//为订单锁定库存
@Override
@Transactional(rollbackFor =NoStockException.class )//不写class也可以,默认是运行时异常都会回滚
public Boolean orderLockStock(WareSkuLockVo vo) {
//1.找到每个商品在那个仓库都有库存
List<OrderItemVo> locks = vo.getLocks();
List<SkuWareHasStock> skuWareHasStocks = locks.stream().map(item -> {
SkuWareHasStock stock = new SkuWareHasStock();
Long skuId = item.getSkuId();
stock.setSkuId(skuId);
stock.setNum(item.getCount());
//查询这个商品在哪有库存
List<Long> list = wareSkuDao.listWareIdHasStock(skuId);
stock.setWareId(list);
return stock;
}).collect(Collectors.toList());
//2.锁定库存
for (SkuWareHasStock skuWareHasStock : skuWareHasStocks) {
Boolean skuStocked=false;//当前商品是否锁住
Long skuId = skuWareHasStock.getSkuId();
List<Long> wareIds = skuWareHasStock.getWareId();
if(wareIds==null||wareIds.size()==0){
//没有任何仓库有这个商品的库存,订单失败,全部回滚
throw new NoStockException(skuId);
}
for (Long wareId : wareIds) {
Long count= wareSkuDao.lockSkuStock(skuId,wareId,skuWareHasStock.getNum());
if(count==1){
skuStocked=true;//锁定成功
break;
}else{
//当前仓库锁定失败,尝试下一个仓库
}
}
if(!skuStocked){
//当前商品所有仓库都没有锁住
throw new NoStockException(skuId);
}
}
//能走到这就说明锁定成功了
return true;
}
两个方法
lockSkuStock方法当库存总量-锁定库存量大于当前商品购买量的时候更新锁定库存数,如果该方法返回1就说明锁定库存成功了
listWareIdHasStcok 查询到当前有库存的仓库id
<update id="lockSkuStock">
update wms_ware_sku set stock_locked=stock_locked+#{num}
where sku_Id=#{skuId} and ware_id=#{wareId} and stock-stock_locked>=#{num}
</update>
<select id="listWareIdHasStock" resultType="java.lang.Long">
select ware_id from wms_ware_sku where sku_id=#{skuId} and stock-stock_locked>0
</select>
库存模块自动解锁功能
前情:在库存服务的orderLockStock方法中(该方法曾被submitOrder方法远程调用),我们发送了rabbitMQ的信息
//为订单锁定库存
@Override
@Transactional(rollbackFor = NoStockException.class)//不写class也可以,默认是运行时异常都会回滚
public Boolean orderLockStock(WareSkuLockVo vo) {
/*
保存库存工作单的详情
*/
WareOrderTaskEntity wareOrderTaskEntity = new WareOrderTaskEntity();
wareOrderTaskEntity.setOrderSn(vo.getOrderSn());
wareOrderTaskService.save(wareOrderTaskEntity);
//1.找到每个商品在那个仓库都有库存
List<OrderItemVo> locks = vo.getLocks();
List<SkuWareHasStock> skuWareHasStocks = locks.stream().map(item -> {
SkuWareHasStock stock = new SkuWareHasStock();
Long skuId = item.getSkuId();
stock.setSkuId(skuId);
stock.setNum(item.getCount());
//查询这个商品在哪有库存
List<Long> list = wareSkuDao.listWareIdHasStock(skuId);
stock.setWareId(list);
return stock;
}).collect(Collectors.toList());
//2.锁定库存
for (SkuWareHasStock skuWareHasStock : skuWareHasStocks) {
Boolean skuStocked = false;//当前商品是否锁住
Long skuId = skuWareHasStock.getSkuId();
List<Long> wareIds = skuWareHasStock.getWareId();
if (wareIds == null || wareIds.size() == 0) {
//没有任何仓库有这个商品的库存,订单失败,全部回滚
throw new NoStockException(skuId);
}
/*
1.如果每一个商品都锁定成功,将当前商品锁定了几件的工作单记录发给MQ
2.锁定失败,前面保存的工作信息就回滚了,但是消息还是发送出去了,由于回滚会查不到消息里数据的id,所以也没关系
*/
for (Long wareId : wareIds) {
Long count = wareSkuDao.lockSkuStock(skuId, wareId, skuWareHasStock.getNum());
if (count == 1) {
skuStocked = true;//锁定成功
/*
锁定成功后保存库存工作单详情的信息
*/
WareOrderTaskDetailEntity detailEntity = new WareOrderTaskDetailEntity();
detailEntity.setSkuId(skuId);
detailEntity.setSkuNum(skuWareHasStock.getNum());
detailEntity.setWareId(wareId);
detailEntity.setTaskId(wareOrderTaskEntity.getId());
detailEntity.setLockStatus(1);//1表示锁定成功,2表示解锁 3表示扣减
wareOrderTaskDetailService.save(detailEntity);
/*
给RabbitMQ发送消息
*/
StockLockedTo stockLockedTo = new StockLockedTo();
stockLockedTo.setId(wareOrderTaskEntity.getId());
StockDetailTo stockDetailTo = new StockDetailTo();
BeanUtils.copyProperties(detailEntity,stockDetailTo);
//如果只发To只封装detailId的话,数据回滚后就完全没有用户之前锁定的信息了
stockLockedTo.setDetailTo(stockDetailTo);
rabbitTemplate.convertAndSend("stock-event-exchange", "stock.locked",stockLockedTo);
break;
} else {
//当前仓库锁定失败,尝试下一个仓库
skuStocked=false;
}
}
if (!skuStocked) {
//当前商品所有仓库都没有锁住
throw new NoStockException(skuId);
}
}
//能走到这就说明锁定成功了
return true;
}
在closeOrder方法中我们给库存服务发送了
功能运用场景:
1.下订单成功,订单过期没有支付被系统自动取消,或者被用户手动取消的时候
2.下订单成功,库存锁定成功,但是接下来的业务调用失败导致订单回滚,之前锁定的库存就要自动解锁
库存模块创建的RabbitMQ配置
package com.wuyimin.gulimall.ware.config;
/**
* @ Author wuyimin
* @ Date 2021/8/25-18:58
* @ Description 返回Json配置
*/
@Configuration
public class MyRabbitConfig {
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();//返回消息转成json
}
//交换机
@Bean
public Exchange stockEventExchange(){
return new TopicExchange("stock-event-exchange",true,false,null);
}
//普通队列用于解锁库存
@Bean
public Queue stockReleaseStockQueue(){
return new Queue("stock.release.stock.queue",true,false,false,null);
}
//延迟队列
@Bean
public Queue stockDelayQueue(){
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "stock-event-exchange");
arguments.put("x-dead-letter-routing-key", "stock.release");
// 消息过期时间 2分钟
arguments.put("x-message-ttl", 120000);
return new Queue("stock.delay.queue",true,false,false,arguments);
}
/**
* 交换机和延迟队列绑定
*/
@Bean
public Binding stockLockedBinding() {
return new Binding("stock.delay.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.locked",
null);
}
/**
* 交换机和普通队列绑定
*/
@Bean
public Binding stockReleaseBinding() {
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.release.#",
null);
}
}
由于库存模块需要调用订单模块的内容,而之前在订单模块配置的拦截器导致远程调用无法成功(因为需要登录)这显然是不合情理的,所以对于特定的远程调用url我们进行放行;这里调用的是AntPathMather()对象的match方法来做判断
package com.wuyimin.gulimall.order.interceptor;
/**
* @ Author wuyimin
* @ Date 2021/8/26-10:52
* @ Description 拦截未登录用户
*/
@Component //放入容器中
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberRespVo> loginUser=new ThreadLocal<>();//方便其他请求拿到
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri=request.getRequestURI();
//放行远程调用,匹配/order/order/status/**的uri直接放行
boolean match = new AntPathMatcher().match("/order/order/status/**", uri);
if(match){
return true;
}
xxxxx
}
}
在ware服务里创建一个Listener监听队列消息
package com.wuyimin.gulimall.ware.listener;
/**
* @ Author wuyimin
* @ Date 2021/8/29-20:53
* @ Description
*/
@Service
public class StockReleaseListener {
@Autowired
WareSkuService wareSkuService;
//这里我放在类方法上报错了
@RabbitListener(queues = "stock.release.stock.queue")//监听队列
/*
处理消息的方法(解锁)
*/
public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
System.out.println("进入了方法");
try {
System.out.println("收到了消息开始处理。。。");
wareSkuService.handleStockLockedRelease(to);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);//不选择批量回复
} catch (Exception e) {
System.out.println("拒收了消息。。。");
//有异常就让他重新回队
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
其中处理消息的方法逻辑如下:
wareOrderTaskDetai对应的数据库表
wareOrderTask就orderSn这个关键属性
StcockLockedTo关联了订单号和具体订单号
@Data
public class StockLockedTo implements Serializable {
private Long id;
private StockDetailTo detailTo;//工作单详情的所有id
}
/*
处理消息的方法(解锁)
*/
@Override
public void handleStockLockedRelease(StockLockedTo to) {
System.out.println("收到了解锁库存的消息");
StockDetailTo detailTo = to.getDetailTo();//详细信息
Long detailToId = detailTo.getId();//具体工作单的id
/*
解锁的两种情况:根据我们拿到的To,我们取查询数据库,看是否能得到订单的锁定库存结果
如果没有:库存锁定失败了,库存回滚,无需解锁,无需操作
如果有:说明是库存以下的10/0错误,这种时候需要回滚
*/
WareOrderTaskDetailEntity byId = wareOrderTaskDetailService.getById(detailToId);
if(byId!=null){
//解锁--到此库存系统一切正常,但是不确定订单是什么情况
//订单情况1:没有这个订单(订单回滚),必须解锁
//订单情况2:有这个订单,要看订单状态(如果是已取消:就可以取解锁库存,其他任何状态都不可以解锁库存)
Long orderId = to.getId();//拿到订单的id
WareOrderTaskEntity orderTaskEntity = wareOrderTaskService.getById(orderId);
String orderSn = orderTaskEntity.getOrderSn();//订单号,我们需要拿着这个订单取查询订单状态
R r = orderFeignService.getOrderByOrderSn(orderSn);
if(r.getCode()==0){
//远程调用成功
Integer status = r.getData(new TypeReference<Integer>() {
});
if(status==null||status==4){
//订单已经被取消了,订单不存在, 解锁库存
if(byId.getLockStatus()==1){//当前具体工作单必须是未解锁状态才行
unlockStock(detailTo.getSkuId(),detailTo.getWareId(),detailTo.getSkuNum(),detailTo.getId());
}
}
}else{
//远程服务失败
throw new RuntimeException("远程服务失败,消息消费失败");
}
}
}
远程调用的getOrderByOrderSn方法,需要注意的是,这里没有数据也是一种成功的情况,也表明订单不存在了
@GetMapping("/status/{orderSn}")
public R getOrderByOrderSn(@PathVariable("orderSn") String orderSn){
OrderEntity order_sn = orderService.getOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderSn));
if(order_sn!=null){
Integer status = order_sn.getStatus();
return R.ok().setData(status);
}
else return R.ok().setData(null);
}
unlock解锁方法
<update id="unlockStock">
update wms_ware_sku set stock_locked=stock_locked-#{num}
where sku_id=#{skuId} and ware_id=#{wareId}
</update>
定时关单逻辑实现
order服务确立的绑定关系,这里就是一个延时1分钟逻辑
@Configuration
public class MyMQConfig {
//创建绑定关系,队列和交换机的便捷方式
@Bean
public Queue orderDelayQueue(){
HashMap<String, Object> map = new HashMap<>();
map.put("x-dead-letter-exchange","order-event-exchange");//死信路由
map.put("x-dead-letter-routing-key","order.release.order");//死信的路由键
map.put("x-message-ttl",60000);//消息过期时间一分钟
//队列名字,是否持久化,是否排他(只能被一个连接使用),是否自动删除
return new Queue("order.delay.queue",true,false,false,map);
}
@Bean
public Queue orderReleaseOrderQueue(){
return new Queue("order.release.order.queue",true,false,false);
}
@Bean
public Exchange orderEventExchange(){
//名字,是否持久化,是否自动删除 Topic交换机可以绑定多个队列
return new TopicExchange("order-event-exchange",true,false);
}
@Bean
//两个绑定关系
public Binding orderCreateOrder(){
return new Binding("order.delay.queue", Binding.DestinationType.QUEUE,
"order-event-exchange","order.create.order",null);
}
@Bean
public Binding orderReleaseOrder(){
return new Binding("order.release.order.queue", Binding.DestinationType.QUEUE,
"order-event-exchange","order.release.order",null);
}
}
生产者发送消息,也就是发送给交换机order-event-exchange
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo orderSubmitVo) {
xxxxxx
//RabbitMQ发送消息---到这里下面的步骤都不可能失败了,所以可以发送订单创建成功的消息
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order);
return responseVo;
xxxxxx
}
按照我们指定的延时逻辑它会进入死信队列1分钟后路由到order.release.order.queue,消费者消费此消息
package com.wuyimin.gulimall.order.listener;
@RabbitListener(queues = "order.release.order.queue")
@Component
public class OrderCloseListener {
@Autowired
OrderService orderService;
@RabbitHandler
public void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException {
System.out.println("收到过期的订单信息:准备关闭订单"+orderEntity);
//手动签收消息(拿到原生消息,选择不批量告诉)
try {
orderService.closeOrder(orderEntity);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
接受到消息后调用我们的closeOrder关单方法
package com.wuyimin.gulimall.order.service.impl;
//订单过期后关闭订单
@Override
public void closeOrder(OrderEntity orderEntity) {
OrderEntity byId = this.getById(orderEntity);
if(byId.getStatus().equals(OrderStatusEnum.CREATE_NEW.getCode())){
//只有待付款的状态需要关单
byId.setStatus(OrderStatusEnum.CANCLED.getCode());
this.updateById(byId);
}
}
//保存订单信息
private void saveOrder(OrderCreateVo orderCreateTo) {
OrderEntity order = orderCreateTo.getOrder();
order.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
order.setCreateTime(new Date());
order.setModifyTime(new Date());
this.save(order);
orderItemService.saveBatch(orderCreateTo.getOrderItems());
}
现在存在的问题,如果在一分钟期间卡死,只检查两分钟那一次,没检查到但是后续库存又被锁了,就会一直被锁
此时我们添加一个绑定关系,让他关单之后立即去通知库存服务解锁库存
package com.wuyimin.gulimall.order.config;
/**
* @ Author wuyimin
* @ Date 2021/8/29-15:59
* @ Description 测试MQ可用性
*/
@Configuration
public class MyMQConfig {
/*
添加的绑定关系
*/
@Bean
public Binding orderReleaseOther(){
return new Binding("stock.release.stock.queue", Binding.DestinationType.QUEUE,
"order-event-exchange","order.release.other.#",null);
}
}
关单的时候发送消息给库存服务,相当于上了一个双保险,注意这里发送的消息是以order.release.other发出的,会跟着上图的交换机被路由到stock.release.stock.queue队列
package com.wuyimin.gulimall.order.service.impl;
@Override
public void closeOrder(OrderEntity orderEntity) {
OrderEntity byId = this.getById(orderEntity);
if(byId.getStatus().equals(OrderStatusEnum.CREATE_NEW.getCode())){
//只有待付款的状态需要关单
byId.setStatus(OrderStatusEnum.CANCLED.getCode());
this.updateById(byId);
/*
给仓储服务发送消息
*/
OrderTo orderTo = new OrderTo();
BeanUtils.copyProperties(orderEntity,orderTo);
rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
}
}
我们使用RabbitListener和RabbitHandler注解配合接受两种不同的实体类
package com.wuyimin.gulimall.ware.listener;
/**
* @ Author wuyimin
* @ Date 2021/8/29-20:53
* @ Description
*/
@RabbitListener(queues = "stock.release.stock.queue")//监听队列
@Service
public class StockReleaseListener {
@Autowired
WareSkuService wareSkuService;
/*
处理消息的方法(解锁)
*/
@RabbitHandler
public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
System.out.println("进入了方法");
try {
System.out.println("收到了消息开始处理。。。");
wareSkuService.handleStockLockedRelease(to);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);//不选择批量回复
} catch (Exception e) {
System.out.println("拒收了消息。。。");
//有异常就让他重新回队
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
@RabbitHandler
/*
处理消息的方法(解锁)
*/
public void handleOrderCloseRelease(OrderTo to, Message message, Channel channel) throws IOException {
System.out.println("收到订单关闭的消息");
try{
wareSkuService.handleStockLockedRelease(to);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);//不选择批量回复
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
其中第二个方法的handleStockLockedRelease方法逻辑如下:
package com.wuyimin.gulimall.ware.service.impl;
/*
考虑服务卡顿
*/
@Transactional
@Override
public void handleStockLockedRelease(OrderTo to) {
String orderSn=to.getOrderSn();
//查以下最新库存的状态
WareOrderTaskEntity orderTaskEntity=wareOrderTaskService.getOrderTaskByOrderSn(orderSn);//按照名字来查到库存工作单
Long id = orderTaskEntity.getId();
//按照工作单id找到所有工作单详情
List<WareOrderTaskDetailEntity> detailEntities = wareOrderTaskDetailService.list(
new QueryWrapper<WareOrderTaskDetailEntity>().eq("task_id", id).eq("lock_status",1));//状态为1表示未解锁 2为已解锁
//解锁操作
for (WareOrderTaskDetailEntity detailEntity : detailEntities) {
unlockStock(detailEntity.getSkuId(),detailEntity.getWareId(),detailEntity.getSkuNum(),detailEntity.getId());
}
}
整个订单和仓库服务的整体逻辑
秒杀功能
秒杀基本流程
刷新秒杀商品的上架信息
使用分布式锁,对秒杀商品上架进行处理,由于它是一个定时任务,如果每台及其都来执行这个定时任务就无法保证幂等性,这时就需要分布式锁
@Slf4j
@Service
public class SeckillSkuScheduled {
@Autowired
SecKillService secKillService;
@Autowired
RedissonClient redissonClient;
private final String UPLOAD_LOCK="seckill:upload:lock";
//每分钟上架以后三天要秒杀的商品
@Scheduled(cron ="0 * * * * ?")
public void uploadSeckillSkuLatest3Days(){
log.info("上架商品信息");
//1.重复上架无需处理
//添加分布式锁
RLock lock = redissonClient.getLock(UPLOAD_LOCK);
lock.lock(10, TimeUnit.SECONDS);
try {
secKillService.uploadSeckillSkuLatest3Days();
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
需要处理的问题:
如果一个物品的库存信息(用信号量来模拟在第一场秒杀活动上架了,第二场需要再上架该商品的库存信息,不能因为第一场商品有了第二场就不上架)
如果一个商品在两场活动中都有,我们根据前缀+id的方式无法判断它来自哪个场次,所以key应该是前缀+id+场次
public void uploadSeckillSkuLatest3Days() {
//1.去扫描需要参与秒杀的活动
R r = couponFeignService.getLates3DaySession();
if (r.getCode() == 0) {
//上架商品数据
List<SeckillSessionsWithSkus> data = r.getData(new TypeReference<List<SeckillSessionsWithSkus>>() {
});
//缓存活动信息
saveSessionInfos(data);
//缓存活动的关联商品信息
saveSessionSkuInfos(data);
}
}
远程查询获得三天内秒杀商品的方法:
@Override
public List<SeckillSessionEntity> getLates3DaySession() {
String startTime = getStartTime();
String endTime = getEndTime();
//筛选日期
List<SeckillSessionEntity> list = this.list(new QueryWrapper<SeckillSessionEntity>().between("start_time", startTime, endTime));
if(list!=null&&list.size()>0){
List<SeckillSessionEntity> collect = list.stream().peek(item -> {
Long id = item.getId();
//设置商品
List<SeckillSkuRelationEntity> entities = seckillSkuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", id));
item.setRelationEntities(entities);
}).collect(Collectors.toList());
return collect;
}
return null;
}
保存活动的信息的函数:
private void saveSessionInfos(List<SeckillSessionsWithSkus> data) {
data.forEach(session -> {
Long startTime = session.getStartTime().getTime();
Long endTime = session.getEndTime().getTime();
String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;
Boolean aBoolean = redisTemplate.hasKey(key);
if (!aBoolean) {
List<String> collect = session.getRelationEntities().stream().map(item -> item.getPromotionSessionId() + "_" + item.getSkuId().toString()
).collect(Collectors.toList());
//以集合的方式向左边批量添加元素
redisTemplate.opsForList().leftPushAll(key, collect);
}
});
}
保存此次活动关联的所有商品sku的函数:
private void saveSessionSkuInfos(List<SeckillSessionsWithSkus> data) {
data.forEach(item -> {
BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
item.getRelationEntities().forEach(vo -> {
//解决问题1,2
if (!ops.hasKey(vo.getPromotionSessionId() + "_" + vo.getSkuId().toString())) {
SeckillSkuRedisTo to = new SeckillSkuRedisTo();
//除了秒杀信息我们还需要sku的详细信息
R info = productFeignService.info(vo.getSkuId());
if (info.getCode() == 0) {
SkuInfoVo skuInfo = info.getData("skuInfo", new TypeReference<SkuInfoVo>() {
});
if (skuInfo != null) {
to.setSkuInfoVo(skuInfo);
}
}
//秒杀信息
BeanUtils.copyProperties(vo, to);
//保存开始结束时间
to.setStartTime(item.getStartTime().getTime());
to.setEndTime(item.getEndTime().getTime());
//保存随机码--公平秒杀
String token = UUID.randomUUID().toString().replace("_", "");
to.setRandomCode(token);
to.setSeckillLimit(1);
//获取信号量,限流
//如果两个场次都有这个秒杀商品,当前这个商品场次的库存信息已经上架了就不需要再上架了
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
semaphore.trySetPermits(vo.getSeckillCount());//商品可以秒杀的数量作为信号量
String s = JSON.toJSONString(to);
ops.put(vo.getPromotionSessionId().toString() + "_" + vo.getSkuId().toString(), s);
}
});
});
}
查询到当前会场秒杀商品
@Override
public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
//确定当前时间是哪个秒杀场次的
Date date = new Date();
long time = date.getTime();
Set<String> keys = redisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");//获得所有带有这个前缀的key,只要后面截取的时间段包含现在的时间就表面在场次内
for (String key : keys) {
String replace = key.replace(SESSIONS_CACHE_PREFIX, "");
String[] s = replace.split("_");
Long start = Long.parseLong(s[0]);
Long end = Long.parseLong(s[1]);
if (start <= time && time <= end) {
//是当前场次,准备返回商品信息
List<String> range = redisTemplate.opsForList().range(key, -100, 100);
BoundHashOperations<String, String, Object> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
List<Object> objects = hashOps.multiGet(range);
if (objects != null) {
List<SeckillSkuRedisTo> collect = objects.stream().map(item -> {
SeckillSkuRedisTo to = JSON.parseObject(item.toString(), SeckillSkuRedisTo.class);
return to;
//当前秒杀开始了需要随机码
}).collect(Collectors.toList());
return collect;
}
}
}
//获取这个秒杀场次需要的所有商品信息
return null;
}
秒杀功能的接口设计
- 服务单一职责+独立部署:秒杀服务顶不住压力挂掉不会影响别人(已经完成)
- 秒杀链接加密:防止恶意攻击,1000次/s的请求攻击,防止链接暴露,工作人员提前秒杀商品(已经完成--随机码)
- 库存预热+快速扣减:秒杀读多写少,无需每次实时校验库存,我们库存预热放到redis中,信号量控制进来秒杀的请求(已经完成)
- 动静分离:nginx做好动静分离,保证秒杀和商品详情页的动态请求才打到后端服务器集群,使用cdn网络,分担本集群的压力(已经完成)
- 恶意请求拦截:识别非法攻击请求并且拦截
- 流量错峰:使用各种手段,将流量分担到更大宽度的时间点,比如验证码,加入购物车
- 限流,熔断,降级:前端限流+后端限流,限制次数,限制总量,快速失败降级运行,熔断防止雪崩
- 队列削封:所有秒杀成功的请求,进入队列,慢慢创建,扣减库存即可
秒杀进一步流程--添加MQ
秒杀服务
@GetMapping("/kill")
public String secKill(@RequestParam("killId") String killId, @RequestParam("key") String key, @RequestParam("num") Integer num, Model model){
//1.判断是否登录-整合拦截器机制和springSession
//2.秒杀成功就返回这个订单号
String orderSn=secKillService.kill(killId,key,num);
model.addAttribute("orderSn",orderSn);
return "success";
}
package com.wuyimin.gulimall.seckill.service.impl;
@Override
public String kill(String killId, String key, Integer num) {
MemberRespVo user = LoginUserInterceptor.loginUser.get();//获得用户信息
//获取当前秒杀商品的详细信息
BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
String s = ops.get(killId);
if (StringUtils.isEmpty(s)) {
return null;
} else {
SeckillSkuRedisTo to = JSON.parseObject(s, SeckillSkuRedisTo.class);
//校验合法性,判断时间区间(略)
//校验随机码
String randomCode = to.getRandomCode();
if (randomCode.equals(key)) {
//校验购物数量是否合理
if (num <= to.getSeckillLimit()) {
//判断此用户是否已经买过了,只要秒杀成功了,就去redis里占位 用户id_sessionId_skuId
String userKey = user.getId() + "_" + to.getPromotionSessionId() + "_" + to.getSkuId();
//存的是买的商品的数量,超时时间设置(本来应该是整个会场持续的时间这里模拟30s)
Boolean hasNotBought = redisTemplate.opsForValue().setIfAbsent(userKey, num.toString(), 30, TimeUnit.SECONDS);//不存在的时候才占位
if (hasNotBought) {
//从来没买过就获取信号量
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
try {
boolean isGet = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);//这个获取信号量的方法是非阻塞的,等待时间未100ms
String orderSn = IdWorker.getTimeId()+ UUID.randomUUID().toString().replace("_","");
if(isGet){
//给mq发送消息
SecKillTo secKillTo = new SecKillTo();
secKillTo.setMemberId(user.getId());
secKillTo.setNum(num);
secKillTo.setPromotionSessionId(to.getPromotionSessionId());
secKillTo.setOrderSn(orderSn);
secKillTo.setSeckillPrice(to.getSeckillPrice());
secKillTo.setSkuId(to.getSkuId());
rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",secKillTo);
return orderSn;
}
} catch (InterruptedException e) {
return null;
}
}
}
}
}
return null;
}
}
添加路由关系,最终会交给到order.seckill.order这个队列中
package com.wuyimin.gulimall.order.config;
@Bean
public Queue orderSeckillOrderQueue(){
return new Queue("order.seckill.order.queue",true,false,false);
}
@Bean
public Binding orderSeckillOther(){
return new Binding("order.seckill.order.queue", Binding.DestinationType.QUEUE,
"order-event-exchange","order.seckill.order",null);
}
创建监听器
package com.wuyimin.gulimall.order.listener;
/**
* @ Author wuyimin
* @ Date 2021/9/1-14:06
* @ Description
*/
@Slf4j
@Component
@RabbitListener(queues = "order.seckill.order.queue")
public class OrderSecKillListener {
@Autowired
OrderService orderService;
@RabbitHandler
public void listener(SecKillTo to, Channel channel, Message message) throws IOException {
System.out.println("准备创建秒杀单的详细信息"+to);
//手动签收消息(拿到原生消息,选择不批量告诉)
try {
orderService.createSeckill(to);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
保留秒杀的信息到数据库
@Override
public void createSeckill(SecKillTo to) {
//保存订单信息
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(to.getOrderSn());
orderEntity.setMemberId(to.getMemberId());
orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
orderEntity.setPayAmount(to.getSeckillPrice().multiply(new BigDecimal(to.getNum().toString())));
save(orderEntity);
//保存订单项目信息
OrderItemEntity itemEntity = new OrderItemEntity();
itemEntity.setOrderSn(to.getOrderSn());
itemEntity.setRealAmount(to.getSeckillPrice().multiply(new BigDecimal(to.getNum().toString())));
itemEntity.setSkuQuantity(to.getNum());
orderItemService.save(itemEntity);
}