• 使用RabbitMQ最终一致性库存解锁


    一、基本介绍

    ①延时队列(实现定时任务)

    场景:比如未付款订单,超过一定时间后,系统自动取消订单并释放占有物品。

    常用解决方案: spring的 schedule定时任务轮询数据库:
    缺点:消耗系统内存、增加了数据库的压力、存在较大的时间误差
    解决: rabbitmqExchange的消息TTL和死信结合

    ②消息的TL(Time To Live)消息的TTL就是消息的存活时间。

     RabbitMQ可以对队列和消息分别设置TTL
    - 对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。
    - 如果队列设置了,消息也设置了,那么会取小的。所以一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置)。这里单讲单个消息的TTL,因为它才是实现延迟任务的关键。可以通过设置消息的 expiration-message-字段或者x--ttl属性来设置时间,两者是一样的效果。

    ③ Dead Letter Exchanges (DLX)

    一个消息在满足如下条件下,会进死信路由,记住这里是路由而不是队列,一个路由可以对应很多队列。(什么是死信)
    - 一个消息被Consumer拒收了,并且 reject方法的参数里是。也就是说不会被再次放在队列里,被其他消费者使用。(basic.reject/basic.nack) requeue=false上面的消息的TTL到了,消息过期了。-
    - 一队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上
     Dead Letter Exchangeexch其实就是一种普通的,和创建其他exchange没有两样。只是在某一个设置 Dead Letter Exchange的队列中有消息过期了,会自动触发消息的转发,发送到 Dead Letter Exchange中去。
    ·我们既可以控制消息在一段时间后变成死信,又可以控制变成死信的消息被路由到某一个指定的交换机,结合二者,其实就可以实现一个延时队列

    二、推荐:给队列设置延时时间

    ①:因为RabbitMQ采用惰性检查机制

    RabbitMq采用惰性检查机制,也就是懒检查机制:比如消息队列中存放了多条消息,第一条是5分钟过期,第二条是1分钟过期,第三条是1秒钟过期,按照正常的过期逻辑,应该是1秒过期的先排出这个队列,进入死信队列中,但是实际RabbitMQ是先拿第一条消息,也就是5分钟过期的,一看5分钟还没到过期时间,然后等待5分钟会将第一条消息拿出来,放入死信队列,这里就会出现问题,第二条设置1分钟的和第三条设置1秒钟的消息必须要等待第一条5分钟过期后才能过期,等待第一条消息过期5分钟了,拿第二条、三条的时候都不需要判断就已经过期了,直接就放入死信队列中,所以第二条、三条需要等待第一条消息过了5分钟才能过期,这样的延时根本就没产生对应的效果。

    ②:理论结构图

    ③:项目结构图

     ④:代码实现

    4.1:基础设置

    4.1.1:创建信道、队列、路由(结构图)

    4.1.2:MyRabbit配置

    @Configuration
    public class MyRabbitConfig {
        /**
         * 使用JSON序列化机制,进行消息转移
         */
        @Bean
        public MessageConverter messageConverter() {
            return new Jackson2JsonMessageConverter();
        }
    
        @Bean
        public Exchange stockEventExchange() {
            TopicExchange topicExchange = new TopicExchange("stock-event-exchange", true, false);
            return topicExchange;
        }
    
        @Bean
        public Queue stockReleaseStockQueue() {
            return new Queue("stock.release.stock.queue", true, false, false);
        }
    
    @Bean
    public Queue stockDelayQueue() { HashMap<String, Object> args = new HashMap<>(); args.put("x-dead-letter-exchange", "stock-event-exchange"); args.put("x-dead-letter-routing-key", "stock.release"); args.put("x-message-ttl", 120000); //延时2min return new Queue("stock.delay.queue", true, false, false,args); } @Bean public Binding stockReleaseBinding() { return new Binding("stock.release.stock.queue", Binding.DestinationType.QUEUE, "stock-event-exchange", "stock.release.#", null); } @Bean public Binding stockLockedBinding() { return new Binding("stock.delay.queue", Binding.DestinationType.QUEUE, "stock-event-exchange", "stock.locked", null); } }

     4.1.3:设置监听器

    @Service
    @RabbitListener(queues = "stock.release.stock.queue")
    public class StockRelaeaseListener {
        @Autowired
        WareSkuService wareskuService;
    
        @RabbitHandler
        public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
            System.out.println("收到解锁信息");
            try {
                wareskuService.unlockStock(to);
                channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            } catch (Exception e) {
                channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
            }
        }
    
        @RabbitHandler
        public void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {
    
            System.out.println("订单关闭准备解锁库存");
            try {
                wareskuService.unlockStock(orderTo);
                channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            } catch (Exception e) {
                channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
            }
    
        }
    }

    4.2、wareskuService类:

    4.2.1:orderLockStock方法:

        /**
         * 为某个订单锁定库存
         *
         * @param vo
         * @return
         * @Transactional(rollbackFor = NoStockException.class)运行出现异常时回滚
         */
        @Transactional
        @Override
        public Boolean orderLockStock(WareSkuLockVo vo) {
            /**
             * 保存库存工作单的详情,为了追溯哪个仓库锁了多少
             */
            WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
            taskEntity.setOrderSn(vo.getOrderSn());
            orderTaskService.save(taskEntity);
    
            //1、按照下单的收获地址,找到就近仓库进行锁定库存
            List<OrderItemVo> locks = vo.getLocks();
    
            List<SkuWareHasStock> collect = locks.stream().map(item -> {
                SkuWareHasStock stock = new SkuWareHasStock();
                Long skuId = item.getSkuId();
                stock.setSkuId(skuId);
                stock.setNum(item.getCount());
                //查询这个商品在哪里有库存
                List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId);
                stock.setWareId(wareIds);
                return stock;
            }).collect(Collectors.toList());
    
            for (SkuWareHasStock hasStock : collect) {
                Boolean skuStocked = false;
                Long skuId = hasStock.getSkuId();
                List<Long> wareIds = hasStock.getWareId();
                if (wareIds == null || wareIds.size() == 0) {
                    //没有库存抛出异常
                    throw new NoStockException(skuId);
                }
                //如果每一个商品都锁成功,将当前商品锁定了几件发送给MQ
                //如果锁定失败,前面保存的工作单信息就回滚了。
                for (Long wareId : wareIds) {
                    //成功就返回1,否则就是0
                    Long count = wareSkuDao.lockSkuStock(skuId, wareId, hasStock.getNum());
                    if (count == 1) {
                        //TODO:表明锁住了,发消息告诉MQ库存锁定成功
                        //在数据表wms_ware_order_task_detail中存入库存单(*仓库/*商品/*数量/被锁*件)做记号
                        WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity(null, skuId, "", hasStock.getNum(), taskEntity.getId(), wareId, 1);
                        orderTaskDetailService.save(entity);
                        StockLockedTo lockedTo = new StockLockedTo();
                        lockedTo.setId(taskEntity.getId());
                        StockDetailTo stockDetailTo = new StockDetailTo();
                        //拷贝属性和数值
                        BeanUtils.copyProperties(entity, stockDetailTo);
                        //防止wms_ware_order_task_detail表内数据因为回滚丢失,所以new一个StockLockedTo类记录失败提交的数据
                        lockedTo.setDetail(stockDetailTo);
                        //将库存工作单的详情放入exchange中
                        rabbitTemplate.convertAndSend("stock-event-exchange", "stock.locked", lockedTo);
                        skuStocked = true;
                        break;
                    } else {
                        //锁失败了,重试下一个仓库
                    }
                }
                if (skuStocked == false) {
                    //当前商品所有仓库都没锁住库存数量
                    throw new NoStockException(skuId);
                }
            }
            //肯定全部都是锁定
            return true;
        }

    4.2.1、unlockStock方法:

        @Override
        public void unlockStock(StockLockedTo to) {
            StockDetailTo detail = to.getDetail();
            Long detailId = detail.getId();
            /**
             * 1、查询数据库wms_ware_order_task_detail表关于这个订单的锁定库存信息
             * ①表里有关于锁库存的信息,队列设置延时时间,检查订单的状态,确认是否需要进行解锁
             * 1.1:解锁前查看订单情况:则需要解锁库存
             * 1.1.1查看订单状态,查看订单状态,若订单已取消则必须解锁库存
             * 1.1.2查看订单状态,订单未取消则不能解锁库存
             *
             * 1.2:如果没有订单情况:则必须解锁库存
             *
             * ②没有则代表整个库存锁定失败,事务回滚了,这种情况无需解锁
             *
             * 只要解锁库存失败,利用手动模式
             */
            WareOrderTaskDetailEntity byId = orderTaskDetailService.getById(detailId);
            if (byId != null) {
                //解锁
                Long id = to.getId();
                WareOrderTaskEntity taskEntity = orderTaskService.getById(id);
                String orderSn = taskEntity.getOrderSn();//根据订单号查询订单状态
                //查找订单是否创建成功,此处远程调用会因为拦截器需要先登录,因此按4.3进行修改
                R r = orderFeignService.getOrderStatus(orderSn);
                if (r.getCode() == 0) {
                    //订单数据返回成功
                    OrderVo data = r.getData(new TypeReference<OrderVo>() {
                    });
                    //只有订单状态是取消状态/或者订单不存在才可以解锁.4为状态码代表订单是取消状态
                    System.out.println("Data1:" + data);
                    if (data == null || data.getStatus() == 4) {
                        //当前库存单详情状态1已锁定但是未解锁才可以解锁
                        unLockStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum(), detailId);
                     /*   if (byId.getLockStatus() == 1) {
                            System.out.println("Data2:" + data);
    
                        }*/
                    }
                } else {
                    //消息拒绝以后重新放入队列里,让其他人继续消费解锁
                    throw new RuntimeException("远程服务失败");
                }
            }
        }

     4.2.2:unLockStock方法:

        //库存解锁方法
        private void unLockStock(Long skuId, Long wareId, Integer num, Long taskDetailId) {
            wareSkuDao.unlockStock(skuId, wareId, num);
        }
    
    //wareSkuDao.xml中更新代码:
    
    /**<update id="unlockStock">
        UPDATE wms_ware_sku SET stock_locked = stock_locked- #{num} WHERE sku_id= #{skuId} AND ware_id=#{wareId}
    </update>**/

     4.2.3:unlockStock(OrderTo orderTo)方法:

     /*
        *防止订单服务卡顿,导致订单消息一直更改不了,库存优先到期,查订单状态新建状态,什么都做不了就走了
        *导致卡顿的订单,永远不能解锁
        */
        @Transactional
        @Override
        public void unlockStock(OrderTo orderTo) {
            String orderSn = orderTo.getOrderSn();
            //进行到这一步再查一下最新的状态
           WareOrderTaskEntity task = orderTaskService.getOrderTaskByOrderSn(orderSn);
           //获取was_ware_order_task中的id,从而以其获取was_ware_order_task_detail状态为1(未解锁)的库存
            Long id = task.getId();
            List<WareOrderTaskDetailEntity> entities = orderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>().eq("task_id", id).eq("lock_status", 1));
            //Long skuId, Long wareId, Integer num, Long taskDetailId
            for (WareOrderTaskDetailEntity entity:entities){
                unLockStock(entity.getSkuId(),entity.getWareId(),entity.getSkuNum(),entity.getId());
            }
        }

    4.3:拦截器修改

    @Component
    public class LoginUserInterceptor implements HandlerInterceptor {
        public static ThreadLocal<MemberResVo> loginUser = new ThreadLocal<>();
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
            //此地址下不进行拦截
            String uri = request.getRequestURI();
            boolean match = new AntPathMatcher().match("/order/order/status/**", uri);
            boolean match1 = new AntPathMatcher().match("/payed/notify", uri);
            if (match || match1){
                return true;
            }
    //获取登录用户的键 MemberResVo attribute = (MemberResVo) request.getSession().getAttribute(AuthServerConstant.LONG_USER); if (attribute!=null){ loginUser.set(attribute); return true; }else { request.getSession().setAttribute("msg","请先进行登录!"); response.sendRedirect("http://auth.gulimall.com/login.html"); return false; } } }
  • 相关阅读:
    杨辉三角形
    open live writer
    已加载"C:WindowsSysWOW64msvcp120d.dll".无法查找或打开 PDB 文件.
    4.标准信号与槽
    python的单元测试unittest(一)
    python面向对象--类与对象
    python的文件操作与目录操作os模块
    Jenkins的安装与配置
    selenium切换到iframe
    selenium对富文本的操作
  • 原文地址:https://www.cnblogs.com/linchenguang/p/13810779.html
Copyright © 2020-2023  润新知