百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT知识 > 正文

Kafka消息可靠传输之幂等、事务机制

liuian 2025-06-15 17:36 91 浏览

一般而言,消息中间件的消息传输保障有3个层级,分别如下。

at most once:至多一次。消息可能会丢失,但绝对不会重复传输。

at least once:最少一次。消息绝不会丢失,但可能会重复传输。

exactly once:恰好一次。每条消息肯定会被传输一次且仅传输一次。

Kafka 的消息传输保障机制非常直观。当生产者向 Kafka 发送消息时,一旦消息被成功提交到日志文件,由于多副本机制的存在,这条消息就不会丢失。如果生产者发送消息到 Kafka 之后,遇到了网络问题而造成通信中断,那么生产者就无法判断该消息是否已经提交。虽然 Kafka 无法确定网络故障期间发生了什么,但生产者可以进行多次重试来确保消息已经写入 Kafka,这个重试的过程中有可能会造成消息的重复写入,所以这里 Kafka 提供的消息传输保障为 at least once。

对消费者而言,消费者处理消息和提交消费位移的顺序在很大程度上决定了消费者提供哪一种消息传输保障。如果消费者在拉取完消息之后,应用逻辑先处理消息后提交消费位移,那么在消息处理之后且在位移提交之前消费者宕机了,待它重新上线之后,会从上一次位移提交的位置拉取,这样就出现了重复消费,因为有部分消息已经处理过了只是还没来得及提交消费位移,此时就对应 at least once。

如果消费者在拉完消息之后,应用逻辑先提交消费位移后进行消息处理,那么在位移提交之后且在消息处理完成之前消费者宕机了,待它重新上线之后,会从已经提交的位移处开始重新消费,但之前尚有部分消息未进行消费,如此就会发生消息丢失,此时就对应 at most once。

Kafka 从 0.11.0.0 版本开始引入了幂等和事务这两个特性,以此来实现 EOS(exactly once semantics,精确一次处理语义)。

幂等

所谓的幂等,简单地说就是对接口的多次调用所产生的结果和调用一次是一致的。生产者在进行重试的时候有可能会重复写入消息,而使用 Kafka 的幂等性功能之后就可以避免这种情况。

开启幂等性功能的方式很简单,只需要显式地将生产者客户端参数 enable.idempotence 设置为 true 即可(这个参数的默认值为 false),参考如下:

properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);

# 或者

properties.put(“enable.idempotence”, true);

不过如果要确保幂等性功能正常,还需要确保生产者客户端的 retries、acks、
max.in.flight.requests.per.connection 这几个参数不被配置错。实际上在使用幂等性功能的时候,用户完全可以不用配置(也不建议配置)这几个参数。

如果用户显式地指定了 retries 参数,那么这个参数的值必须大于0,否则会报出 ConfigException:

org.apache.kafka.common.config.ConfigException: Must set retries to non-zero when using the idempotent producer.

如果用户没有显式地指定 retries 参数,那么 KafkaProducer 会将它置为 Integer.MAX_VALUE。同时还需要保证
max.in.flight.requests.per.connection 参数的值不能大于5(这个参数的值默认为5,在 2.2.1 节中有相关的介绍),否则也会报出 ConfigException:

org.apache.kafka.common.config.ConfigException: Must set max.in.flight. requests.per.connection to at most 5 to use the idempotent producer.

如果用户还显式地指定了 acks 参数,那么还需要保证这个参数的值为 -1(all),如果不为 -1(这个参数的值默认为1),那么也会报出 ConfigException:

org.apache.kafka.common.config.ConfigException: Must set acks to all in order to use the idempotent producer. Otherwise we cannot guarantee idempotence.

如果用户没有显式地指定这个参数,那么 KafkaProducer 会将它置为-1。开启幂等性功能之后,生产者就可以如同未开启幂等时一样发送消息了。

为了实现生产者的幂等性,Kafka 为此引入了 producer id(以下简称 PID)和序列号(sequence number)这两个概念,这两个概念其实在第2节中就讲过,分别对应 v2 版的日志格式中 RecordBatch 的 producer id 和 first seqence 这两个字段(参考第2节)。

