通过消息队列实现数据同步

一、要通过消息队列实现数据同步,可以按照以下步骤进行操作:

  1. 系统A产生需要同步的数据,将数据放入消息队列中。
  2. 消息队列中的数据被系统B消费并处理。
  3. 系统B将处理结果反馈给消息队列中。
  4. 系统A从消息队列中获取反馈结果,并进行后续处理。

        实现这个过程时,要注意以下几点:

  1. 确认消息队列选择:根据业务需求,选择适合的消息队列服务,比如RabbitMQ、Kafka等。
  2. 设置消息格式:在发送消息时,需要定义消息格式,包括消息体和消息头部分,以便接收方解析消息并做出正确的处理。
  3. 处理消息:系统B对接收到的消息进行处理,并生成处理结果返回给消息队列。
  4. 内容检查:在处理消息之前,要对消息内容做必要的检查,确保数据的格式正确,防止因为错误数据导致的系统故障。
  5. 监测处理时间: 消息生产者应该持续监测消耗者的处理时间,避免消费者堆积消息而使得处理时间越来越长。

二、消息队列中间件的区别

        RabbitMQ、Kafka、ActiveMQ和RocketMQ都是常见的消息队列中间件,它们都通过异步传输消息来提高应用程序之间的解耦性,同时还可以通过缓存消息减轻系统瓶颈带来的负面影响。它们各有特点,下面从使用场景、架构模型、性能表现和可靠性四个方面进行比较:

        使用场景

  • RabbitMQ:适合处理高流量、低延迟的任务,支持 AMQP 协议,用户较多,包括云计算、金融、电子商务、游戏等领域的应用。
  • Kafka:适合处理大规模的数据同步、离线处理和实时流式处理,支持高吞吐率的分布式处理,主要用户为大数据处理领域。
  • ActiveMQ:适合处理 JMS 消息,支持多种消息协议,包括 OpenWire、Stomp、AMQP 等,用户适用范围广,包括金融、医疗、电子商务等领域的应用。
  • RocketMQ:适合于大规模异构分布式系统之间的消息通信、数据同步、流式处理等场景,其适用于微服务架构、秒杀、物联网、在线教育等领域应用。

    架构模型
  • RabbitMQ:使用Erlang语言编写,采用AMQP协议作为消息传输协议,支持多种交换机类型。
  • Kafka:采用Scala语言编写,运行在JVM上,是一个分布式、可复制、高可靠的分布式消息系统。
  • ActiveMQ:基于Java消息服务(JMS)规范开发,提供了多种消息模型,支持高可用的主备复制架构以及数据集群。
  • RocketMQ:使用Java语言编写,可以扩展成一个分布式集群,提供支持顺序消息和事务消息等功能。

    性能表现
  • RabbitMQ:吞吐量较高,延迟较低,支持持久化消息,可靠性较高。
  • Kafka:吞吐量非常高,能够处理 PB 级别的数据,延迟非常低,但需要专业的运维技术,配置也比较复杂。
  • ActiveMQ:吞吐量和延迟较为平衡,但是在大流量并发情况下性能会受影响。
  • RocketMQ:吞吐量很高,稳定性与可靠性也非常好,而且对于异构系统之间的消息通信,支持分布式事务消息。

    可靠性
  • RabbitMQ:支持多节点复制和持久化消息,主备切换速度较快,可靠性高。
  • Kafka:采用分区和副本机制,各个副本之间进行同步,支持捕获消息的最终状态,可靠性非常高。
  • ActiveMQ:主从架构,有备份节点可以接管,主节点故障时可以自动恢复,保证可靠性。
  • RocketMQ:支持多种消息模式和事务操作,消息可靠性较高,存储消息的机制可以避免数据丢失。同时 RocketMQ 还提供了大量的监控和报警机制,可以在故障发生时及时通知管理员。
