RabbitMQ(七)ACK 消息确认机制

news2024/10/6 18:23:28

目录

    • 一、简介
      • 1.1 背景
      • 1.2 定义
      • 1.3 如何查看确认/未确认的消息数?
    • 二、消息确认机制的分类
      • 2.1 消息发送确认
        • 1)ConfirmCallback方法
        • 2)ReturnCallback方法
        • 3)代码实现方式一:统一配置
          • a.配置类
          • a.生产者
          • c.消费者
          • d.测试结果
        • 4)代码实现方式二:单独配置
      • 2.2 消息接收确认
        • 1)basicAck() 方法
        • 2)basicReject() 方法
        • 3)basicNack() 方法
        • 4)代码实现
          • a.配置方式一:代码配置
          • b.配置方式二:配置文件
          • c.生产者
          • d.消费者
          • e.测试结果

在这里插入图片描述

当我们在项目中引入了新的中间件之后,数据的风险性就要多一层考虑。那么,RabbitMQ 的消息是怎么知道有没有被消费者消费的呢?生产者又怎么确保自己发送成功了呢?这些问题将在文章中进行解答。

一、简介

1.1 背景

在 MQ 中,消费者和生产者并不直接进行通信,生产者只负责把消息发送到队列,消费者只负责从队列获取消息。

  • 消费者从队列 获取到消息后,这条消息就不在队列中了。如果此时消费者所在的信道 因为网络中断没有消费到,那这条消息就 被永远地丢失了。所以,我们希望等待消费者 成功消费掉这条消息之后再删除消息
  • 生产者向交换机 发送消息后,也 不能保证消息准确发送过去了,消息就像 石沉大海 一样,所以 发送消息也需要进行消息确认

1.2 定义

为了保证消息从队列可靠地到达消费者,RabbitMQ 提供了 消息确认机制(Message Acknowledgement)

消费者在订阅队列时,可以指定 autoAck 参数:

  • autoAck=false:RabbitMQ 会 等待消费者显式地回复确认信号 后才从内存(或磁盘)中移除消息(实际上时先打上删除标记,之后再删除)。
  • autoAck=true:RabbitMQ 会 自动把发送出去的消息置为确认,然后内存(或磁盘)中删除,而 不管消费者是否真正地消费到了这些消息

采用消息确认机制后,只要设置 autoAck 参数为 false消费者就有足够的时间处理消息(任务),不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,因为 RabbitMQ 会一直等待持有消息知道消费者显式调用 Basic.Ack 命令为止。

对于 RabbitMQ 服务器端而言,当 autoAck 参数为 false 时,队列中的消息分成了两部分:

  • 一部分是 等待投递给消费者的消息
  • 另一部分是 已经投递给消费者,但是还没有收到消费者确认信号的消息

在这里插入图片描述

如果 RabbitMQ 服务器端 一直没有收到消费者的确认信息,并且 消费此消息的消费者已经断开连接,则服务器端会安排 该消息重新进入队列,等待投递给下一个消费者(也可能还是原来的那个消费者)。

RabbitMQ 不会为未确认的消息设置过期时间,它 判断此消息是否需要重新投递给消费者的唯一依据是该消息连接是否已经断开,这个设计的原因是 RabbitMQ 允许消费者消费一条消息的时间可以很久很久。

1.3 如何查看确认/未确认的消息数?

RabbitMQ 的 Web 管理平台上可以看到当前队列中的 “Ready” 状态和 “Unacknowledged” 状态的消息数:

  • Read 状态: 等待投递给消费者的消息数。
  • Unacknowledged 状态: 已经投递给消费者但是未收到确认信号的消息树。

在这里插入图片描述


二、消息确认机制的分类

RabbitMQ 消息确认机制分为两大类:

  1. 消息发送确认,又分为:
    • 生产者到交换机的确认
    • 交换机到队列的确认
  2. 消息接收确认

2.1 消息发送确认

RabbitMQ 的消息发送确认有两种实现方式:ConfirmCallback 方法、ReturnCallback 方法。

