使用延迟队列解决分布式事务问题——以订单未支付过期,解锁库存为例

news2025/1/9 17:00:50

目录

一、前言

二、库存

三、订单


一、前言

上一篇使用springcloud-seata解决分布式事务问题-2PC模式我们说到了使用springcloud-seata解决分布式的缺点——不适用于高并发场景

因此我们使用延迟队列来解决分布式事务问题,即使用柔性事务-可靠消息-最终一致性方案(异步确保型)

以下是下订单的代码

//    @GlobalTransactional  不使用seata
    @Transactional
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {

        submitVoThreadLocal.set(vo);
        MemberResVo memberResVo = LoginUserInterceptor.loginUser.get();

        SubmitOrderResponseVo response = new SubmitOrderResponseVo();
        response.setCode(0);
        String redisToken = redisTemplate.opsForValue().get(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResVo.getId());
        String orderToken = vo.getOrderToken();
        // 成功返回1  失败返回0
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        // 保证原子性
        Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResVo.getId()), orderToken);
        if(result == 0L) {
            // 验证失败
            response.setCode(1);
            return response;
        } else {
            // 下单,创建订单,校验令牌,检验价格,锁库存
            // TODO 1、创建订单,订单项等信息
            OrderCreateTo order = createOrder();
            // TODO 2、验价
            BigDecimal payAmount = order.getOrder().getPayAmount();
            if(Math.abs(payAmount.subtract(vo.getPayPrice()).doubleValue()) < 0.01) {
                // 金额对比成功后保存订单
                // TODO 3、保存订单
                saveOrder(order);

                WareSkuLockVo wareSkuLockVo = new WareSkuLockVo();
                wareSkuLockVo.setOrderSn(order.getOrder().getOrderSn());
                List<OrderItemVo> collect = order.getOrderItems().stream().map(item -> {
                    OrderItemVo orderItemVo = new OrderItemVo();
                    orderItemVo.setCount(item.getSkuQuantity());
                    orderItemVo.setSkuId(item.getSkuId());
                    orderItemVo.setTitle(item.getSkuName());
                    return orderItemVo;
                }).collect(Collectors.toList());
                wareSkuLockVo.setLocks(collect);
                // TODO  4、锁库存
                // 出异常后,因为远程锁库存成功,但是忘了原因超时了,订单回滚,库存不回滚

                // 为了保证高并发,库存服务自己要回滚,可以发消息给库存服务
                // 库存服务本身也可以使用自动解锁模式 即使用消息队列
                R r = wareFeignService.orderLockStock(wareSkuLockVo);
                if(r.getCode() == 0) {
                    // 锁成功
                    response.setOrder(order.getOrder());

                    // TODO 5 出异常
//                    int i = 10/0;
                    return response;
                } else {
                    // 锁定失败
                    // 抛异常才能使事务回滚
                    response.setCode(3);
                    throw new NoStockException((String)r.get("msg"));

//                    return response;
                }
            } else {
                response.setCode(2); // 金额对比失败
                return response;
            }

        }

    }

二、库存

 库存服务设计图,首先创建stock-event-exchange交换机,还有stock.release.stock.queuestock.delay.queue(死信队列)两个队列,交换机和stock.release.stock.queue之间通过路由stock.release.#绑定,交换机和stock.delay.queue(死信队列)通过路由stock.locked绑定

流程解释:当库存锁定成功后,发消息给交换机,交换机通过路由发送到死信队列中,通过死信队列的延迟效果,在时间到期后再路由到交换机,交换机再放入普通队列(绿色),此时只要有方法监听这个队列,就可以拿到消息进行消费

向rabbitmq注册队列、交换机和绑定的代码如下:

@Configuration
public class MyRabbitConfig {


    @Autowired
    RabbitTemplate template;

    /*
     * 使用JSON序列化机制,进行消息转换
     */
    @Bean
    public MessageConverter messageConverter() {


        return new Jackson2JsonMessageConverter();
    }

//    @RabbitListener(queues = "stock.release.stock.queue")
//    public void handle(Message message) {
//
//    }

