学习项目笔记及心得

商品上下架功能是怎么实现的?实现过程?

b20a6a5b5d70483287f34ce1944009ee.png

product服务商品上下架接口向MQ发送异步消息

  //    商品上架
    @Override
    public void publish(Long skuId, Integer status) {
        if (status == 1){   //上架
            SkuInfo skuInfo = baseMapper.selectById(skuId);
            skuInfo.setPublishStatus(status);
            baseMapper.updateById(skuInfo);
            //整合mq把数据同步到es里面
            rabbitService.sendMessage(MqConst.EXCHANGE_GOODS_DIRECT,
                                    MqConst.ROUTING_GOODS_UPPER,
                                     skuId);
        }else{  //下架
            SkuInfo skuInfo = baseMapper.selectById(skuId);
            skuInfo.setPublishStatus(status);
            baseMapper.updateById(skuInfo);
            //整合mq把数据同步到es里面
            rabbitService.sendMessage(MqConst.EXCHANGE_GOODS_DIRECT,
                    MqConst.ROUTING_GOODS_LOWER,
                    skuId);
        }

    }

search服务接收MQ消息

@Component
public class SkuReceiver {

    @Autowired
    private SkuService skuService;

//    商品上架
    @RabbitListener(bindings = @QueueBinding(   //这是一个 RabbitMQ 消息监听器的注解,指定了监听的队列和消息处理逻辑。
            value = @Queue(value = MqConst.QUEUE_GOODS_UPPER, durable = "true"),//定义了一个名为 MqConst.QUEUE_GOODS_UPPER 的队列,并设置 durable 属性为 true,表示该队列是持久化的,即使 RabbitMQ 服务重启后也不会丢失该队列中的消息。
            exchange = @Exchange(value = MqConst.EXCHANGE_GOODS_DIRECT),//指定了消息交换机的名称,这里是 MqConst.EXCHANGE_GOODS_DIRECT。
            key = {MqConst.ROUTING_GOODS_UPPER}//指定了队列与交换机之间的路由键,即消息发送到该交换机后,根据路由键发送到相应的队列。
    ))
    public void upperSku(Long skuId, Message message, Channel channel) throws IOException {
        //Long skuId: 接收到的消息中携带的商品ID。
        //Message message: RabbitMQ 的消息对象,包含了消息的各种属性和内容。
        //Channel channel: RabbitMQ 的通道对象,用于进行消息确认和处理。
        if (skuId !=null){
            //调用方法商品上架
            skuService.upperSku(skuId);
        }
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),
                false);
        //手动确认消息已被处理。basicAck 方法用于告知 RabbitMQ 该消息已经被成功处理,可以将消息从队列中删除。
        // 参数中的 message.getMessageProperties().getDeliveryTag() 是消息的交付标签,用于唯一标识一条消息,false 表示不批量确认,只确认当前消息。
    }

    //商品下架
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = MqConst.QUEUE_GOODS_LOWER, durable = "true"),
            exchange = @Exchange(value = MqConst.EXCHANGE_GOODS_DIRECT),
            key = {MqConst.ROUTING_GOODS_LOWER}
    ))
    public void lowerSku(Long skuId,Message message,Channel channel) throws IOException {
        if (skuId!=null){
            skuService.lowerSku(skuId);
        }
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),
                false);
    }
}

远程调用product服务获取商品详细信息并同步到ES中