1)ConfirmCallback方法

ConfirmCallback 是一个回调接口,用于确认消息否是到达交换机中

配置方式:

spring.rabbitmq.publisher-confirm-type=correlated

它有三个值:

  • none:禁用发布确认模式,默认值。
  • correlated:发布消息成功到交换机后触发回调方法。
  • simple:经测试有两种效果:一是和 correlated 一样会触发回调方法;二是在发布消息成功后使用 rabbitTemplate 调用 waitForConfirm 或 waitForConfirmsOrDie方法等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑。要注意的是 waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel,则接下来无法发送消息到 broker。
2)ReturnCallback方法

ReturnCallback 也是一个回调接口,用于确认消息是否在交换机中路由到了队列

(该方法可以不使用,因为交换机和队列是在代码里面绑定的,如果消息成功投递到 Broker 后几乎不存在绑定队列失败,除非代码写错了。)

配置方式:

spring.rabbitmq.publisher-returns=true
3)代码实现方式一:统一配置
a.配置类

RabbitDirectConfig.java

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * <p> @Title RabbitDirectConfig
 * <p> @Description 直连交换机配置
 * Direct Exchange是RabbitMQ默认的交换机模式,也是最简单的模式,根据key全文匹配去寻找队列。
 *
 * @author ACGkaka
 * @date 2023/1/12 15:09
 */
@Slf4j
@Configuration
public class RabbitDirectConfig {

    public static final String DIRECT_EXCHANGE_NAME = "TEST_DIRECT_EXCHANGE";
    public static final String DIRECT_ROUTING_NAME = "TEST_DIRECT_ROUTING";
    public static final String DIRECT_QUEUE_NAME = "TEST_DIRECT_QUEUE";

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate();
        rabbitTemplate.setConnectionFactory(connectionFactory);
        // 设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
        rabbitTemplate.setMandatory(true);
        //设置message序列化方法
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());

        // 设置消息发送到交换机(Exchange)回调
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            if (ack) {
                log.info(">>>>>>>>>>【INFO】消息发送到交换机(Exchange)成功, 相关数据: {}", correlationData);
            } else {
                log.error(">>>>>>>>>>【ERROR】消息发送到交换机(Exchange)失败, 错误原因: {}, 相关数据: {}", cause, correlationData);
            }
        });

        // 设置消息发送到队列(Queue)回调(经测试,只有失败才会调用)
        rabbitTemplate.setReturnsCallback((returnedMessage) -> {
            log.error(">>>>>>>>>>【ERROR】消息发送到队列(Queue)失败:响应码: {}, 响应信息: {}, 交换机: {}, 路由键: {}, 消息内容: {}",
                    returnedMessage.getReplyCode(), returnedMessage.getReplyText(), returnedMessage.getExchange(), returnedMessage.getRoutingKey(), returnedMessage.getMessage());
        });
        return rabbitTemplate;
    }

    /**
     * 消息监听-反序列化
     */
    @Bean
    public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        return factory;
    }

    /**
     * 队列,命名:testDirectQueue
     *
     * @return 队列
     */
    @Bean
    public Queue testDirectQueue() {
        // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
        // exclusive:默认false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable。
        // autoDelete:是否自动删除,当没有生产者或消费者使用此队列,该队列会自动删除。

        // 一般设置一下队列的持久化就好,其余两个默认false
        return new Queue(DIRECT_QUEUE_NAME, true);
    }

    /**
     * Direct交换机,命名:testDirectExchange
     * @return Direct交换机
     */
    @Bean
    DirectExchange testDirectExchange() {
        return new DirectExchange(DIRECT_EXCHANGE_NAME, true, false);
    }

    /**
     * 绑定 将队列和交换机绑定,并设置用于匹配键:testDirectRouting
     * @return 绑定
     */
    @Bean
    Binding bindingDirect() {
        return BindingBuilder.bind(testDirectQueue()).to(testDirectExchange()).with(DIRECT_ROUTING_NAME);
    }
}
a.生产者