每个新的生产者实例在初始化的时候都会被分配一个 PID,这个 PID 对用户而言是完全透明的。对于每个 PID,消息发送到的每一个分区都有对应的序列号,这些序列号从0开始单调递增。生产者每发送一条消息就会将 <PID,分区> 对应的序列号的值加1。

broker 端会在内存中为每一对 <PID,分区> 维护一个序列号。对于收到的每一条消息,只有当它的序列号的值(SN_new)比 broker 端中维护的对应的序列号的值(SN_old)大1(即 SN_new = SN_old + 1)时,broker 才会接收它。如果 SN_new< SN_old + 1,那么说明消息被重复写入,broker 可以直接将其丢弃。如果 SN_new> SN_old + 1,那么说明中间有数据尚未写入,出现了乱序,暗示可能有消息丢失,对应的生产者会抛出
OutOfOrderSequenceException,这个异常是一个严重的异常,后续的诸如 send()、beginTransaction()、commitTransaction() 等方法的调用都会抛出 IllegalStateException 的异常。

引入序列号来实现幂等也只是针对每一对 <PID,分区> 而言之,也就是说,Kafka 的幂等只能保证单个生产者会话(session)中单分区的幂等。

ProducerRecord<String, String> record

= new ProducerRecord<>(topic, "key", "msg");

producer.send(record);

producer.send(record);

注意,上面示例中发送了两条相同的消息,不过这仅仅是指消息内容相同,但对 Kafka 而言是两条不同的消息,因为会为这两条消息分配不同的序列号。Kafka 并不会保证消息内容的幂等。

事务

幂等性并不能跨多个分区运作,而事务可以弥补这个缺陷。事务可以保证对多个分区写入操作的原子性。操作的原子性是指多个操作要么全部成功,要么全部失败,不存在部分成功、部分失败的可能。

对流式应用(Stream Processing Applications)而言,一个典型的应用模式为“consume-transform-produce”。在这种模式下消费和生产并存:应用程序从某个主题中消费消息,然后经过一系列转换后写入另一个主题,消费者可能在提交消费位移的过程中出现问题而导致重复消费,也有可能生产者重复生产消息。Kafka中的事务可以使应用程序将消费消息、生产消息、提交消费位移当作原子操作来处理,同时成功或失败,即使该生产或消费会跨多个分区。

为了实现事务,应用程序必须提供唯一的 transactionalId,这个 transactionalId 通过客户端参数 transactional.id 来显式设置,参考如下:

properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "transactionId");

# 或者

properties.put("transactional.id", "transactionId");

事务要求生产者开启幂等特性,因此通过将 transactional.id 参数设置为非空从而开启事务特性的同时需要将 enable.idempotence 设置为 true(如果未显式设置,则 KafkaProducer 默认会将它的值设置为 true),如果用户显式地将 enable.idempotence 设置为 false,则会报出 ConfigException:

org.apache.kafka.common.config.ConfigException: Cannot set a transactional.id without also enabling idempotence.

transactionalId 与 PID 一一对应,两者之间所不同的是 transactionalId 由用户显式设置,而 PID 是由 Kafka 内部分配的。另外,为了保证新的生产者启动后具有相同 transactionalId 的旧生产者能够立即失效,每个生产者通过 transactionalId 获取 PID 的同时,还会获取一个单调递增的 producer epoch(对应下面要讲述的
KafkaProducer.initTransactions() 方法)。如果使用同一个 transactionalId 开启两个生产者,那么前一个开启的生产者会报出如下的错误:

org.apache.kafka.common.errors.ProducerFencedException: Producer attempted an operation with an old epoch. Either there is a newer producer with the same transactionalId, or the producer's transaction has been expired by the broker.

producer epoch 同 PID 和序列号一样在第2节中就讲过了,对应v2版的日志格式中 RecordBatch 的 producer epoch 字段。

从生产者的角度分析,通过事务,Kafka 可以保证跨生产者会话的消息幂等发送,以及跨生产者会话的事务恢复。前者表示具有相同 transactionalId 的新生产者实例被创建且工作的时候,旧的且拥有相同 transactionalId 的生产者实例将不再工作。后者指当某个生产者实例宕机后,新的生产者实例可以保证任何未完成的旧事务要么被提交(Commit),要么被中止(Abort),如此可以使新的生产者实例从一个正常的状态开始工作。