//上架
    @Override
    public void upperSku(Long skuId) {
        //1 通过远程调用 ,根据skuid获取相关信息
        SkuInfo skuInfo = productFeignClient.getSkuInfo(skuId);
        if(skuInfo == null) {
            return;
        }
        Category category = productFeignClient.getCategory(skuInfo.getCategoryId());
        //2 获取数据封装SkuEs对象
        SkuEs skuEs = new SkuEs();
        //封装分类
        if(category != null) {
            skuEs.setCategoryId(category.getId());
            skuEs.setCategoryName(category.getName());
        }
        //封装sku信息部分
        skuEs.setId(skuInfo.getId());
        skuEs.setKeyword(skuInfo.getSkuName()+","+skuEs.getCategoryName());
        skuEs.setWareId(skuInfo.getWareId());
        skuEs.setIsNewPerson(skuInfo.getIsNewPerson());
        skuEs.setImgUrl(skuInfo.getImgUrl());
        skuEs.setTitle(skuInfo.getSkuName());
        if(skuInfo.getSkuType() == SkuType.COMMON.getCode()) {//普通商品
            skuEs.setSkuType(0);
            skuEs.setPrice(skuInfo.getPrice().doubleValue());
            skuEs.setStock(skuInfo.getStock());
            skuEs.setSale(skuInfo.getSale());
            skuEs.setPerLimit(skuInfo.getPerLimit());
        }
        //3 调用方法添加ES
        skuRepository.save(skuEs);
    }

    //下架
    @Override
    public void lowerSku(Long skuId) {
        skuRepository.deleteById(skuId);
    }


}

 

总结

首先,在商品上下架时,product服务向MQ发送一个异步消息(携带商品id),将商品信息同步到消息队列中,由search服务接收消息,然后根据商品id用openfeign通过search服务远程调用product服务获取商品详细信息,最终同步上下架数据到ES中。 

为什么要把数据存到ES里面?(ES的优点)

快速搜索和过滤

Elasticsearch 是一个基于 Lucene 的搜索引擎,具有高效的搜索和过滤功能。将商品详细信息存储在 Elasticsearch 中可以通过复杂的查询语法快速地搜索和过滤商品数据,例如根据关键词、属性、价格范围等条件进行搜索,对于电商网站或商品管理系统非常有用。

全文搜索和关键字匹配

Elasticsearch 支持全文搜索和关键字匹配,可以对商品的各个字段进行搜索,并且支持模糊查询、精确匹配、范围查询等功能,帮助用户更快地找到需要的商品。

实时更新和同步

Elasticsearch 支持实时索引更新和同步,可以在商品信息发生变化时立即更新索引,保证搜索结果的实时性。这对于电商平台等需要实时更新商品信息的场景非常重要。

总结

总体来说,将商品详细信息存储到 Elasticsearch 中可以提供快速、灵活、实时的搜索和分析功能,帮助商家和用户更好地管理和查找商品,提升用户体验和平台的竞争力。

为什么要用MQ做商品上下架?

 


异步处理

因为在商品上下架时同步更新数据库,同步ES比较耗时,通过MQ可以将这些操作异步化,即使某些操作耗时较长,也不会影响到用户的实时操作相应。这样可以提高系统的性能和吞吐量。

削峰填谷

其次在高并发或突发流量下,直接将上下架操作同步处理可能会给数据库或其他服务带来压力,通过MQ可以将请求分散到不同的时间段或节点进行处理,从而平滑流量峰值,减轻系统压力。

总结

综合来说,使用消息队列(MQ)结合商品上下架功能可以实现异步处理、削峰填谷、解耦服务、数据同步以及可靠性和重试机制等多种优势,提高系统的性能、稳定性和可维护性,适用于高并发、大规模的电商系统或商品管理系统等场景。

用户微信授权登录怎么实现的?