SendMessageController.java

import com.demo.config.RabbitDirectConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * <p> @Title SendMessageController
 * <p> @Description 推送消息接口
 *
 * @author ACGkaka
 * @date 2023/1/12 15:23
 */
@Slf4j
@RestController
public class SendMessageController {

    /**
     * 使用 RabbitTemplate,这提供了接收/发送等方法。
     */
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/sendDirectMessage")
    public String sendDirectMessage() {
        String messageId = String.valueOf(UUID.randomUUID());
        String messageData = "Hello world.";
        String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        Map<String, Object> map = new HashMap<>();
        map.put("messageId", messageId);
        map.put("messageData", messageData);
        map.put("createTime", createTime);
        // 将消息携带绑定键值:TEST_DIRECT_ROUTING,发送到交换机:TEST_DIRECT_EXCHANGE
        rabbitTemplate.convertAndSend(RabbitDirectConfig.DIRECT_EXCHANGE_NAME, RabbitDirectConfig.DIRECT_ROUTING_NAME, map);
        return "OK";
    }

}
c.消费者

DirectReceiver.java

import com.demo.config.RabbitDirectConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * <p> @Title DirectReceiver
 * <p> @Description 直连交换机监听类
 *
 * @author ACGkaka
 * @date 2023/1/12 15:59
 */
@Slf4j
@Component
public class DirectReceiver {

    @RabbitListener(queues = RabbitDirectConfig.DIRECT_QUEUE_NAME)
    public void process(Map<String, Object> testMessage) {
        System.out.println("DirectReceiver消费者收到消息:" + testMessage.toString());
    }

}
d.测试结果

成功发送时,执行结果:

在这里插入图片描述

交换机错误时,执行结果:

在这里插入图片描述

路由键错误时,执行结果:

在这里插入图片描述

4)代码实现方式二:单独配置

除了在配置类里面统一设置回调方法外,还可以在每次推送消息到队列时,手动使用 CorrelationData 指定回调方法。

@GetMapping("/sendDirectMessage2")
public String sendDirectMessage2() {
    String messageId = String.valueOf(UUID.randomUUID());
    String messageData = "Hello world.";
    String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    Map<String, Object> map = new HashMap<>();
    map.put("messageId", messageId);
    map.put("messageData", messageData);
    map.put("createTime", createTime);

    //生成唯一标识
    CorrelationData correlationData = new CorrelationData(messageId);

    //不管成功失败都会调用confirm或者throwable,这是异步调用
    correlationData.getFuture().addCallback(
            confirm -> {
                // 设置消息发送到交换机(Exchange)回调
                if (confirm != null && confirm.isAck()) {
                    log.info(">>>>>>>>>>【INFO】发送成功ACK,msgId: {}, message: {}", correlationData.getId(), map);
                } else {
                    log.error(">>>>>>>>>>【ERROR】发送失败NACK,msgId: {}, message: {}", correlationData.getId(), map);
                }
            },
            throwable -> {
                //发生错误,链接mq异常,mq未打开等...报错回调
                System.out.println("发送失败throwable = " + throwable + ",  id:" + correlationData.getId());
            }
    );

    // 将消息携带绑定键值:TEST_DIRECT_ROUTING,发送到交换机:TEST_DIRECT_EXCHANGE
    rabbitTemplate.convertAndSend(RabbitDirectConfig.DIRECT_EXCHANGE_NAME, RabbitDirectConfig.DIRECT_ROUTING_NAME, map, correlationData);
    return "OK";
}

2.2 消息接收确认

消费者确认发生在 监听队列的消费者处理业务失败,如:发生了异常、不符合要求的数据等。这些场景就 需要我们手动处理消息,比如:重新发送消息或者丢弃消息。

RabbitMQ 的 消息确认机制(ACK) 默认是自动确认的。自动确认会 在消息发送给消费者后立即确认,但 存在丢失消息的可能。如果消费端消费逻辑抛出了异常,假如我们使用了事务的回滚,也只是保证了数据的一致性,消息还是丢失了。也就是消费端没有处理成功这条消息,那么就相当于丢失了消息。