名称RabbitMQKafkaActiveMQRocketMQ
使用场景分布式系统、消息队列应用、大规模传输大数据实时处理平台、日志收集和分析支持多种通信协议(包括JMS)、Java应用程序、企业级应用集成、异步通信消息引擎、具备大容量、高可靠、低延迟的特点,可以满足分布式系统响应速度要求高的应用程序场景
架构模型AMQP、主从集群、消息确认机制基于发布/订阅模式,分布式集群、高可用、高吞吐、零拷贝技术JMS、面向消息中间件、支持多种语言接入基于Master-Slave架构,多线程消费,无依赖性
性能表现单机处理能力相对较低、适合于复杂的路由、安全认证、灵活性强吞吐量极高,在处理PB级别数据时仍能保持较高的吞吐量、存储效率很高负载均衡性好、性能稳定但有瓶颈,只能水平扩展,中等吞吐量线性扩展、适合于吞吐量高的场景,快速响应低延迟的消息处理
工具RabbitMQKafkaActiveMQRocketMQ
使用场景适合需要精细控制消息处理流程、任务队列等适合高吞吐量的离线数据处理、实时流处理与分发等适合JMS支持和传统企业应用集成适合互联网金融、电商、物流等对消息一致性要求较高
架构模型基于AMQP协议,采用broker(中间件服务器)、producer(发送者)、consumer(消费者)三部分基于发布/订阅模式的消息系统,如Distributed Log,由producer、broker、consumer等组成基于JMS规范的java消息服务,点对点和发布订阅两种消息模型,分为producer、broker、consumer三部分基于mq事务传输方式,具有多语言和丰富的api支持,更加轻量级的消费队列架构
性能表现RabbitMQ提供高可用、稳定、可靠的消息中间件支持,但吞吐量相对其他三款产品较慢Kafka具有出色的扩展性和高吞吐量、低延迟的优势,但是在一些场景下消息不可靠ActiveMQ在大数据处理、多通道消息传递方面表现较佳,在吞吐量方面略有欠缺RocketMQ高吞吐量、低延迟、高可用性且支持海量堆积与保障数据安全等方面具有优势

综上所述,不同的消息队列中间件有不同的使用场景和特点,选取适合的中间件可以提高系统的性能和可靠性,但也需要根据具体的业务需求进行选择。