3f85bb2b3a37477091b989aa60b74854.png

  1. 得到微信返回code临时票据值。
  2. 拿着code+小程序id+小程序密钥请求微信接口服务(使用HttpClient工具请求)。
  3. 请求微信接口服务,返回两个值session_key和openId(openId是登录微信的唯一标识)。
  4. 添加微信用户信息到数据库。(操作user表,判断是否是第一次使用微信授权登录:如何判断?openId)。
  5. 根据userId查询提货点和团长信息。
    1. 提货点        user表        user_delivery表
    2. 团长           leader表
  6. 使用jwt工具根据userid和username生成token字符串。
  7. 获取当前登录用户信息,放到redis里面,设置有效时间。
  8. 把数据封装到map返回。
    public Result loginWx(@PathVariable String code) {
        //1 得到微信返回code临时票据值
        //2 拿着code + 小程序id + 小程序秘钥 请求微信接口服务
         使用HttpClient工具请求
        //小程序id
        String wxOpenAppId = ConstantPropertiesUtil.WX_OPEN_APP_ID;
        //小程序秘钥
        String wxOpenAppSecret = ConstantPropertiesUtil.WX_OPEN_APP_SECRET;
        //get请求
        //拼接请求地址+参数
        /// 地址?name=value&name1=value1
        StringBuffer url = new StringBuffer()
                .append("https://api.weixin.qq.com/sns/jscode2session")
                .append("?appid=%s")
                .append("&secret=%s")
                .append("&js_code=%s")
                .append("&grant_type=authorization_code");
        String tokenUrl = String.format(url.toString(),
                wxOpenAppId,
                wxOpenAppSecret,
                code);
        //HttpClient发送get请求
        String result = null;
        try {
            result = HttpClientUtils.get(tokenUrl);
        } catch (Exception e) {
            throw new SsyxException(ResultCodeEnum.FETCH_ACCESSTOKEN_FAILD);
        }

        //3 请求微信接口服务,返回两个值 session_key 和 openid
         openId是你微信唯一标识
        JSONObject jsonObject = JSONObject.parseObject(result);
        String session_key = jsonObject.getString("session_key");
        String openid = jsonObject.getString("openid");

//        openid = "odo3j4q2KskkbbW-krfE-cAxUnzU1";
        //4 添加微信用户信息到数据库里面
         操作user表
         判断是否是第一次使用微信授权登录:如何判断?openId
        User user = userService.getUserByOpenId(openid);
        if(user == null) {
            user = new User();
            user.setOpenId(openid);
            user.setNickName(openid);
            user.setPhotoUrl("");
            user.setUserType(UserType.USER);
            user.setIsNew(0);
            userService.save(user);
        }

        //5 根据userId查询提货点和团长信息
        提货点  user表  user_delivery表
        团长    leader表
        LeaderAddressVo leaderAddressVo =
                userService.getLeaderAddressByUserId(user.getId());

        //6 使用JWT工具根据userId和userName生成token字符串
        String token = JwtHelper.createToken(user.getId(), user.getNickName());

        //7 获取当前登录用户信息,放到Redis里面,设置有效时间
        UserLoginVo userLoginVo = userService.getUserLoginVo(user.getId());
        redisTemplate.opsForValue()
                .set(RedisConst.USER_LOGIN_KEY_PREFIX+user.getId(),
                        userLoginVo,
                        RedisConst.USERKEY_TIMEOUT,
                        TimeUnit.DAYS);

        //8 需要数据封装到map返回
        Map<String,Object> map = new HashMap<>();
        map.put("user",user);
        map.put("token",token);
        map.put("leaderAddressVo",leaderAddressVo);
        return Result.ok(map);
    }

总结

首先得到微信返回code临时票据,然后拿着code+小程序id+小程序密钥用HttpClient工具请求微信接口服务,此时会返回session_key和openId,其中openId是登录微信的唯一标识,用它可以判断当前登录用户是否是第一次登录,然后根据userid查询用户提货店和对应团长信息,使用jwt根据userid和username生成token字符串,最后获取当前登录用户信息,存到redis里面,封装数据返回。

用登录拦截器+ThteadLocal做了什么事情?

a27bf5f0793c4ffdb656c405088a9128.png

因为在微信授权接口里返回的token都被放在请求头,而拦截器做的事情是在页面加载之前从请求头获取token字符串,从token获取userid,然后根据userid获取登录用户信息,最后存入ThreadLocal跟当前线程绑定。