    @Bean
    public Exchange stockEventExchange() {
        // String name, boolean durable, boolean autoDelete, Map<String, Object> arguments
        return new TopicExchange("stock-even-exchange", true, false, null);
    }

    @Bean
    public Queue stockReleaseStockQueue() {
        //String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
        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-even-exchange");
        arguments.put("x-dead-letter-routing-key", "stock.release");
        arguments.put("x-message-ttl", 120000); // 消息过期时间 1分钟
        return new Queue("stock.delay.queue", true, false, false, arguments);
    }

    @Bean
    public Binding stockLockBinding() {

        return new Binding("stock.release.stock.queue", Binding.DestinationType.QUEUE,
                "stock-even-exchange",
                "stock.release.#",null);
    }

    @Bean
    public Binding stockReleaseBinding() {

        return new Binding("stock.delay.queue", Binding.DestinationType.QUEUE,
                "stock-even-exchange",
                "stock.locked",null);
    }


}

库存锁定方法,若锁定成功会发送消息到死信队列

    @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());

        // 2、锁定库存

        // 1、如果每一个商品都锁定成功,将当前商品锁定了几件的工作单记录发给MQ
        // 2、锁定失败,前面保存的工作单信息就回滚了。即使要解锁记录,由于去数据库查不到id,所以就不用解锁了

        for (SkuWareHasStock hasStock : collect) {
            Boolean skuStocked = false;
            Long skuId = hasStock.getSkuId();
            List<Long> wareId = hasStock.getWareId();
            if(wareId == null || wareId.size() == 0) {
                // 没有任何仓库有这个商品的库存
                throw new NoStockException(skuId);
            }
            for(Long ware : wareId) {
                // 返回受影响的行数 成功返回1 失败返回0
                Long count = wareSkuDao.lockSkuStock(skuId, ware, hasStock.getNum());
                if(count == 1) {
                    //成功
                    skuStocked = true;
                    // TODO 告诉MQ库存锁定成功
                    WareOrderTaskDetailEntity orderTaskDetailEntity = new WareOrderTaskDetailEntity(null, skuId, "", hasStock.getNum(), taskEntity.getId(), ware, 1);
                    orderTaskDetailService.save(orderTaskDetailEntity);
                    // 通知rabbitmq锁定成功
                    StockLockedTo stockLockedTo = new StockLockedTo();
                    StockDetailTo stockDetailTo = new StockDetailTo();
                    BeanUtils.copyProperties(orderTaskDetailEntity, stockDetailTo);
                    // 只发id不行,防止回滚以后详情表也被回滚了,而库存又被扣减了,此时就无法解锁了
                    stockLockedTo.setDetail(stockDetailTo);
                    stockLockedTo.setId(taskEntity.getId());
                    rabbitTemplate.convertAndSend("stock-even-exchange", "stock.locked", stockLockedTo);
                    break;
                } else {
                    //当前仓库锁失败,重试下一个仓库
                }
            }
            if(skuStocked == false) {
                throw new NoStockException(skuId);
            }
        }
        return true;
    }

然后是监听普通队列的方法

@Slf4j
@RabbitListener(queues = "stock.release.stock.queue")
@Service
public class StockReleaseListener {

    @Autowired
    private WareSkuService wareSkuService;

    /**
     * 1、库存自动解锁
     *  下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
     *
     *  2、订单失败
     *      库存锁定失败
     *
     *   只要解锁库存的消息失败,一定要告诉服务解锁失败
     *
     *   该方法是处理库存自己发给自己的
     */
    @RabbitHandler
    public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
        log.info("******收到解锁库存的信息******");
        try {

            //当前消息是否被第二次及以后(重新)派发过来了
            // Boolean redelivered = message.getMessageProperties().getRedelivered();

            //解锁库存
            wareSkuService.unlockStock(to);
            // 手动删除消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            // 解锁失败 将消息重新放回队列,让别人消费
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }

}