三、消息队列如何解决消息丢失、消息重复消费

        消息队列在保证高可用和高并发的前提下,也需要保证消息发送和消费的准确性,这就需要解决消息丢失和重复消费的问题。具体方法如下:

        消息丢失

  • 消息持久化:当消息发送到消息队列时,可以设置消息为持久化消息。此时消息会存储在磁盘上,即使消息队列宕机,也能够通过消息的恢复来避免消息的丢失。
  • 同步复制:对于一些关键的消息,可以采用同步复制的方式进行处理。即发送消息后,只有在所有副本节点都确认接收后才返回发送成功。这种方式的可靠性非常高,但是由于需要等待所有副本节点响应,会降低系统的吞吐量。
  • 异步复制:异步复制是指只要主节点发送了消息,就认为消息发送成功,然后异步复制到备份节点。虽然可以提高系统的吞吐量,但是在出现节点故障的情况下,可能导致一部分消息的丢失。

    消息重复消费
  • 消费幂等性:为了避免消息被重复消费,需要保证消息的幂等性。即多次消费同一条消息,最终保持的结果是一致的。消费幂等性可以通过设置消息的唯一标识符进行实现。
  • 消费者分组:为了避免重复消费,可以将不同的消费者分组。即同一个消费组内的消费者只会消费其中的一条消息。当有多个消费者需要消费同一条消息时,只有其中一个消费者能够消费该消息。
  • 消息确认机制:消息队列可以提供消息确认机制。在消费者消费完消息后,需要向消息队列发送一个确认消息。消息队列收到确认消息后,就会删除该条消息,防止消息被重复消费。

        其中消息确认机制和消息持久化机制如下:

        消息确认机制

        消息确认机制:指在消费者接收到消息后,对消息进行确认操作。大部分消息队列系统都支持消息确认机制。

  • 在 RabbitMQ 中有 "basic.ack" 和 "basic.nack" 命令,当消费者成功处理消息后,需要发送 "basic.ack" 命令给 RabbitMQ 表示已经确认;
  • 在 Kafka 中,在消费者处理完消息后,会将其偏移量 offset 提交给 Kafka 集群。如果提交失败,则说明没有正确消费该消息。为了保证消息不被重复消费,Kafka 中消费者可以设置自动提交或手动提交两种模式,默认为自动提交;
  • 在 ActiveMQ 和 RocketMQ 中,均提供了可靠的消息投递方式,当消费者成功处理消息后,需要向 ActiveMQ 或 RocketMQ 服务端发送回执,表示已经成功消费。

        消息持久化

        在消息队列中,消息持久化即一个消费者成功处理某个消息后,该消息应该被持久化地存储在消息队列中,以防止该消息丢失的情况出现。各消息队列系统在消息持久化方面也都提供了相关的机制。

        在 RabbitMQ 中,通过设置消息的持久化级别来实现消息持久化。在发送消息时,可以指定消息的持久化级别。当你把消息设置为持久化时,消息会优先保存到磁盘上,而不是RAM,以防止在故障时丢失消息。如果未启用持久化,则消息仅保存在RAM中,并且在服务器关闭或发生故障时会丢失。 要启用持久化,可以在发布消息时将delivery_mode设置为2。

        Kafka 是一个分布式的消息队列系统,它通过一些机制来实现消息持久化:
        日志文件存储: Kafka 使用一系列日志文件来持久化数据。每个主题都由一个或多个分区组成,每个分区中的消息会被追加到该分区对应的日志文件中,这样即使发生故障也不会丢失数据。 零拷贝技术: Kafka 使用了操作系统级别的零拷贝技术,将数据在内存和磁盘之间复制的次数降到最少。当消费者从 Kafka 集群拉取消息时,它们可以直接读取磁盘上的数据,而无需将其复制到另一个缓冲区中。

        在 ActiveMQ 中,消息的持久化是通过 KahaDB 存储机制来实现的。KahaDB 是 ActiveMQ 自带的数据存储引擎,使用 B-Tree 索引结构和毫秒级别的编码支持持久化存储和高速读取。当消息被发送到 ActiveMQ 的队列或主题时,它们首先被存储在内存中,并写入调度日志(Journal),然后再异步地将其刷新到磁盘上。如果 broker(代理服务器)重启或崩溃,ActiveMQ 可以从磁盘中恢复未被消费的消息,并重新投递给消费者进行处理。

        RocketMQ 中的消息持久化依赖于 commit log(提交日志)机制。当消息被发送到 RocketMQ 时,它们首先被写入 commit log 文件,包括消息的索引、主体和属性等信息。之后,消息会被放入内存中的映射文件(Mapped File)和索引文件(Index File)。如果 broker(代理服务器)重启或崩溃,RocketMQ 会从 commit log 文件中恢复未被消费的消息,并重新进行消费。        

        在 RocketMQ 中,消息的存储结构包含三个文件:commit log、consume queue 和 index。其中,commit log 是存储所有消息的地方,consume queue 存储了每个消费者的位置信息和已消费的消息 ID,而 index 记录了特定主题或队列的索引信息,可以将消息检索到指定的 commit log 文件中,实现 RocketMQ 的高效读写操作。

        综上所述,消息队列在解决消息丢失和重复消费问题时,可以采用多种方式进行处理,需要根据具体的业务需求进行选择。同时,在实际开发中,还需要对消息队列进行监控和报警,及时发现问题并进行处理。

四、消息队列以外的消息丢失、消息重复消费问题

        既然消息队列已经解决了消息丢失和重复消费问题,那使用消息队列还会出现消息丢失、消息重复消费的问题吗?答案是会。

        使用消息队列,仍然可能出现消息丢失和重复消费的问题。这取决于具体实现细节和系统设计。

        消息丢失

        消息丢失通常发生在消息发布时,因为生产者在发送消息到队列之前可能会发生一些错误,例如网络中断或代码异常。如果生产者无法正确地将消息发送到队列,则该消息将永远丢失,除非您使用持久化队列,并将消息保存到磁盘上。

        此外,消费者也可能在处理消息时出错,这会导致消息丢失。例如,如果您的消息消费者崩溃并重新启动,则它可能会从头开始重新处理所有消息,这意味着某些消息将不再被处理。

        消息重复消费

        另一个常见的问题是消息重复处理。这通常发生在消费者处理完消息后未正确确认消息已被处理,而是在超时时间内进行了再次处理。当多个消费者从同一队列中读取消息时,重复消费的问题可能会更加明显。为了解决此类问题,您可以使用幂等¥,确保处理消息的操作可以重复执行而不会引起问题。

        总之,在使用消息队列时,您需要考虑各种情况和风险,并采取措施来应对这些问题。