将用户信息存入ThreadLocal可以在当前线程的执行过程中任何地方都可以方便地获取用户登录信息,而不需要每次都去从Redis或者其他地方重新获取。这样可以减少重复的获取操作,提高了代码的执行效率和性能。但需要注意的是,使用ThreadLocal需要注意内存泄漏和及时清理的问题,确保在线程执行完毕后及时清理ThreadLocal中的数据。

如何使用自定义拦截器?

  1. 创建拦截器类实现HandlerInterceptor
  2. 编写注册拦截器类继承WebMvcConfigurerAdapter
  3. 实现addInterceptors指定拦截器的URL路径
  4. 在拦截器的preHandle、postHandle和afterCompletion方法中编写具体的逻辑。
@Configuration
public class LoginMvcConfigurerAdapter extends WebMvcConfigurationSupport {

    @Resource
    private RedisTemplate redisTemplate;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new UserLoginInterceptor(redisTemplate))
                .addPathPatterns("/api/**")///api/**路径拦截
                .excludePathPatterns("/api/user/weixin/wxLogin/*");//登录路径不拦截
        super.addInterceptors(registry);
    }
}

 

public class UserLoginInterceptor implements HandlerInterceptor {

    private RedisTemplate redisTemplate;
    public UserLoginInterceptor(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        this.getUserLoginVo(request);
        return true;
    }

    private void getUserLoginVo(HttpServletRequest request) {
        //从请求头获取token
        String token = request.getHeader("token");

        //判断token不为空
        if(!StringUtils.isEmpty(token)) {
            //从token获取userId
            Long userId = JwtHelper.getUserId(token);
            //根据userId到Redis获取用户信息
            UserLoginVo userLoginVo = (UserLoginVo)redisTemplate.opsForValue()
                    .get(RedisConst.USER_LOGIN_KEY_PREFIX + userId);
            //获取数据放到ThreadLocal里面
            if(userLoginVo != null) {
                AuthContextHolder.setUserId(userLoginVo.getUserId());
                AuthContextHolder.setWareId(userLoginVo.getWareId());
                AuthContextHolder.setUserLoginVo(userLoginVo);
            }
        }
    }
}

使用CompletableFuture优化了什么功能?怎么实现的? 

由于商品详情功能需要进行三次远程调用,为了提升用户体验以及系统流畅,所以使用CompletableFuture优化商品详情功能。

e2326ff4aa074efbbc26fb9b1c969eeb.png

创建ThreadPoolExecutor线程池配置类 

@Configuration
public class ThreadPoolConfig {

    @Bean
    public ThreadPoolExecutor threadPoolExecutor() {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2,      //核心线程数,即线程池中保持活跃的线程数量,即使它们是空闲的。
                5,                  //最大线程数,即线程池中允许存在的最大线程数量。
                2,                  //线程空闲时间(保持活跃时间),指的是空闲线程等待新任务的最大时间。如果线程在这段时间内没有任务可执行,则会被终止。
                TimeUnit.SECONDS,   //线程空闲时间的时间单位,这里设置为秒。
                new ArrayBlockingQueue<>(3),        //工作队列,用于存放待执行的任务。这里使用的是一个容量为3的ArrayBlockingQueue,即最多可以存放3个任务,超过则会阻塞新任务的提交。
                Executors.defaultThreadFactory(),           //线程工厂,用于创建新线程。 
                new ThreadPoolExecutor.AbortPolicy()        //拒绝策略,指定了当任务无法被执行时的处理方式。AbortPolicy是一种拒绝策略,它会在无法接受新任务时抛出RejectedExecutionException异常。
        );
        return executor;
    }
}

 编写商品详细信息接口

public class ItemApiController {

    @Autowired
    private ItemService itemService;

    //获取sku详细信息
    @GetMapping("item/{id}")
    public Result index(@PathVariable Long id) {
        Long userId = AuthContextHolder.getUserId();
        Map<String,Object> map = itemService.item(id,userId);
        return Result.ok(map);
    }
}

 在CompletableFuture.supplyAsync()或CompletableFuture.runAsync()远程调用对应数据,并用allOf()将三个任务组合起来,并调用join()方法等待所有任务完成。