而从消费者的角度分析,事务能保证的语义相对偏弱。出于以下原因,Kafka 并不能保证已提交的事务中的所有消息都能够被消费:

对采用日志压缩策略的主题而言,事务中的某些消息有可能被清理(相同key的消息,后写入的消息会覆盖前面写入的消息)。

事务中消息可能分布在同一个分区的多个日志分段(LogSegment)中,当老的日志分段被删除时,对应的消息可能会丢失。

消费者可以通过 seek() 方法访问任意 offset 的消息,从而可能遗漏事务中的部分消息。

消费者在消费时可能没有分配到事务内的所有分区,如此它也就不能读取事务中的所有消息。

KafkaProducer 提供了5个与事务相关的方法,详细如下:

void initTransactions();

void beginTransaction() throws ProducerFencedException;

void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets,

String consumerGroupId)

throws ProducerFencedException;

void commitTransaction() throws ProducerFencedException;

void abortTransaction() throws ProducerFencedException;

initTransactions() 方法用来初始化事务,这个方法能够执行的前提是配置了 transactionalId,如果没有则会报出 IllegalStateException:

java.lang.IllegalStateException: Cannot use transactional methods without enabling transactions by setting the transactional.id configuration property.

beginTransaction() 方法用来开启事务;sendOffsetsToTransaction() 方法为消费者提供在事务内的位移提交的操作;commitTransaction() 方法用来提交事务;abortTransaction() 方法用来中止事务,类似于事务回滚。

一个典型的事务消息发送的操作如代码清单14-1所示。

代码清单14-1 事务消息发送示例

Properties properties = new Properties();

properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,

StringSerializer.class.getName());

properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,

StringSerializer.class.getName());

properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList);

properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, transactionId);


KafkaProducer<String, String> producer = new KafkaProducer<>(properties);


producer.initTransactions();

producer.beginTransaction();


try {

//处理业务逻辑并创建ProducerRecord

ProducerRecord<String, String> record1 = new ProducerRecord<>(topic, "msg1");

producer.send(record1);

ProducerRecord<String, String> record2 = new ProducerRecord<>(topic, "msg2");

producer.send(record2);

ProducerRecord<String, String> record3 = new ProducerRecord<>(topic, "msg3");

producer.send(record3);

//处理一些其他逻辑

producer.commitTransaction();

} catch (ProducerFencedException e) {

producer.abortTransaction();

}

producer.close();

在消费端有一个参数 isolation.level,与事务有着莫大的关联,这个参数的默认值为“read_uncommitted”,意思是说消费端应用可以看到(消费到)未提交的事务,当然对于已提交的事务也是可见的。这个参数还可以设置为“read_committed”,表示消费端应用不可以看到尚未提交的事务内的消息。

举个例子,如果生产者开启事务并向某个分区值发送3条消息 msg1、msg2 和 msg3,在执行 commitTransaction() 或 abortTransaction() 方法前,设置为“read_committed”的消费端应用是消费不到这些消息的,不过在 KafkaConsumer 内部会缓存这些消息,直到生产者执行 commitTransaction() 方法之后它才能将这些消息推送给消费端应用。反之,如果生产者执行了 abortTransaction() 方法,那么 KafkaConsumer 会将这些缓存的消息丢弃而不推送给消费端应用。

日志文件中除了普通的消息,还有一种消息专门用来标志一个事务的结束,它就是控制消息(ControlBatch)。控制消息一共有两种类型:COMMIT 和 ABORT,分别用来表征事务已经成功提交或已经被成功中止。KafkaConsumer 可以通过这个控制消息来判断对应的事务是被提交了还是被中止了,然后结合参数 isolation.level 配置的隔离级别来决定是否将相应的消息返回给消费端应用,如下图所示。注意 ControlBatch 对消费端应用不可见,后面还会对它有更加详细的介绍。




本节开头就提及了 consume-transform-produce 这种应用模式,这里还涉及在代码清单14-1中尚未使用的 sendOffsetsToTransaction() 方法。该模式的具体结构如下图所示。与此对应的应用示例如代码清单14-2所示。




代码清单14-2 消费—转换—生产模式示例

public class TransactionConsumeTransformProduce {

public static final String brokerList = "localhost:9092";


public static Properties getConsumerProperties(){

Properties props = new Properties();

props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList);

props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,

StringDeserializer.class.getName());