五、消息发布时出现的消息丢失问题,有什么解决办法?

        消息丢失是消息队列应用中比较常见的问题,通常是由于发送端未正确配置或者网络传输等原因导致。下面介绍一些解决消息丢失问题的方法:

        消息持久化

        消息持久化是指将消息保存在持久存储器中,即使消息中间件宕机,也可以将消息恢复到消息中间件中。这样可以保证消息的可靠性和不被丢失。在消息发送时,需要设置消息为持久化消息,这样可以保证消息在中间件宕机后不会被丢失。许多消息中间件支持消息持久化,例如RabbitMQ、Kafka等。

        报告消息发送失败

        在消息发送过程中,可以使用回调函数或者其他方式来报告消息发送失败的情况。当消息发送失败时,可以根据错误类型来识别问题所在,并及时进行处理。对于非常关键的消息,还可以通过消息重试机制来保证消息能够成功发送。

        发送确认机制

        发送确认机制是指在消息发送之后,由消息中间件向消息生产者返回一个确认消息。消息生产者可以根据确认消息来判断是否成功发送消息。如果没有收到确认消息,则可以尝试重新发送消息。很多消息中间件都提供了发送确认机制,例如ActiveMQ、RabbitMQ等。

        使用事务机制

        事务机制是指在消息发送前开启一个事务,在事务中发送消息,然后提交或回滚该事务。只有在事务被成功提交后,才会将消息发送到消息中间件。如果在事务过程中发生了错误,可以进行回滚操作,从而避免了消息发送失败的情况。很多消息中间件都提供了事务机制,例如RocketMQ等。

        综上所述,为避免消息丢失问题,可以采用多种手段,需要根据实际业务需求选择合适的方法。同时,在编写代码时,需要编写清晰的异常处理和重试机制,及时发现问题并进行处理。

        不同的解决方案具有不同的特点,适用于不同的场景。下面是各种解决方案适用的场景:

        消息持久化

        适用于对数据可靠性有较高要求的场景。消息持久化可以将消息存储在持久化存储器中,即使消息中间件宕机,也可以将消息恢复到消息中间件中。这样可以保证消息的可靠性和不被丢失。例如,金融支付、电商下单等场景需要保证消息的可靠性,可以采用消息持久化方式。

        报告消息发送失败

        适用于需要实时处理错误的场景。当消息发送失败时,可以根据错误类型来识别问题所在,并及时进行处理。例如,电商退货流程中,如果消息发送失败,可以利用回调函数及时提示用户并解决相关问题。

        发送确认机制

        适用于需要保证消息发送成功的场景。发送确认机制是指在消息发送之后,由消息中间件向消息生产者返回一个确认消息。消息生产者可以根据确认消息来判断是否成功发送消息。如果没有收到确认消息,则可以尝试重新发送消息。例如,订单系统中需要保证订单信息能够成功发送到仓库系统中,可以采用发送确认机制。

        使用事务机制

        适用于对消息顺序、数据完整性有较高要求的场景。在事务中发送消息,然后提交或回滚该事务。只有在事务被成功提交后,才会将消息发送到消息中间件。如果在事务过程中发生了错误,可以进行回滚操作,从而避免了消息发送失败的情况。例如,飞机航班预订系统中需要保证订单信息的数据完整性和合法性,可以采用事务机制。

        总之,对于不同的业务场景,可以根据实际需求选择合适的解决方案来保证消息的可靠性和准确性。同时,在实际运维中,还需要对消息队列进行监控和报警,及时发现问题并进行处理。