@Service
public class ItemServiceImpl implements ItemService {

    @Autowired
    private ProductFeignClient productFeignClient;

    @Autowired
    private ActivityFeignClient activityFeignClient;

    @Autowired
    private SkuFeignClient skuFeignClient;
    @Resource
    private ThreadPoolExecutor threadPoolExecutor;
//    //详情
    @Override
    public Map<String, Object> item(Long skuId, Long userId) {
        Map<String, Object> result = new HashMap<>();

        //skuId查询
        CompletableFuture<SkuInfoVo> skuInfocompletableFuture =
                CompletableFuture.supplyAsync(() -> {               //supplyAsync有返回值   //runAsync没有返回值
                    //远程调用获取sku对应数据
                    SkuInfoVo skuInfoVo = productFeignClient.getSkuInfoVo(skuId);
                    result.put("skuInfoVo", skuInfoVo);
                    return skuInfoVo;
                }, threadPoolExecutor);

        //sku对应优惠卷信息
        CompletableFuture<Void> activityCompletableFuture = CompletableFuture.runAsync(() -> {
            //远程调用获取优惠卷
            Map<String, Object> activityMap =
                    activityFeignClient.findActivityAndCoupon(skuId, userId);
            result.putAll(activityMap);
        }, threadPoolExecutor);

        //更新商品热度
        CompletableFuture<Void> hotCompletableFuture = CompletableFuture.runAsync(() -> {
            //远程调用更新热度
            skuFeignClient.incrHotScore(skuId);
        }, threadPoolExecutor);

        //任务组合
        CompletableFuture.allOf(
                skuInfocompletableFuture,
                activityCompletableFuture,
                hotCompletableFuture
        ).join();
        return result;

    }
}

 总结

首先配置ThreadPoolExecutor线程池,设定核心线程数,最大线程数等相关参数,在商品详情接口中用CompletableFuture的supplyAsync异步执行任务远程调用商品信息,优惠券信息,更新商品热度。然后用CompletableFuture的allOf方法将三个线程任务组合起来并用join方法等待所有任务完成。这样可以确保在所有异步任务完成后再继续执行下面的代码,最后返回结果。

更新商品热度怎么实现的? 

c7752122cdc74fb398351f3b44ae8f56.png

  //更新商品热度
    @Override
    public void incrHotScore(Long skuId) {
        String key = "hotScore";
        //redis保存数据,每次+1
        Double hotScore = redisTemplate.opsForZSet().incrementScore(key, "skuId:" + skuId, 1);
        //规则
        if(hotScore%10==0) {
            //更新es
            Optional<SkuEs> optional = skuRepository.findById(skuId);
            SkuEs skuEs = optional.get();
            skuEs.setHotScore(Math.round(hotScore));
            skuRepository.save(skuEs);
        }
    }