消息的确认模式有三种:

  1. AcknowledgeMode.NONE:自动确认。(默认)
  2. AcknowledgeMode.AUTO:根据情况确认。
  3. AcknowledgeMode.MANUAL:手动确认。(推荐)

消费者收到消息后,手动调用 Channel 的 basicAck()/basicReject()/basicNack() 方法后,RabbitMQ 收到消息后,才认为本次投递完成。

  1. basicAck():用于确认当前消息。
  2. basicReject():用于拒绝当前消息,可以自定义是否重回队列。
  3. basicNack():用于批量拒绝消息(这是 AMPQ 0-9-1 的 RabbitMQ 扩展)。
1)basicAck() 方法

basicAck() 方法 用于确认当前消息Channel 类中的方法定义如下:

void basicAck(long deliveryTag, boolean multiple) throws IOException;

参数说明:

  • long deliveryTag: 当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel。RabbitMQ 会用 basic.deliver 方法向消费者推送消息,这个方法携带了一个 deliveryTag,它代表了 RabbitMQ 向该 Channel 投递的这条消息的唯一标识ID,是一个单调递增的正整数,deliveryTag 的范围仅限于当前 Channel。
  • boolean multiple: 是否批处理,一般为 false,当该参数为 true 时,则可以一次性确认 deliveryTag 小于等于传入值的所有消息。
2)basicReject() 方法

basicReject() 方法 用于明确拒绝当前的消息。RabbitMQ 在 2.0.0 版本开始引入,Channel 类中的方法定义如下:

void basicReject(long deliveryTag, boolean requeue) throws IOException;

参数说明:

  • long deliveryTag: 当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel。RabbitMQ 会用 basic.deliver 方法向消费者推送消息,这个方法携带了一个 deliveryTag,它代表了 RabbitMQ 向该 Channel 投递的这条消息的唯一标识ID,是一个单调递增的正整数,deliveryTag 的范围仅限于当前 Channel。
  • boolean requeue: 是否重新放回队列。
    • 如果参数为 true,则 RabbitMQ 会重新将这条消息存入队列,以便发送给下一个订阅的消费者。
    • 如果参数为 false,则 RabbitMQ 会立即把消息从队列中移除,不会把它发送给新的消费者。
3)basicNack() 方法

basicNack() 方法 用于批量拒绝消息。由于 basicReject() 方法一次只能拒绝一条消息,如果想批量拒绝消息,则可以使用 basicNack() 方法。Channel 类中的方法定义如下:

参数说明:

  • long deliveryTag: 当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel。RabbitMQ 会用 basic.deliver 方法向消费者推送消息,这个方法携带了一个 deliveryTag,它代表了 RabbitMQ 向该 Channel 投递的这条消息的唯一标识ID,是一个单调递增的正整数,deliveryTag 的范围仅限于当前 Channel。
  • boolean multiple: 是否批处理,一般为 false,当该参数为 true 时,则可以一次性确认 deliveryTag 小于等于传入值的所有消息。
  • boolean requeue: 是否重新放回队列。
    • 如果参数为 true,则 RabbitMQ 会重新将这条消息存入队列,以便发送给下一个订阅的消费者。
    • 如果参数为 false,则 RabbitMQ 会立即把消息从队列中移除,不会把它发送给新的消费者。
4)代码实现
a.配置方式一:代码配置

如果我们之前配置了 Jackson2JsonMessageConverter.java 的序列化方式,那么我们可以接着指定消费方的消息确认模式为 AcknowledgeMode.MANUL

/**
 * 消息监听配置
 */
@Bean
public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
    // 设置连接工厂
    factory.setConnectionFactory(connectionFactory);
    // 设置消息确认模式
    factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
    // 设置反序列化
    factory.setMessageConverter(new Jackson2JsonMessageConverter());
    return factory;
}
b.配置方式二:配置文件