props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,

StringDeserializer.class.getName());

props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);

props.put(ConsumerConfig.GROUP_ID_CONFIG, "groupId");

return props;

}


public static Properties getProducerProperties(){

Properties props = new Properties();

props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList);

props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,

StringSerializer.class.getName());

props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,

StringSerializer.class.getName());

props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "transactionalId");

return props;

}


public static void main(String[] args) {

//初始化生产者和消费者

KafkaConsumer<String, String> consumer =

new KafkaConsumer<>(getConsumerProperties());

consumer.subscribe(Collections.singletonList("topic-source"));

KafkaProducer<String, String> producer =

new KafkaProducer<>(getProducerProperties());

//初始化事务

producer.initTransactions();

while (true) {

ConsumerRecords<String, String> records =

consumer.poll(Duration.ofMillis(1000));

if (!records.isEmpty()) {

Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();

//开启事务

producer.beginTransaction();

try {

for (TopicPartition partition : records.partitions()) {

List<ConsumerRecord<String, String>> partitionRecords

= records.records(partition);

for (ConsumerRecord<String, String> record :

partitionRecords) {

//do some logical processing.

ProducerRecord<String, String> producerRecord =

new ProducerRecord<>("topic-sink", record.key(),

record.value());

//消费—生产模型

producer.send(producerRecord);

}

long lastConsumedOffset = partitionRecords.

get(partitionRecords.size() - 1).offset();

offsets.put(partition,

new OffsetAndMetadata(lastConsumedOffset + 1));

}

//提交消费位移

producer.sendOffsetsToTransaction(offsets,"groupId");

//提交事务

producer.commitTransaction();

} catch (ProducerFencedException e) {

//log the exception

//中止事务

producer.abortTransaction();

}

}

}

}

}

注意:在使用 KafkaConsumer 的时候要将 enable.auto.commit 参数设置为 false,代码里也不能手动提交消费位移。

为了实现事务的功能,Kafka 还引入了事务协调器(TransactionCoordinator)来负责处理事务,这一点可以类比一下组协调器(GroupCoordinator)。每一个生产者都会被指派一个特定的 TransactionCoordinator,所有的事务逻辑包括分派 PID 等都是由 TransactionCoordinator 来负责实施的。TransactionCoordinator 会将事务状态持久化到内部主题 __transaction_state 中。下面就以最复杂的 consume-transform-produce 的流程(参考下图,后面就以“事务流程图”称呼)为例来分析 Kafka 事务的实现原理。




1. 查找TransactionCoordinator

TransactionCoordinator 负责分配 PID 和管理事务,因此生产者要做的第一件事情就是找出对应的 TransactionCoordinator 所在的 broker 节点。与查找 GroupCoordinator 节点一样,也是通过 FindCoordinatorRequest 请求来实现的,只不过 FindCoordinatorRequest 中的 coordinator_type 就由原来的0变成了1,由此来表示与事务相关联。

Kafka 在收到 FindCoorinatorRequest 请求之后,会根据 coordinator_key(也就是 transactionalId)查找对应的 TransactionCoordinator 节点。如果找到,则会返回其相对应的 node_id、host 和 port 信息。具体查找 TransactionCoordinator 的方式是根据 transactionalId 的哈希值计算主题 __transaction_state 中的分区编号,具体算法如代码清单14-3所示。

代码清单14-3 计算分区编号

Utils.abs(transactionalId.hashCode) % transactionTopicPartitionCount

其中
transactionTopicPartitionCount 为主题 __transaction_state 中的分区个数,这个可以通过 broker 端参数
transaction.state.log.num.partitions 来配置,默认值为50。

找到对应的分区之后,再寻找此分区 leader 副本所在的 broker 节点,该 broker 节点即为这个 transactionalId 对应的 TransactionCoordinator 节点。细心的读者可以发现,这一整套的逻辑和查找 GroupCoordinator 的逻辑如出一辙。

2. 获取PID

在找到 TransactionCoordinator 节点之后,就需要为当前生产者分配一个 PID 了。凡是开启了幂等性功能的生产者都必须执行这个操作,不需要考虑该生产者是否还开启了事务。生产者获取 PID 的操作是通过 InitProducerIdRequest 请求来实现的,InitProducerIdRequest 请求体结构如下图所示,其中 transactional_id 表示事务的 transactionalId,transaction_timeout_ms 表示 TransactionCoordinaor 等待事务状态更新的超时时间,通过生产者客户端参数 transaction.timeout.ms 配置,默认值为60000。