wareSkuService的unlockStock方法如下,只有订单为取消状态或者订单不存在才解锁库存

    /*
     * 解锁的方法,如果报错就抛异常让StockReleaseListener去抓
     */
    @Override
    public void unlockStock(StockLockedTo to) {
        //库存工作单的id
        StockDetailTo detail = to.getDetail();
        Long detailId = detail.getId();

        /**
         * 解锁
         * 1、查询数据库关于这个订单锁定库存信息
         *   有:证明库存锁定成功了
         *      解锁:订单状况
         *          1、没有这个订单,必须解锁库存
         *          2、有这个订单,不一定解锁库存
         *              订单状态:已取消:解锁库存
         *                      已支付:不能解锁库存
         */
        WareOrderTaskDetailEntity taskDetailInfo = orderTaskDetailService.getById(detailId);
        if (taskDetailInfo != null) {
            //查出wms_ware_order_task工作单的信息
            Long id = to.getId();
            WareOrderTaskEntity orderTaskInfo = orderTaskService.getById(id);
            //获取订单号查询订单状态
            String orderSn = orderTaskInfo.getOrderSn();
            //远程查询订单信息
            R orderData = orderFeignService.getOrderStatus(orderSn);
            if (orderData.getCode() == 0) {
                //订单数据返回成功
                OrderVo orderInfo = orderData.getData("data", new TypeReference<OrderVo>() {});

                //判断订单状态是否已取消或者支付或者订单不存在
                if (orderInfo == null || orderInfo.getStatus() == 4) {
                    //订单已被取消,才能解锁库存
                    if (taskDetailInfo.getLockStatus() == 1) {
                        //当前库存工作单详情状态1,已锁定,但是未解锁才可以解锁
                        unLockStock(detail.getSkuId(),detail.getWareId(),detail.getSkuNum(),detailId);
                    }
                }
            } else {
                //消息拒绝以后重新放在队列里面,让别人继续消费解锁
                //远程调用服务失败
                throw new RuntimeException("远程调用服务失败");
            }
        } else {
            //无需解锁
        }
    }

unLockStock方法如下:

    public void unLockStock(Long skuId, Long wareId, Integer num ,Long taskDetailId) {
        wareSkuDao.unLockStock(skuId, wareId, num);
        // 解锁后应该改变订单详情的状态
        WareOrderTaskDetailEntity wareOrderTaskDetailEntity = new WareOrderTaskDetailEntity();
        wareOrderTaskDetailEntity.setId(taskDetailId);
        wareOrderTaskDetailEntity.setLockStatus(2);
        orderTaskDetailService.updateById(wareOrderTaskDetailEntity);
    }

小结:即锁库存成功后,我们要搞一个定时任务一样,在一段时间(当然这里设计的时间需要比订单支付的时间长,比如pdd下单,没支付,30分钟后就会自动取消订单,所以要比30分钟长才可以)后检查一下订单是否支付,没有支付过期了或者说取消订单了,我们需要把锁住的库存恢复这里恢复我们使用得是wms_ware_order_task表(主要记录订单,即是哪一个订单的)wms_ware_order_task_detail表(记录订单中的每一项商品,比如skuId为42的锁住了2件)

其结构如下:

接着我们需要考虑传递什么值到rabbitmq队列里,使得拿到值的方法可以有足够的信息恢复(解锁库存),因此我们封装了一个StockLockedTo,其中的StockDetailTo其实就是表ware_order_task的实体,只是为了传输重新封装了一个StockDetailTo,所以监听的方法拿到消息中的实体后就可以进行解锁库存操作了 

@Data
public class StockLockedTo {

    private Long id; //库存工作单的id
    private StockDetailTo detail; // 工作详情
}

 三、订单

那么如果订单被取消的时候直接发送消息给mq的库存队列,然后mq进行解锁,是不是一个双重保障呢?确实如此,就相当于订单是主体,而库存的队列相当于补偿

订单设计图如下

解读:创建一个order-event-exchange交换机和两个队列,死信队列order.delay.queue和普通队列order.release.order.queue,交换机和order.delaly.queue通过路由order.create.order绑定,交换机和order.release.order.queue通过路由order.release.order绑定。当订单创建成功后,发消息到死信队列,如果30分钟内没付款,就会通过路由器进入普通队列,此时监听普通队列的方法就可以取出消息更改订单状态,同时发送解锁消息给库存的队列