我们可以直接在 application.yml 中进行如下配置:

# 确认模式,默认auto,自动确认;manual:手动确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual

注意: yaml中指定的是消费端容器的默认配置,如果我们在代码中有自定义注入 RabbitListenerContainerFactory 示例之后,还需要使用默认配置,需要在代码中进行设置,如下所示:

@Autowired
private SimpleRabbitListenerContainerFactoryConfigurer configurer;

/**
 * 消息监听配置
 */
@Bean
public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
    // 设置连接工厂
    factory.setConnectionFactory(connectionFactory);
    // 采用yaml中的配置
    configurer.configure(factory, connectionFactory);
    // 设置反序列化
    factory.setMessageConverter(new Jackson2JsonMessageConverter());
    return factory;
}
c.生产者

SendMessageController.java

import com.demo.config.RabbitDirectConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * <p> @Title SendMessageController
 * <p> @Description 推送消息接口
 *
 * @author ACGkaka
 * @date 2023/1/12 15:23
 */
@Slf4j
@RestController
public class SendMessageController {

    /**
     * 使用 RabbitTemplate,这提供了接收/发送等方法。
     */
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/sendDirectMessage")
    public String sendDirectMessage() {
        String messageId = String.valueOf(UUID.randomUUID());
        String messageData = "Hello world.";
        String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        Map<String, Object> map = new HashMap<>();
        map.put("messageId", messageId);
        map.put("messageData", messageData);
        map.put("createTime", createTime);
        // 将消息携带绑定键值:TEST_DIRECT_ROUTING,发送到交换机:TEST_DIRECT_EXCHANGE
        rabbitTemplate.convertAndSend(RabbitDirectConfig.DIRECT_EXCHANGE_NAME, RabbitDirectConfig.DIRECT_ROUTING_NAME, map);
        return "OK";
    }

}
d.消费者

DirectReceiver.java

import com.demo.config.RabbitDirectConfig;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Map;

/**
 * <p> @Title DirectReceiver
 * <p> @Description 直连交换机监听类
 *
 * @author ACGkaka
 * @date 2023/1/12 15:59
 */
@Slf4j
@Component
public class DirectReceiver {

    @RabbitListener(queues = RabbitDirectConfig.DIRECT_QUEUE_NAME)
    public void process(Map<String, Object> testMessage, Message message, Channel channel) throws IOException {
        try {
            log.info("DirectReceiver消费者收到消息: {}", testMessage.toString());

            // 手动答应消费完成,从队列中删除该消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);

        } catch (Exception e) {
            log.error("DirectReceiver消费者消费失败,原因: {}", e.getMessage(), e);

            // 手动答应消费完成,从队列中删除该消息(不重回队列)
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
        }
    }

}
e.测试结果

场景一:消费者进行手动确认,生产者推送2条消息:

可以看到,生产者推送2条消息后立马被消费了。

在这里插入图片描述

场景二:消费者不进行手动确认,生产者推送2条消息:

虽然消费者消费完毕,但是由于没有进行手动确认,所以2条消息会一直处于 Unacked 状态,直到消费者下线。

在这里插入图片描述

关闭 SpringBoot 程序,消费者下线后,消息由 Unacked 状态转为 Ready 状态,等待下一个消费者上线后重新进行消费。

在这里插入图片描述

整理完毕,完结撒花~ 🌻





参考地址:

1.RabbitMQ(4):消息确认机制详解,https://juejin.cn/post/7029232312197840904

2.RabbitMQ消息确认机制(ACK),https://blog.csdn.net/pan_junbiao/article/details/112956537

3.RabbitMQ高级,https://blog.csdn.net/hnhroot/article/details/125921527

4.关于rabbitMQ在yml配置手动ack不生效,重复答应的问题,https://blog.csdn.net/love_Saber_Archer/article/details/109111088

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

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

相关文章

Linux------mv命令超详解(狠狠爱住)