添加商品到购物车怎么实现? 

 

 编写接口参数登录用户id,商品id,商品数量

 //添加商品到购物车
    //添加内容:当前登录用户id,skuId,商品数量
    @GetMapping("addToCart/{skuId}/{skuNum}")
    public Result addToCart(@PathVariable("skuId") Long skuId,
                            @PathVariable("skuNum") Integer skuNum) {
        //获取当前登录用户Id
        Long userId = AuthContextHolder.getUserId();
        cartInfoService.addToCart(userId,skuId,skuNum);
        return Result.ok(null);
    }

 接口实现

 //返回购物车在redis的key
    private String getCartKey(Long userId) {
        // user:userId:cart
        return RedisConst.USER_KEY_PREFIX + userId + RedisConst.USER_CART_KEY_SUFFIX;
    }
    //添加商品到购物车
    @Override
    public void addToCart(Long userId, Long skuId, Integer skuNum) {
        //1 因为购物车数据存储到redis里面,
        // 从redis里面根据key获取数据,这个key包含userId
        String cartKey = this.getCartKey(userId);
        BoundHashOperations<String,String,CartInfo> hashOperations =
                redisTemplate.boundHashOps(cartKey);

        //2 根据第一步查询出来的结果,得到是skuId + skuNum关系
        CartInfo cartInfo = null;
        //目的:判断是否是第一次添加这个商品到购物车
        // 进行判断,判断结果里面,是否有skuId
        if(hashOperations.hasKey(skuId.toString())) {
            //3 如果结果里面包含skuId,不是第一次添加
            //3.1 根据skuId,获取对应数量,更新数量
            cartInfo = hashOperations.get(skuId.toString());
            //把购物车存在商品之前数量获取数量,在进行数量更新操作
            Integer currentSkuNum = cartInfo.getSkuNum() + skuNum;
            if(currentSkuNum < 1) {
                return;
            }

            //更新cartInfo对象
            cartInfo.setSkuNum(currentSkuNum);
            cartInfo.setCurrentBuyNum(currentSkuNum);

            //判断商品数量不能大于限购数量
            Integer perLimit = cartInfo.getPerLimit();
            if(currentSkuNum > perLimit) {
                throw new SsyxException(ResultCodeEnum.SKU_LIMIT_ERROR);
            }

            //更新其他值
            cartInfo.setIsChecked(1);
            cartInfo.setUpdateTime(new Date());
        } else {
            //4 如果结果里面没有skuId,就是第一次添加
            //4.1 直接添加
            skuNum = 1;

            //远程调用根据skuId获取skuInfo
            SkuInfo skuInfo = productFeignClient.getSkuInfo(skuId);
            if(skuInfo == null) {
                throw new SsyxException(ResultCodeEnum.DATA_ERROR);
            }

            //封装cartInfo对象
            cartInfo = new CartInfo();
            cartInfo.setSkuId(skuId);
            cartInfo.setCategoryId(skuInfo.getCategoryId());
            cartInfo.setSkuType(skuInfo.getSkuType());
            cartInfo.setIsNewPerson(skuInfo.getIsNewPerson());
            cartInfo.setUserId(userId);
            cartInfo.setCartPrice(skuInfo.getPrice());
            cartInfo.setSkuNum(skuNum);
            cartInfo.setCurrentBuyNum(skuNum);
            cartInfo.setSkuType(SkuType.COMMON.getCode());
            cartInfo.setPerLimit(skuInfo.getPerLimit());
            cartInfo.setImgUrl(skuInfo.getImgUrl());
            cartInfo.setSkuName(skuInfo.getSkuName());
            cartInfo.setWareId(skuInfo.getWareId());
            cartInfo.setIsChecked(1);
            cartInfo.setStatus(1);
            cartInfo.setCreateTime(new Date());
            cartInfo.setUpdateTime(new Date());
        }

        //5 更新redis缓存
        hashOperations.put(skuId.toString(),cartInfo);

        //6 设置有效时间
        this.setCartKeyExpire(cartKey);
    }

总结 

首先生成根据用户ID生成购物车在 Redis 中的键值,以便唯一每个用户的购物车。返回的键值的格式为 user:userId:cart,如 user:123:cart。然后使用boundHashOps 方法根据key获取购物车数据的操作对象,通过 hasKey(skuId.toString()) 方法判断购物车中是否已存在某个商品,存在则不是第一次添加,更新对应cartInfo对象,不存在则是第一次添加,封装cartInfo对象。最后用更新redis缓存并设置有效时间。

怎么用Redission分布式锁完成生成订单功能的?