向rabbitmq注册队列和交换机的代码:
 

@Configuration
public class MyMQConfig {
    // 通过bean的形式向rabbitmq创建Queue、Exchange、Binding



    /**
     * 死信队列
     *
     * @return
     */@Bean
    public Queue orderDelayQueue() {
        /*
            Queue(String name,  队列名字
            boolean durable,  是否持久化
            boolean exclusive,  是否排他
            boolean autoDelete, 是否自动删除
            Map<String, Object> arguments) 属性
         */
        HashMap<String, Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange", "order-event-exchange");
        arguments.put("x-dead-letter-routing-key", "order.release.order");
        arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟
        Queue queue = new Queue("order.delay.queue", true, false, false, arguments);

        return queue;
    }

    /**
     * 普通队列
     *
     * @return
     */
    @Bean
    public Queue orderReleaseQueue() {

        Queue queue = new Queue("order.release.order.queue", true, false, false);

        return queue;
    }

    /**
     * TopicExchange
     *
     * @return
     */
    @Bean
    public Exchange orderEventExchange() {
        /*
         *   String name,
         *   boolean durable,
         *   boolean autoDelete,
         *   Map<String, Object> arguments
         * */
        return new TopicExchange("order-event-exchange", true, false);

    }


    @Bean
    public Binding orderCreateBinding() {
        /*
         * String destination, 目的地(队列名或者交换机名字)
         * DestinationType destinationType, 目的地类型(Queue、Exhcange)
         * String exchange,
         * String routingKey,
         * Map<String, Object> arguments
         * */
        return new Binding("order.delay.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.create.order",
                null);
    }

    @Bean
    public Binding orderReleaseBinding() {

        return new Binding("order.release.order.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.release.order",
                null);
    }

    /*
     * 订单释放直接发送消息到进行绑定
     */
    @Bean
    public Binding orderReleaseOtherBinding() {
        return new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.release.other.#",
                null);
    }
}

下订单的方法

    @Transactional
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {

        submitVoThreadLocal.set(vo);
        MemberResVo memberResVo = LoginUserInterceptor.loginUser.get();

        SubmitOrderResponseVo response = new SubmitOrderResponseVo();
        response.setCode(0);
        String redisToken = redisTemplate.opsForValue().get(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResVo.getId());
        String orderToken = vo.getOrderToken();
        // 成功返回1  失败返回0
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        // 保证原子性
        Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResVo.getId()), orderToken);
        if(result == 0L) {
            // 验证失败
            response.setCode(1);
            return response;
        } else {
            // 下单,创建订单,校验令牌,检验价格,锁库存
            // TODO 1、创建订单,订单项等信息
            OrderCreateTo order = createOrder();
            // TODO 2、验价
            BigDecimal payAmount = order.getOrder().getPayAmount();
            if(Math.abs(payAmount.subtract(vo.getPayPrice()).doubleValue()) < 0.01) {
                // 金额对比成功后保存订单
                // TODO 3、保存订单
                saveOrder(order);

                WareSkuLockVo wareSkuLockVo = new WareSkuLockVo();
                wareSkuLockVo.setOrderSn(order.getOrder().getOrderSn());
                List<OrderItemVo> collect = order.getOrderItems().stream().map(item -> {
                    OrderItemVo orderItemVo = new OrderItemVo();
                    orderItemVo.setCount(item.getSkuQuantity());
                    orderItemVo.setSkuId(item.getSkuId());
                    orderItemVo.setTitle(item.getSkuName());
                    return orderItemVo;
                }).collect(Collectors.toList());
                wareSkuLockVo.setLocks(collect);
                // TODO  4、锁库存
                // 出异常后,因为远程锁库存成功,但是忘了原因超时了,订单回滚,库存不回滚

                // 为了保证高并发,库存服务自己要回滚,可以发消息给库存服务
                // 库存服务本身也可以使用自动解锁模式 即使用消息队列
                R r = wareFeignService.orderLockStock(wareSkuLockVo);
                if(r.getCode() == 0) {
                    // 锁成功
                    response.setOrder(order.getOrder());

                    // TODO 5 出异常
//                    int i = 10/0;
                    // TODO 订单创建成功发送消息给MQ
                    rabbitTemplate.convertAndSend("order-event-exchange", "order.create.order", order.getOrder());
                    return response;
                } else {
                    // 锁定失败
                    // 抛异常才能使事务回滚
                    response.setCode(3);
                    throw new NoStockException((String)r.get("msg"));

//                    return response;
                }
            } else {
                response.setCode(2); // 金额对比失败
                return response;
            }

        }
    }