mv 命令用于移动文件或目录&#xff0c;也可以用来重命名文件或目录。 基本语法&#xff1a; mv [选项] 源文件 目标文件 常用选项&#xff1a; -i 或 --interactive&#xff1a;交互式地移动文件&#xff0c;如果目标文件已经存在&#xff0c;则会询问是否覆盖。-u 或 --…

网络安全卷么?

官方一直宣传网络安全是朝阳行业&#xff0c;每年缺少大几百万人才&#xff0c;民间网络安全公司生存堪忧&#xff0c;只能靠各种低价冲标&#xff0c;安全技术也是各种卷。你身边的网络安全人过的怎么样呢&#xff1f; 官方宣传的是对的网络安全现在是朝阳行业&#xff0c;缺口…

Apache POI 操作Excel表格

1、Apache POI介绍 Apache POI 是一个处理Miscrosoft Office文件格式的开源项目&#xff0c;在Java中可以用来对Miscrosoft Office的各种文件进行读写操作&#xff0c;本文是介绍Apache POI操作Excel的基础案例。 2、Apache POI操作Excel 引入pom依赖 <!--excel POI依赖…

电源模块电阻测试:万用表如何测量电源的电阻?

电阻是电路中常用的电子元件&#xff0c;它可以调节电压、限制电流&#xff0c;从而保护电路。电阻测试是电源模块的常规测试项目之一&#xff0c;常见的电阻测试方法是通过万用表来测量电阻阻值&#xff0c;具体如下&#xff1a; 一、两线法 适用于测量较大的电阻值&#xff0…

如何快速搭建自己的外贸/跨境电商独立站?

外贸老鸟一定听过行业内某些大神通过运营外贸独立站获得成百上千封的询盘&#xff0c;看到别人每天询盘接到手软常常羡慕不已&#xff0c;再回头看看自己的网站&#xff0c;一年到头也收不到几个询盘。等到终于抽出时间下定决心要打造自己的外贸独立站接单平台&#xff0c;又常…

逻辑卷学习后续----------缩容

一、缩容&#xff1a;缩减大小 ext4可以 &#xff0c; xfs无法缩减&#xff0c;缩减会影响业务 1.解挂载 2.检查文件系统完整性 3.缩减文件系统 4.缩减逻辑卷上下一致 5.再挂载回去 添加磁盘 文件系统只能装ext4 缩减文件系统 resize2fs 挂载失败需要重新安装文件系统…

电商数据公司的卓越之路:优秀特质与优质服务的探索

在数字化浪潮中&#xff0c;电商数据公司作为行业变革的驱动者&#xff0c;正发挥着越来越重要的作用。这些公司通过收集、分析和利用海量数据&#xff0c;为电商企业提供决策支持、市场洞察和个性化服务。然而&#xff0c;要在竞争激烈的市场中脱颖而出&#xff0c;一家优秀的…

两种差价,昂首资本一分钟讲清楚

在交易中各位投资者都知道交易中能盈利就是做到了低买高卖&#xff0c;但是其中差价又是能影响盈利多少的因素&#xff0c;今天昂首资本就一分钟讲清楚两种差价&#xff0c;其中一种很多投资者根本就没有听说过&#xff0c;更别说使用了。 其实谈起外汇差价&#xff0c;昂首资…

2024最新阿里云服务器优惠活动大全_特价活动清单

2024年阿里云服务器优惠活动大全&#xff0c;阿里云2核2G服务器ECS经济型e实例、3M固定带宽、40G ESSD Entry云盘&#xff0c;99元一年&#xff0c;续费和不涨价也是99元&#xff0c;新老用户均可以买。阿里云轻量应用服务器2核2G3M带宽优惠价格61元一年&#xff0c;云服务器吧…

2024全栈软件测试工程师,“我“的测试该如何走...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、什么是全栈测试…

Cypress安装与使用教程(3)—— 软测大玩家