六、消费者可能在处理消息时出错,导致消息丢失,有什么解决办法?

        针对消费者在处理消息时出错可能会导致消息丢失的问题,以下是一些解决办法:

  1. 确认机制(Acknowledgement mechanism):消费者收到消息之后,将会向生产者发送一个确认回执(ACK),告诉它已经成功接收并处理了该消息。如果生产者没有收到 ACK 或者在一段时间内没有收到 ACK,就会认为发送失败,然后进行重试。

  2. 重试机制(Retrying mechanism):如果出现了错误,可以使用重试机制。当消费者无法处理消息时,可以将该消息放回队列末尾,以便稍后再次处理。一些消息队列服务商会自动提供重试机制。

  3. 持久化机制(Persistence mechanism):消费者可能由于某些网络或硬件错误而在处理消息时失败,但是这个错误不意味着消息不应该被处理。持久化机制可以确保消息在存储中,即使消费者宕机也能够重新启动,并恢复未处理的消息。

  4. 监控机制(Monitoring mechanism):建立监控机制可以帮助发现和解决消息丢失的问题,以及其他与消息相关的问题。可以使用监控工具或手动检查日志来跟踪所有传入和传出的消息,从而确定何时丢失消息。

  5. 事务机制(Transaction mechanism):如果您没有使用消息队列,则可能需要考虑使用事务机制来确保成功处理消息。这通常通过提交/回滚方法实现,以便在发生错误时可以回滚。

        需要注意的是,解决方案应该结合您所使用的消息队列服务商以及设计并实施适当的错误处理策略。

七、使用事务:可以将消息发送操作与提交操作封装在同一事务中,以确保消息成功地发出并已保存到队列中

下面是使用 Spring 的 JmsTemplate 进行发送消息的 Java 代码示例,该示例将消息发送操作和提交操作封装在同一事务中。如果发生任何异常,则会回滚事务并重新尝试发送消息:

import org.springframework.jms.core.JmsTemplate;
import org.springframework.transaction.annotation.Transactional;

@Transactional
public class MessageSender {
    private JmsTemplate jmsTemplate;

    public void setJmsTemplate(JmsTemplate jmsTemplate) {
        this.jmsTemplate = jmsTemplate;
    }

    public void sendMessage(String message) {
        try {
            jmsTemplate.convertAndSend(message);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

上述代码使用 @Transactional 注解声明一个事务,并在类中定义了一个 JmsTemplate 对象来发送消息。sendMessage() 方法将消息作为字符串参数传入,然后使用 jmsTemplate.convertAndSend() 方法将消息发送到队列中。如果发送消息时发生任何异常,则会抛出一个运行时异常,并自动回滚事务。否则,事务会自动提交,确保消息成功地保存在队列中。

这是一个简单的示例,具体实现可能因为不同的 JMS 提供者而有所不同,但是这个基本思路应该可以适用于大多数事务性消息传递场景。

八、消息发布时出现的消息丢失问题,使用发送确认机制解决

下面是使用发送确认机制解决消息丢失问题的 Java 代码示例:

@Component
public class MessageProducer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendMessage(String exchange, String routingKey, Object message) {
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        rabbitTemplate.convertAndSend(exchange, routingKey, message, correlationData);
    }

    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            if (ack) {
                System.out.println("消息已正确投递到队列:" + correlationData.getId());
            } else {
                System.out.println("消息投递失败:" + correlationData.getId() + ",原因:" + cause);
            }
        });
    }
}

在上述代码中,我们使用 Spring Boot 的 RabbitTemplate 对象来发送消息,并启用了发送确认机制。当消息发送成功时,会执行 setConfirmCallback 方法中的 ack 为 true 的回调函数,从而完成消息投递的确认。如果消息发送失败,会执行 ack 为 false 的回调函数,并打印错误信息。

在使用该 MessageProducer 类发送消息时,只需要调用 sendMessage 方法即可。例如:

// 发送消息

messageProducer.sendMessage("exchange_name", "routing_key", "Hello World");

这样,在实际运行中,就可以保证消息发送的可靠性和稳定性,并及时处理可能发生的异常情况,保证消息不会丢失。

九、重试机制

        下面是使用重试机制解决消息消费失败的 Java 代码示例:

// 自定义消息处理器
@Component
public class MessageHandler {

    // 最大重试次数
    private static final int MAX_RETRIES = 3;

    // 重试间隔时间(毫秒)
    private static final long RETRY_INTERVAL = 5000L;

    @Retryable(value = {Exception.class}, maxAttempts = MAX_RETRIES, backoff = @Backoff(delay = RETRY_INTERVAL))
    public void handleMessage(Message message) throws Exception {
        // 处理消息
        String content = new String(message.getBody(), "UTF-8");
        System.out.println("收到消息:" + content);
        // 模拟消息处理失败
        throw new Exception("消息处理失败");
    }

    @Recover
    public void recover(Exception e, Message message) {
        System.out.println("重试 " + MAX_RETRIES + " 次后仍然无法处理消息,放弃处理该消息:" + new String(message.getBody()));
    }
}

// 配置类
@Configuration
@EnableRetry
public class AppConfig {

    @Autowired
    private MessageHandler messageHandler;

    @Bean
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setConcurrentConsumers(1);
        factory.setMaxConcurrentConsumers(1);
        return factory;
    }

    @Bean
    public MessageListenerAdapter listenerAdapter() {
        MessageListenerAdapter adapter = new MessageListenerAdapter(messageHandler);
        adapter.setDefaultListenerMethod("handleMessage");
        adapter.setMessageConverter(new Jackson2JsonMessageConverter());
        return adapter;
    }

    @Bean
    public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter) {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.setQueueNames("queue_name");
        container.setMessageListener(listenerAdapter);
        container.setConcurrentConsumers(1);
        container.setMaxConcurrentConsumers(1);
        return container;
    }
}

        在上述代码中,我们定义了一个自定义的消息处理器 MessageHandler,并在其中使用了 Spring Retry 框架提供的 @Retryable 和 @Recover 注解来实现重试机制。当消息处理失败时,会抛出异常并触发重试机制,最多重试 MAX_RETRIES 次(本例中为 3 次),每次重试的时间间隔为 RETRY_INTERVAL(本例中为 5000 毫秒)。

        在配置类中,我们将 messageHandler 对象作为 MessageListenerAdapter 的处理器,并通过 SimpleMessageListenerContainer 对象启动消息监听器,监听名为 "queue_name" 的 RabbitMQ 队列。当队列中有消息发送过来时,会触发 messageHandler 对象中的 handleMessage 方法进行消息处理,如果处理失败会触发重试机制,直至消息处理成功或达到最大重试次数后放弃处理该消息。

        以下是使用 Spring Boot 发送消息到 RabbitMQ 队列中的 Java 代码实现细节:

@Component
public class MessageProducer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendMessage(String exchange, String routingKey, Object message) {
        // convertAndSend 方法会将消息序列化为 byte 数组,并将其发送到指定 exchange 和 routingKey 所对应的队列中
        rabbitTemplate.convertAndSend(exchange, routingKey, message);
        System.out.println("已成功发送消息:" + message);
    }
}

在上述代码中,我们定义了一个 MessageProducer 类,用于发送消息到指定的 RabbitMQ 队列中。在 sendMessage 方法中,我们使用了 Spring Boot 提供的 RabbitTemplate 对象,通过调用其中的 convertAndSend 方法来发送消息。该方法会将消息序列化为 byte 数组,并将其发送到指定的 exchange 和 routingKey 所对应的队列中。

最后,在 sendMessage 方法中打印日志,以便确认消息已经成功发送到队列中。

当消费者从队列中接收到消息时,会触发 messageHandler 对象中的 handleMessage 方法进行消息处理。如果处理失败,则会触发重试机制,最多重试 MAX_RETRIES 次(本例中为 3 次),每次重试间隔时间为 RETRY_INTERVAL(本例中为 5000 毫秒)。当达到最大重试次数后,会放弃处理该消息。

在实际应用中,要根据具体业务需求选择合适的重试次数和时间间隔,并在消息处理失败时抛出异常以触发重试机制。另外,还需要确保消费者在处理消息时具有幂等性,以避免因重试机制而导致消息重复消费的问题。

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值