生成订单实现过程 

 //生成订单
    @Override
    public Long submitOrder(OrderSubmitVo orderParamVo) {
        //第一步 设置给哪个用户生成订单  设置orderParamVo的userId
        Long userId = AuthContextHolder.getUserId();
        orderParamVo.setUserId(userId);

        //第二步 订单不能重复提交,重复提交验证
        // 通过redis + lua脚本进行判断
         lua脚本保证原子性操作
        //1 获取传递过来的订单 orderNo
        String orderNo = orderParamVo.getOrderNo();
        if(StringUtils.isEmpty(orderNo)) {
            throw new SsyxException(ResultCodeEnum.ILLEGAL_REQUEST);
        }

        //2 拿着orderNo 到 redis进行查询,
        String script = "if(redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('del', KEYS[1]) else return 0 end";
        //3 如果redis有相同orderNo,表示正常提交订单,把redis的orderNo删除
        Boolean flag = (Boolean)redisTemplate
                        .execute(new DefaultRedisScript(script, Boolean.class),
                                    Arrays.asList(RedisConst.ORDER_REPEAT + orderNo), orderNo);
        //4 如果redis没有相同orderNo,表示重复提交了,不能再往后进行
        if(!flag) {
            throw new SsyxException(ResultCodeEnum.REPEAT_SUBMIT);
        }

        //第三步 验证库存 并且 锁定库存
        // 比如仓库有10个西红柿,我想买2个西红柿
        // ** 验证库存,查询仓库里面是是否有充足西红柿
        // ** 库存充足,库存锁定 2锁定(目前没有真正减库存)
        //1、远程调用service-cart模块,获取当前用户购物车商品(选中的购物项)
        List<CartInfo> cartInfoList =
                        cartFeignClient.getCartCheckedList(userId);

        //2、购物车有很多商品,商品不同类型,重点处理普通类型商品
        List<CartInfo> commonSkuList = cartInfoList.stream()
                .filter(cartInfo -> cartInfo.getSkuType() == SkuType.COMMON.getCode())
                .collect(Collectors.toList());

        //3、把获取购物车里面普通类型商品list集合,
        // List<CartInfo>转换List<SkuStockLockVo>
        if(!CollectionUtils.isEmpty(commonSkuList)) {
            List<SkuStockLockVo> commonStockLockVoList = commonSkuList.stream().map(item -> {
                SkuStockLockVo skuStockLockVo = new SkuStockLockVo();
                skuStockLockVo.setSkuId(item.getSkuId());
                skuStockLockVo.setSkuNum(item.getSkuNum());
                return skuStockLockVo;
            }).collect(Collectors.toList());

            //4、远程调用service-product模块实现锁定商品
             验证库存并锁定库存,保证具备原子性
            Boolean isLockSuccess =
                    productFeignClient.checkAndLock(commonStockLockVoList, orderNo);
            if(!isLockSuccess) {//库存锁定失败
                throw new SsyxException(ResultCodeEnum.ORDER_STOCK_FALL);
            }
        }

        //第四步 下单过程
        //1 向两张表添加数据
        // order_info 和 order_item
        Long orderId = this.saveOrder(orderParamVo,cartInfoList);

        //下单完成,删除购物车记录
        //发送mq消息
        rabbitService.sendMessage(MqConst.EXCHANGE_ORDER_DIRECT,
                MqConst.ROUTING_DELETE_CART,orderParamVo.getUserId());

        //第五步 返回订单id
        return orderId;
    }

 验证和锁定库存方法

 