&#x1f60f;作者简介&#xff1a;博主是一位测试管理者&#xff0c;同时也是一名对外企业兼职讲师。 &#x1f4e1;主页地址&#xff1a;【Austin_zhai】 &#x1f646;目的与景愿&#xff1a;旨在于能帮助更多的测试行业人员提升软硬技能&#xff0c;分享行业相关最新信息。…

重生奇迹MU中需要组队的地方

我们在重生奇迹mu中组队的问题上来说&#xff0c;每个玩家的定义都是不同的&#xff0c;但是根据实际情况来定的话&#xff0c;在很多场合下是需要组队才行的&#xff0c;必须是要通过组队这样能够对我们产生出来的效果和帮助才能是更大的&#xff0c;只是要注意好&#xff0c;…

电商补单API获取商品详情API调用说明(支持高并发请求)

商品采集示例 商品详情页 调用API采集商品数据&#xff08;展示部分&#xff09; item_get&#xff1a;采集商品详情页数据&#xff0c;通过传入商品id&#xff0c;返回商品详情页的数据。 item_search&#xff1a;采集商品列表&#xff0c;支持翻页展示。传入关键字&#xf…

海信旗下“隐形冠军”信芯微,授权世强硬创代理32位MCU等产品

近日&#xff0c;世强先进&#xff08;深圳&#xff09;科技股份有限公司&#xff08;下称“世强先进”&#xff09;与海信集团旗下子公司——青岛信芯微电子科技股份有限公司&#xff08;下称“信芯微”&#xff0c;英文名&#xff1a;Hi-image&#xff09;签订授权代理合作协…

格式转换工具,一键转换文件格式

有时候&#xff0c;为了满足工作或学习的需要&#xff0c;我们需要将文件从一种格式转换为另一种格式。传统的单文件转换方式不仅费时&#xff0c;而且容易出错。有没有便捷的方法可以解决这个问题&#xff1f;答案是肯定的&#xff0c;那就是使用【文件批量改名高手】来批量操…

vivo 数据库备份恢复系统演化

作者&#xff1a;vivo 互联网数据库团队 - Han Chaobing 介绍 vivo 数据库备份恢复功能的演化&#xff0c;以及对备份文件的功能扩展。 一、概述 vivo互联网领域拥有的数据库组件分别为 MySQL、MongoDB、TiDB 等&#xff0c;其中MySQL集群占比绝大部分&#xff0c; MongoDB …

一文带你玩转Superset!大数据可视化框架学习网站大盘点!

介绍&#xff1a;Superset是一款由Airbnb开源的现代化企业级BI工具&#xff0c;它主要用于数据分析和可视化工作。作为Apache孵化器项目的一部分&#xff0c;它在处理复杂的数据分析需求上表现出色&#xff0c;并支持多种数据源和丰富的图表类型。 这款工具的主要特点包括自助分…

LED显示屏常用驱动芯片一览表

LED显示屏驱动芯片是专门用于驱动LED显示屏的核心芯片&#xff0c;它能够将输入的电信号转化为驱动能力&#xff0c;以控制LED灯的亮度和颜色。LED显示屏驱动芯片具有高可靠性、低功耗、长寿命等优点&#xff0c;是LED显示屏产业的核心零部件之一。 SM16306SJ LED显示屏驱动芯…

Adobe Experience Design安装指南

XD&#xff08;Adobe Experience Design&#xff09;下载链接 https://pan.baidu.com/s/1MVcaE2GB1Q9YpgmgDxUGJw?pwd0531 1.鼠标右击【Adobe XD 55.1(64bit)】压缩包选择&#xff08;win11以上系统需先点击“显示更多选项”&#xff09;【解压到 Adobe XD 55.1(64bit)】。 …

wav音频文件解析

一、音频相关概念 1、样本 A/D转换器以每秒钟上万次的速率对声波进行采样&#xff0c;每个采样点都记录下了原始模拟声波在某一时刻的状态&#xff0c;通常称之为样本。通过将一串连续的样本连接起来&#xff0c;就可以在计算机中描述一段声音了。 2、采样频率 每一秒钟所采…