保存PID

生产者的 InitProducerIdRequest 请求会被发送给 TransactionCoordinator。注意,如果未开启事务特性而只开启幂等特性,那么 InitProducerIdRequest 请求可以发送给任意的 broker。当 TransactionCoordinator 第一次收到包含该 transactionalId 的 InitProducerIdRequest 请求时,它会把 transactionalId 和对应的 PID 以消息(我们习惯性地把这类消息称为“事务日志消息”)的形式保存到主题 __transaction_state 中,如事务流程图步骤2.1所示。这样可以保证 <transaction_Id, PID> 的对应关系被持久化,从而保证即使 TransactionCoordinator 宕机该对应关系也不会丢失。存储到主题 __transaction_state 中的具体内容格式如下图所示。




其中 transaction_status 包含 Empty(0)、Ongoing(1)、PrepareCommit(2)、PrepareAbort(3)、CompleteCommit(4)、CompleteAbort(5)、Dead(6) 这几种状态。在存入主题 __transaction_state 之前,事务日志消息同样会根据单独的 transactionalId 来计算要发送的分区,算法同代码清单14-3一样。




与 InitProducerIdRequest 对应的 InitProducerIdResponse 响应体结构如上图所示,除了返回 PID,InitProducerIdRequest 还会触发执行以下任务:

增加该 PID 对应的 producer_epoch。具有相同 PID 但 producer_epoch 小于该 producer_epoch 的其他生产者新开启的事务将被拒绝。

恢复(Commit)或中止(Abort)之前的生产者未完成的事务。

3. 开启事务

通过 KafkaProducer 的 beginTransaction() 方法可以开启一个事务,调用该方法后,生产者本地会标记已经开启了一个新的事务,只有在生产者发送第一条消息之后 TransactionCoordinator 才会认为该事务已经开启。

4. Consume-Transform-Produce

这个阶段囊括了整个事务的数据处理过程,其中还涉及多种请求。注:如果没有给出具体的请求体或响应体结构,则说明其并不影响读者对内容的理解,笔者为了缩减篇幅而将其省略。

1)AddPartitionsToTxnRequest

当生产者给一个新的分区(TopicPartition)发送数据前,它需要先向 TransactionCoordinator 发送 AddPartitionsToTxnRequest 请求(AddPartitionsToTxnRequest 请求体结构如下图所示),这个请求会让 TransactionCoordinator 将 <transactionId, TopicPartition> 的对应关系存储在主题 __transaction_state 中,如图事务流程图步骤4.1所示。有了这个对照关系之后,我们就可以在后续的步骤中为每个分区设置 COMMIT 或 ABORT 标记,如事务流程图步骤5.2所示。




如果该分区是对应事务中的第一个分区,那么此时TransactionCoordinator还会启动对该事务的计时。

2)ProduceRequest

这一步骤很容易理解,生产者通过 ProduceRequest 请求发送消息(ProducerBatch)到用户自定义主题中,这一点和发送普通消息时相同,如事务流程图步骤4.2所示。和普通的消息不同的是,ProducerBatch 中会包含实质的 PID、producer_epoch 和 sequence number。

3)AddOffsetsToTxnRequest

通过 KafkaProducer 的 sendOffsetsToTransaction() 方法可以在一个事务批次里处理消息的消费和发送,方法中包含2个参数:Map<TopicPartition, OffsetAndMetadata> offsets 和 groupId。这个方法会向 TransactionCoordinator 节点发送 AddOffsetsToTxnRequest 请求(AddOffsetsToTxnRequest 请求体结构如下图所示),TransactionCoordinator 收到这个请求之后会通过 groupId 来推导出在 __consumer_offsets 中的分区,之后 TransactionCoordinator 会将这个分区保存在 __transaction_state 中,如事务流程图步骤4.3所示。




4)TxnOffsetCommitRequest

