33.项目总结-功能部分

本文详细介绍了电商系统中的购物车功能实现,包括离线购物车和在线购物车的处理,以及如何在用户登录后合并购物车。此外,还讲解了如何通过Redis实现库存的分布式锁,确保并发下的库存准确性。在订单创建过程中,涉及到了三级分类的查询、商品属性分组的检索以及商品上架和下架的逻辑。同时,文章还探讨了如何利用RabbitMQ进行订单创建后的消息通知,以及如何处理秒杀场景下的订单创建。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

--仅列出典型的业务逻辑

三级分类

组装属性结构

目标:查出所有的三级分类,并且要组装成父子结构

表的结构如下,其中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);
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值