//验证和锁定库存
    //对传入的商品库存信息列表进行遍历,逐个验证库存并锁定库存。
    //如果有任何一个商品锁定失败,则将已锁定成功的商品进行解锁,并返回失败状态;否则,将成功锁定的商品信息存储到 Redis 中,并返回成功状态。
    @Override
    public Boolean checkAndLock(List<SkuStockLockVo> skuStockLockVoList, String orderNo) {
        //1 判断skuStockLockVoList集合是否为空
        if(CollectionUtils.isEmpty(skuStockLockVoList)) {
            throw new SsyxException(ResultCodeEnum.DATA_ERROR);
        }

        //2 遍历skuStockLockVoList得到每个商品,验证库存并锁定库存,具备原子性
        skuStockLockVoList.stream().forEach(skuStockLockVo -> {
            this.checkLock(skuStockLockVo);
        });

        //3 只要有一个商品锁定失败,所有锁定成功的商品都解锁
        boolean flag = skuStockLockVoList.stream()
                .anyMatch(skuStockLockVo -> !skuStockLockVo.getIsLock());
        if(flag) {
            //所有锁定成功的商品都解锁
            skuStockLockVoList.stream().filter(SkuStockLockVo::getIsLock)
                    .forEach(skuStockLockVo -> {
                        baseMapper.unlockStock(skuStockLockVo.getSkuId(),
                                skuStockLockVo.getSkuNum());
                    });
            //返回失败的状态
            return false;
        }

        //4 如果所有商品都锁定成功了,redis缓存相关数据,为了方便后面解锁和减库存
        redisTemplate.opsForValue()
                .set(RedisConst.SROCK_INFO+orderNo,skuStockLockVoList);
        return true;
    }


    //针对单个商品进行库存验证并锁定库存。
    //使用 Redisson 实现公平锁,确保多线程环境下操作的原子性。
    private void checkLock(SkuStockLockVo skuStockLockVo) {
        //获取锁
        //公平锁
        RLock rLock =
                this.redissonClient.getFairLock(RedisConst.SKUKEY_PREFIX + skuStockLockVo.getSkuId());
        //加锁
        rLock.lock();

        try {
            //验证库存
            SkuInfo skuInfo =
                    baseMapper.checkStock(skuStockLockVo.getSkuId(),skuStockLockVo.getSkuNum());
            //判断没有满足条件商品,设置isLock值false,返回
            //如果库存不足,将 isLock 设置为 false,表示该商品锁定失败。
            //如果库存充足,执行锁定库存的操作(更新数据库等),并将 isLock 设置为 true,表示该商品锁定成功。
            if(skuInfo == null) {
                skuStockLockVo.setIsLock(false);
                return;
            }
            //有满足条件商品
            //锁定库存:update
            Integer rows =
                    baseMapper.lockStock(skuStockLockVo.getSkuId(),skuStockLockVo.getSkuNum());
            if(rows == 1) {
                skuStockLockVo.setIsLock(true);
            }
        } finally {
            //解锁
            rLock.unlock();
        }
    }

总结 

生成订单方法

 首先设置给哪个用户生成订单,从ThreadLocal里获取当前用户id,然后通过redis和Lua脚本验证订单是否以及提交过,防止重复提交订单,通过获取传递过来的orderNo到redis进行查询,如果redis有相同的orderNo,表示正常提交订单,删除redis中的订单号,如果没有相同orderNo,表示重复提交,抛出异常。通过远程调用cart服务获取用户选中的购物项,过滤出普通类型商品并转换为库存锁定对象,远程调用product服务进行库存验证并锁定,最后向订单信息表和订单项表添加数据,生成订单。下单完成后删除购物车中相应记录,发送MQ消息通知其他模块。

验证库存方法

  1. 判断传入的商品库存信息列表是否为空。
  2. 遍历商品库存信息列表,调用私有方法 checkLock 验证库存并锁定库存。
  3. 如果有任何一个商品锁定失败,则将已锁定成功的商品进行解锁,并返回失败状态;否则,将成功锁定的商品信息存储到 Redis 中,并返回成功状态。

锁定库存方法

  1. 获取商品对应的公平锁对象 RLock
  2. 加锁:使用公平锁 RLock 对商品进行加锁操作,确保在多线程环境下的线程安全性。
  3. 在加锁的情况下,验证商品的库存情况。
    • 如果库存不足,将 isLock 设置为 false,表示该商品锁定失败。
    • 如果库存充足,执行锁定库存的操作(更新数据库等),并将 isLock 设置为 true,表示该商品锁定成功。
  4. 最后,在 finally 块中进行解锁操作,确保不管验证过程中是否发生异常,都能正确释放锁资源。

 

 

 

 

  • 18
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值