监听普通队列的方法

@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {
    @Autowired
    OrderService orderService;
    @RabbitHandler
    public void lisentner(OrderEntity entity, Channel channel, Message message) throws IOException {

        try {
            orderService.closeOrder(entity);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (IOException e) {
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
        }
        System.out.println("收到订单信息,即将关闭订单" + entity);

    }


}

closeOrder方法如下:

    @Override
    public void closeOrder(OrderEntity entity) {
        OrderEntity orderEntity = this.getById(entity.getId());
        // 订单的状态需要是新创建的
        if(orderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()) {
            //不能使用上面的orderEntity,然后直接更新状态进行更新,因为在创建过程中经历了那么多
            // 步骤,可能一些属性已经发生改变了。
            OrderEntity update = new OrderEntity();
            update.setId(orderEntity.getId());
            update.setStatus(OrderStatusEnum.CANCLED.getCode());
            this.updateById(update);
            OrderTo orderTo = new OrderTo();
            BeanUtils.copyProperties(orderEntity, orderTo);
            // 立即发送消息给库存通知其解锁
            rabbitTemplate.convertAndSend("order-event-exchange", "order.release.other", orderTo);
        }
    }

因为封装到rabbitmq进行传递的消息不一样,所以库存的监听方法需要增加

@Slf4j
@RabbitListener(queues = "stock.release.stock.queue")
@Service
public class StockReleaseListener {

    @Autowired
    private WareSkuService wareSkuService;

    /**
     * 1、库存自动解锁
     *  下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
     *
     *  2、订单失败
     *      库存锁定失败
     *
     *   只要解锁库存的消息失败,一定要告诉服务解锁失败
     *
     *   该方法是处理库存自己发给自己的
     */
    @RabbitHandler
    public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
        log.info("******收到解锁库存的信息******");
        try {

            //当前消息是否被第二次及以后(重新)派发过来了
            // Boolean redelivered = message.getMessageProperties().getRedelivered();

            //解锁库存
            wareSkuService.unlockStock(to);
            // 手动删除消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            // 解锁失败 将消息重新放回队列,让别人消费
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }

    /*
     * 该方法是库存处理订单服务发消息队列的消息
     * 为什么会有这个步骤
     * 1、当订单服务如果卡顿,然后还没取消订单
     * 2、此时库存消息队列里的时间到了,库存监听到然后一看订单不是取消状态,所以直接更改库存的详情状态
     * 3、而过后订单服务反应过来了,取消了订单,但库存永远也回不去了,被锁死了
     */
    @RabbitHandler
    public void handleOrderCloseRelease(OrderTo to, Message message, Channel channel) throws IOException {
        log.info("******订单关闭准备解锁库存******");
        try {

            //当前消息是否被第二次及以后(重新)派发过来了
            // Boolean redelivered = message.getMessageProperties().getRedelivered();

            //解锁库存
            wareSkuService.unlockStock(to);
            // 手动删除消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            // 解锁失败 将消息重新放回队列,让别人消费
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
}
unlockStock方法如下:
    /*
     * 防止订单服务卡顿,导致订单状态消息一直改不了,库存消息优先到期,查订单状态新建状态,什么都不做就走了
     * 导致卡顿的订单永远不能解锁库存
     */
    @Override
    public void unlockStock(OrderTo to) {
        String orderSn = to.getOrderSn();
        WareOrderTaskEntity taskEntity = orderTaskService.getOne(new QueryWrapper<WareOrderTaskEntity>().eq("order_sn", orderSn));
        List<WareOrderTaskDetailEntity> entities = orderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>()
                .eq("task_id", taskEntity.getId())
                .eq("lock_status", 1));

        for (WareOrderTaskDetailEntity entity : entities) {
            unLockStock(entity.getSkuId(), entity.getWareId(), entity.getSkuNum(), entity.getId());
        }

    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1021573.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【JAVASE】图书管理系统

⭐ 作者&#xff1a;小胡_不糊涂 &#x1f331; 作者主页&#xff1a;小胡_不糊涂的个人主页 &#x1f4c0; 收录专栏&#xff1a;浅谈Java &#x1f496; 持续更文&#xff0c;关注博主少走弯路&#xff0c;谢谢大家支持 &#x1f496; 图书管理系统 1. 设计思路图2. 创建 boo…

2023年 国赛 数学建模C 基于遗传算法和神经网络的销量定价模型

一、写在开头 阅读者可能需要先阅读2023年国赛C题才能读懂下面的内容。 文章着重于解题方向指引和经历分享&#xff0c;只解释部分核心代码。 二、内容概述 刚刚做完比赛&#xff0c;对这段经历和对问题的处理方法进行下记录。 三、个人经历 今年大三&#xff0c;第一…

stm32f103步进电机S曲线计算

S曲线主要实现低速扭力大&#xff0c;更快更稳 https://zhuanlan.zhihu.com/p/396648926?utm_campaign&utm_mediumsocial&utm_oi1361101006265331712&utm_psn1686906450235133952&utm_sourcezhihu 可点击上面链接查看啤酒杯的运动动画 摘自一段知乎上一段关于…

html div span 容器元素

html div && span 容器元素 div 标签定义 HTML 文档中的一个分隔区块或者一个区域部分, 标签常用于组合块级元素&#xff0c;以便通过 CSS 来对这些元素进行格式化 span 用于对文档中的行内元素进行组合 标签提供了一种将文本的一部分或者文档的一部分独立出来的方式 &…

【ELFK】之zookeeper

一、Zookeeper是什么&#xff1f; zooleeper是一个分布式服务管理框架。存储业务服务节点元数据及信息&#xff0c;并复制&#xff1b;通知客户端在zookeeper上注册的服务节点状态&#xff0c;通过文件系统通知机制 1、Zookeeper工作机制 Zookeeper从设计模式角度来理解 是…

Java——文件操作IO

一 、文件File 狭义的文件&#xff1a; 指硬盘上的 文件 和 目录 。 广义的文件&#xff1a; 泛指计算机中的很多软硬件资源。 针对硬盘这种持久化存储的I/O设备&#xff0c;当我们想要进行数据保存时&#xff0c; 往往不是保存成一个整体&#xff0c;而是独立成一个个的单位…

C/C++简单计算器 2019年12月电子学会青少年软件编程(C/C++)等级考试一级真题答案解析

目录 C/C简单计算器 一、题目要求 1、编程实现 2、输入输出 二、解题思路 1、案例分析 三、程序代码 四、程序说明 五、运行结果 六、考点分析 C/C简单计算器 2019年12月 C/C编程等级考试一级编程题 一、题目要求 1、编程实现 一个最简单的计算器&#xff0c;支持…

Hadoop NameNode执行命令工作流程

Hadoop NameNode执行命令工作流程 客户端API或者CLI与NameNode的交互命令数据的格式(1) 预处理流程(2) 创建NameNode与NameNodePrcServer流程(3) HDFS API以及CLI的命令到NameNode的工作执行流程(4) 执行命令的参数流动 客户端API或者CLI与NameNode的交互命令数据的格式 hadoop…

读高性能MySQL(第4版)笔记10_查询性能优化(上)

1. 三管齐下 1.1. 不做、少做、快速地做 1.2. 如果查询太大&#xff0c;服务端会拒绝接收更多的数据并抛出相应错误 1.3. 如果查询写得很糟糕&#xff0c;即使库表结构再合理、索引再合适&#xff0c;也无法实现高性能 1.4. 查询优化、索引优化、库表结构优化需要齐头并进&…

JS的WebAPI

WebAPI背景知识 什么是 WebAPI 前面学习的 JS 分成三个大的部分 ECMAScript: 基础语法部分 DOM API: 操作页面结构 BOM API: 操作浏览器 WebAPI 就包含了 DOM BOM. 什么是 API API 是一个更广义的概念. 而 WebAPI 是一个更具体的概念, 特指 DOMBOM&#xff0c;所谓的 API …

使用 Elasticsearch、OpenAI 和 LangChain 进行语义搜索

在本教程中&#xff0c;我将引导您使用 Elasticsearch、OpenAI、LangChain 和 FastAPI 构建语义搜索服务。 LangChain 是这个领域的新酷孩子。 它是一个旨在帮助你与大型语言模型 (LLM) 交互的库。 LangChain 简化了与 LLMs 相关的许多日常任务&#xff0c;例如从文档中提取文本…

服务网格和CI/CD集成:讨论服务网格在持续集成和持续交付中的应用。

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

混淆矩阵细致理解

1、什么是混淆矩阵 混淆矩阵&#xff08;Confusion Matrix&#xff09;是深度学习和机器学习领域中的一个重要工具&#xff0c;用于评估分类模型的性能。它提供了一个清晰的视觉方式来展示模型的预测结果与真实标签之间的关系&#xff0c;尤其在分类任务中&#xff0c;帮助我们…

浅谈应急照明系统在民用建筑的设计应用与产品选型

贾丽丽 安科瑞电气股份有限公司 上海嘉定 201801 【摘要】应急照明分为备用照明、安全照明及疏散照明。文章介绍了应急照明系统的设计、灯具选择、灯具布置、配电等要求。并结合实例进行疏散照明的计算&#xff0c;以指导应急照明系统的设计与应用。 【关键词】照度&#xf…

大数据学习1.4-xShell配置Hadoop

1.创建hadoop目录 mkdir /usr/local/hadoop 2.切换到hadoop中 cd /usr/local/hadoop/ 3.将hadoop直接拖到xShell中 4.解压hadoop tar -zxvf hadoop-2.7.1.tar.gz 5.配置环境变量 vi /etc/profile export PATH$PATH:/usr/local/hadoop/hadoop-2.7.1/bin 6.加载配置文件(不能…

【刷题】蓝桥杯

蓝桥杯2023年第十四届省赛真题-平方差 - C语言网 (dotcpp.com) 初步想法&#xff0c;x y2 − z2&#xff08;yz)(y-z) 即xa*b&#xff0c;ayz&#xff0c;by-z 2yab 即ab是2的倍数就好了。 即x存在两个因数之和为偶数就能满足条件。 但时间是&#xff08;r-l&#xff09;*x&am…

Mybatis学习笔记8 查询返回专题

1.返回实体类 2.返回List<实体类> 3.返回Map 4.返回List<Map> 5.返回Map<String,Map> 6.resultMap结果集映射 7.返回总记录条数 新建模块 依赖 目录结构 1.返回实体类 如果返回多条,用单个实体接收会出异常 2.返回List<实体类> 即使返回一条记…

【软考】系统集成项目管理工程师(四)项目管理一般知识

一、 项目 1、 项目的定义 为大到特定的目的、使用一定的资源、在确定的期间内、为特定发起人而提供独特的产品、服务或成果而进行的一次性努力。 2、项目目标 分类描述成果性目标项目目标&#xff1a;满足客观要求的产品、系统、服务或者成果&#xff1b;例&#xff1a;①…

删除链表中所有含有val的节点

给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,6,3,4,5,6], val 6 输出&#xff1a;[1,2,3,4,5] 思路1&#xff1a;遍历查找&#xf…

大数据学习1.1-Centos8网络配置

1.查看虚拟网卡 2.配置网络信息 打勾处取消 记住箭头的数字 3.修改 网络连接 4.进入虚拟网络 5.进入属性 6.修改IPv4 5.将iIP和DNS进行修改 6.配置网络信息-进入修改网络配置文件 # 进入root用户 su root # 进入网络配置文件 cd /etc/sysconfig/network-scripts/ # 修改网络配…