这个请求也是 sendOffsetsToTransaction() 方法中的一部分,在处理完 AddOffsetsToTxnRequest 之后,生产者还会发送 TxnOffsetCommitRequest 请求给 GroupCoordinator,从而将本次事务中包含的消费位移信息 offsets 存储到主题 __consumer_offsets 中,如事务流程图步骤4.4所示。

5. 提交或者中止事务

一旦数据被写入成功,我们就可以调用 KafkaProducer 的 commitTransaction() 方法或 abortTransaction() 方法来结束当前的事务。

1)EndTxnRequest 无论调用 commitTransaction() 方法还是 abortTransaction() 方法,生产者都会向 TransactionCoordinator 发送 EndTxnRequest 请求(对应的 EndTxnRequest 请求体结构如下图所示),以此来通知它提交(Commit)事务还是中止(Abort)事务。




TransactionCoordinator 在收到 EndTxnRequest 请求后会执行如下操作:

将 PREPARE_COMMIT 或 PREPARE_ABORT 消息写入主题 __transaction_state,如事务流程图步骤5.1所示。

通过 WriteTxnMarkersRequest 请求将 COMMIT 或 ABORT 信息写入用户所使用的普通主题和 __consumer_offsets,如事务流程图步骤5.2所示。

将 COMPLETE_COMMIT 或 COMPLETE_ABORT 信息写入内部主题 __transaction_state,如事务流程图步骤5.3所示。

2)WriteTxnMarkersRequest

WriteTxnMarkersRequest 请求是由 TransactionCoordinator 发向事务中各个分区的 leader 节点的,当节点收到这个请求之后,会在相应的分区中写入控制消息(ControlBatch)。控制消息用来标识事务的终结,它和普通的消息一样存储在日志文件中,前面章节中提及了控制消息,RecordBatch 中 attributes 字段的第6位用来标识当前消息是否是控制消息。如果是控制消息,那么这一位会置为1,否则会置为0,如下图所示。




attributes 字段中的第5位用来标识当前消息是否处于事务中,如果是事务中的消息,那么这一位置为1,否则置为0。由于控制消息也处于事务中,所以attributes字段的第5位和第6位都被置为1。ControlBatch 中只有一个 Record,Record 中的 timestamp delta 字段和 offset delta 字段的值都为0,而控制消息的 key 和 value 的内容如下图所示。




就目前的 Kafka 版本而言,key 和 value 内部的 version 值都为0,key 中的 type 表示控制类型:0表示 ABORT,1表示 COMMIT;value 中的 coordinator_epoch 表示 TransactionCoordinator 的纪元(版本),TransactionCoordinator 切换的时候会更新其值。

3)写入最终的COMPLETE_COMMIT或COMPLETE_ABORT

TransactionCoordinator 将最终的 COMPLETE_COMMIT 或 COMPLETE_ABORT 信息写入主题 __transaction_state 以表明当前事务已经结束,此时可以删除主题 __transaction_state 中所有关于该事务的消息。由于主题 __transaction_state 采用的日志清理策略为日志压缩,所以这里的删除只需将相应的消息设置为墓碑消息即可

————————————————

版权声明:本文为CSDN博主「車輪の唄」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:
https://blog.csdn.net/asdfsadfasdfsa/article/details/104806981

相关推荐

驱动网卡(怎么从新驱动网卡)
驱动网卡(怎么从新驱动网卡)

网卡一般是指为电脑主机提供有线无线网络功能的适配器。而网卡驱动指的就是电脑连接识别这些网卡型号的桥梁。网卡只有打上了网卡驱动才能正常使用。并不是说所有的网卡一插到电脑上面就能进行数据传输了,他都需要里面芯片组的驱动文件才能支持他进行数据传输...

2026-01-30 00:37 liuian

win10更新助手装系统(微软win10更新助手)

1、点击首页“系统升级”的按钮,给出弹框,告诉用户需要上传IMEI码才能使用升级服务。同时给出同意和取消按钮。华为手机助手2、点击同意,则进入到“系统升级”功能华为手机助手华为手机助手3、在检测界面,...

windows11专业版密钥最新(windows11专业版激活码永久)

 Windows11专业版的正版密钥,我们是对windows的激活所必备的工具。该密钥我们可以通过微软商城或者通过计算机的硬件供应商去购买获得。获得了windows11专业版的正版密钥后,我...

手机删过的软件恢复(手机删除过的软件怎么恢复)
手机删过的软件恢复(手机删除过的软件怎么恢复)

操作步骤:1、首先,我们需要先打开手机。然后在许多图标中找到带有[文件管理]文本的图标,然后单击“文件管理”进入页面。2、进入页面后,我们将在顶部看到一行文本:手机,最新信息,文档,视频,图片,音乐,收藏,最后是我们正在寻找的[更多],单击...

2026-01-29 23:55 liuian

一键ghost手动备份系统步骤(一键ghost 备份)

  步骤1、首先把装有一键GHOST装系统的U盘插在电脑上,然后打开电脑马上按F2或DEL键入BIOS界面,然后就选择BOOT打USDHDD模式选择好,然后按F10键保存,电脑就会马上重启。  步骤...

怎么创建局域网(怎么创建局域网打游戏)

  1、购买路由器一台。进入路由器把dhcp功能打开  2、购买一台交换机。从路由器lan端口拉出一条网线查到交换机的任意一个端口上。  3、两台以上电脑。从交换机任意端口拉出网线插到电脑上(电脑设置...

精灵驱动器官方下载(精灵驱动手机版下载)

是的。驱动精灵是一款集驱动管理和硬件检测于一体的、专业级的驱动管理和维护工具。驱动精灵为用户提供驱动备份、恢复、安装、删除、在线更新等实用功能。1、全新驱动精灵2012引擎,大幅提升硬件和驱动辨识能力...

一键还原系统步骤(一键还原系统有哪些)

1、首先需要下载安装一下Windows一键还原程序,在安装程序窗口中,点击“下一步”,弹出“用户许可协议”窗口,选择“我同意该许可协议的条款”,并点击“下一步”。  2、在弹出的“准备安装”窗口中,可...

电脑加速器哪个好(电脑加速器哪款好)

我认为pp加速器最好用,飞速土豆太懒,急速酷六根本不工作。pp加速器什么网页都加速,太任劳任怨了!以上是个人观点,具体性能请自己试。ps:我家电脑性能很好。迅游加速盒子是可以加速电脑的。因为有过之...

任何u盘都可以做启动盘吗(u盘必须做成启动盘才能装系统吗)

是的,需要注意,U盘的大小要在4G以上,最好是8G以上,因为启动盘里面需要装系统,内存小的话,不能用来安装系统。内存卡或者U盘或者移动硬盘都可以用来做启动盘安装系统。普通的U盘就可以,不过最好U盘...

u盘怎么恢复文件(u盘文件恢复的方法)

开360安全卫士,点击上面的“功能大全”。点击文件恢复然后点击“数据”下的“文件恢复”功能。选择驱动接着选择需要恢复的驱动,选择接入的U盘。点击开始扫描选好就点击中间的“开始扫描”,开始扫描U盘数据。...

系统虚拟内存太低怎么办(系统虚拟内存占用过高什么原因)

1.检查系统虚拟内存使用情况,如果发现有大量的空闲内存,可以尝试释放一些不必要的进程,以释放内存空间。2.如果系统虚拟内存使用率较高,可以尝试增加系统虚拟内存的大小,以便更多的应用程序可以使用更多...

剪贴板权限设置方法(剪贴板访问权限)
剪贴板权限设置方法(剪贴板访问权限)

1、首先打开iphone手机,触碰并按住单词或图像直到显示选择选项。2、其次,然后选取“拷贝”或“剪贴板”。3、勾选需要的“权限”,最后选择开启,即可完成苹果剪贴板权限设置。仅参考1.打开苹果手机设置按钮,点击【通用】。2.点击【键盘】,再...

2026-01-29 21:37 liuian

平板系统重装大师(平板重装win系统)

如果你的平板开不了机,但可以连接上电脑,那就能好办,楼主下载安装个平板刷机王到你的个人电脑上,然后连接你的平板,平板刷机王会自动识别你的平板,平板刷机王上有你平板的我刷机包,楼主点击下载一个,下载完成...

联想官网售后服务网点(联想官网售后服务热线)

联想3c服务中心是联想旗下的官方售后,是基于互联网O2O模式开发的全新服务平台。可以为终端用户提供多品牌手机、电脑以及其他3C类产品的维修、保养和保险服务。根据客户需求层次,联想服务针对个人及家庭客户...