在 Spring Boot 中使用 Kafka 并保证顺序性(Topic 分区为 100)的完整案例

1. 项目依赖

xml

<!-- pom.xml -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.kafka</groupId>
        <artifactId>spring-kafka</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

2. 配置文件

yaml

# application.yml
spring:
  kafka:
    bootstrap-servers: localhost:9092
    
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
      acks: all
      retries: 3
      batch-size: 16384
      buffer-memory: 33554432
      properties:
        enable.idempotence: true # 启用幂等性
        max.in.flight.requests.per.connection: 1 # 关键:保证顺序性的配置
    
    consumer:
      group-id: order-group
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      enable-auto-commit: false # 手动提交offset
      auto-offset-reset: earliest
      properties:
        isolation.level: read_committed # 读取已提交的消息

    listener:
      ack-mode: manual_immediate # 手动提交
      concurrency: 1 # 每个分区一个消费者实例(重要!)

3. 生产者配置类

java

import org.apache.kafka.clients.producer.ProducerConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class KafkaProducerConfig {
    
    @Bean
    public ProducerFactory<String, String> producerFactory() {
        Map<String, Object> configProps = new HashMap<>();
        configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, 
                       "org.apache.kafka.common.serialization.StringSerializer");
        configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, 
                       "org.apache.kafka.common.serialization.StringSerializer");
        
        // 关键配置:保证顺序性
        configProps.put(ProducerConfig.ACKS_CONFIG, "all"); // 确保消息被所有副本接收
        configProps.put(ProducerConfig.RETRIES_CONFIG, 3);
        configProps.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 1);
        configProps.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
        
        // 性能优化
        configProps.put(ProducerConfig.LINGER_MS_CONFIG, 5);
        configProps.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
        configProps.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "snappy");
        
        return new DefaultKafkaProducerFactory<>(configProps);
    }
    
    @Bean
    public KafkaTemplate<String, String> kafkaTemplate() {
        return new KafkaTemplate<>(producerFactory());
    }
}

4. 顺序消息生产者

java

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.stereotype.Component;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;

@Component
@Slf4j
public class OrderProducer {
    
    private static final String TOPIC = "order-topic";
    
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;
    
    /**
     * 发送顺序消息
     * @param orderId 订单ID - 作为partition key,保证相同订单的消息进入同一分区
     * @param message 消息内容
     * @param messageSeq 消息序列号(用于顺序验证)
     */
    public void sendOrderedMessage(String orderId, String message, int messageSeq) {
        // 使用订单ID作为key,确保相同订单的消息进入同一个分区
        String key = orderId;
        String value = String.format("seq-%d|%s", messageSeq, message);
        
        // 发送消息
        ListenableFuture<SendResult<String, String>> future = 
                kafkaTemplate.send(TOPIC, key, value);
        
        // 异步回调
        future.addCallback(new ListenableFutureCallback<SendResult<String, String>>() {
            @Override
            public void onSuccess(SendResult<String, String> result) {
                log.info("发送成功: orderId={}, seq={}, partition={}, offset={}", 
                        orderId, messageSeq, 
                        result.getRecordMetadata().partition(),
                        result.getRecordMetadata().offset());
            }
            
            @Override
            public void onFailure(Throwable ex) {
                log.error("发送失败: orderId={}, seq={}, error={}", 
                        orderId, messageSeq, ex.getMessage());
                // 这里可以添加重试逻辑
                retrySend(orderId, value);
            }
        });
    }
    
    /**
     * 批量发送订单的多个步骤(保证顺序)
     */
    public void sendOrderProcess(String orderId) {
        // 模拟订单处理流程,这些消息必须按顺序消费
        sendOrderedMessage(orderId, "订单创建", 1);
        sendOrderedMessage(orderId, "支付成功", 2);
        sendOrderedMessage(orderId, "库存扣减", 3);
        sendOrderedMessage(orderId, "订单发货", 4);
        sendOrderedMessage(orderId, "订单完成", 5);
    }
    
    private void retrySend(String key, String value) {
        // 实现重试逻辑
        // 注意:重试时要保持相同的key才能保证进入同一分区
    }
    
    /**
     * 同步发送(确保消息发送成功)
     */
    public SendResult<String, String> sendSync(String orderId, String message, int seq) 
            throws Exception {
        String key = orderId;
        String value = String.format("seq-%d|%s", seq, message);
        
        return kafkaTemplate.send(TOPIC, key, value).get();
    }
}

5. 消费者配置类

java

import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class KafkaConsumerConfig {
    
    @Bean
    public ConsumerFactory<String, String> consumerFactory() {
        Map<String, Object> props = new HashMap<>();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        props.put(ConsumerConfig.GROUP_ID_CONFIG, "order-consumer-group");
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, 
                 "org.apache.kafka.common.serialization.StringDeserializer");
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, 
                 "org.apache.kafka.common.serialization.StringDeserializer");
        
        // 关键配置:保证顺序消费
        props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 1); // 每次只拉取一条消息
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); // 手动提交
        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
        props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed");
        
        return new DefaultKafkaConsumerFactory<>(props);
    }
    
    @Bean
    public ConcurrentKafkaListenerContainerFactory<String, String> 
            kafkaListenerContainerFactory() {
        
        ConcurrentKafkaListenerContainerFactory<String, String> factory =
                new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(consumerFactory());
        
        // 重要:每个分区一个消费者线程
        // 100个分区需要配置足够的并发数
        factory.setConcurrency(100); // 与分区数匹配
        
        // 手动提交模式
        factory.getContainerProperties().setAckMode(
                org.springframework.kafka.listener.ContainerProperties.AckMode.MANUAL_IMMEDIATE);
        
        // 批量消费关闭
        factory.setBatchListener(false);
        
        return factory;
    }
}

6. 顺序消息消费者

java

import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component
@Slf4j
public class OrderConsumer {
    
    // 用于跟踪每个订单的消费序列号
    private final Map<String, Integer> orderSeqTracker = new ConcurrentHashMap<>();
    
    /**
     * 监听订单Topic的100个分区
     */
    @KafkaListener(
            topics = "order-topic",
            groupId = "order-consumer-group",
            containerFactory = "kafkaListenerContainerFactory"
    )
    public void consumeOrderMessage(ConsumerRecord<String, String> record, 
                                   Acknowledgment ack) {
        try {
            String orderId = record.key();
            String value = record.value();
            
            // 解析消息序列号
            String[] parts = value.split("\\|", 2);
            int currentSeq = Integer.parseInt(parts[0].replace("seq-", ""));
            String message = parts.length > 1 ? parts[1] : "";
            
            // 获取该订单的上一个序列号
            Integer lastSeq = orderSeqTracker.get(orderId);
            
            // 顺序验证
            if (lastSeq != null && currentSeq != lastSeq + 1) {
                log.warn("消息顺序错误! orderId={}, 期望seq={}, 实际seq={}, 等待重试...", 
                        orderId, lastSeq + 1, currentSeq);
                
                // 不提交ack,等待重新消费
                // 注意:这里可能会导致消息重复消费,需要业务层做幂等处理
                return;
            }
            
            // 处理业务逻辑
            processOrderMessage(orderId, currentSeq, message);
            
            // 更新序列号
            orderSeqTracker.put(orderId, currentSeq);
            
            // 手动提交offset
            ack.acknowledge();
            
            log.info("消费成功: orderId={}, seq={}, partition={}, offset={}", 
                    orderId, currentSeq, record.partition(), record.offset());
            
        } catch (Exception e) {
            log.error("消费失败: partition={}, offset={}, error={}", 
                    record.partition(), record.offset(), e.getMessage());
            // 根据业务需求决定是否提交offset
        }
    }
    
    private void processOrderMessage(String orderId, int seq, String message) {
        // 业务处理逻辑
        log.info("处理订单消息: orderId={}, seq={}, message={}", orderId, seq, message);
        
        // 模拟业务处理
        switch (seq) {
            case 1:
                // 创建订单
                break;
            case 2:
                // 处理支付
                break;
            case 3:
                // 扣减库存
                break;
            case 4:
                // 发货处理
                break;
            case 5:
                // 订单完成
                // 清理序列号跟踪
                orderSeqTracker.remove(orderId);
                break;
        }
    }
    
    /**
     * 用于监控消费进度
     */
    public Map<String, Integer> getOrderSeqTracker() {
        return new ConcurrentHashMap<>(orderSeqTracker);
    }
}

7. 控制器层

java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/kafka")
public class KafkaController {
    
    @Autowired
    private OrderProducer orderProducer;
    
    @Autowired
    private OrderConsumer orderConsumer;
    
    /**
     * 发送顺序消息
     */
    @PostMapping("/send-order")
    public String sendOrderMessage(@RequestParam String orderId) {
        orderProducer.sendOrderProcess(orderId);
        return "订单消息已发送: " + orderId;
    }
    
    /**
     * 批量发送测试
     */
    @PostMapping("/send-batch")
    public String sendBatchMessages(@RequestParam int count) {
        for (int i = 1; i <= count; i++) {
            String orderId = "ORDER-" + System.currentTimeMillis() + "-" + i;
            orderProducer.sendOrderProcess(orderId);
        }
        return "批量发送完成,共" + count + "个订单";
    }
    
    /**
     * 查看消费状态
     */
    @GetMapping("/status")
    public Object getConsumeStatus() {
        return orderConsumer.getOrderSeqTracker();
    }
}

8. 主应用类

java

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class KafkaOrderApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(KafkaOrderApplication.class, args);
    }
}

9. 测试类

java

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class OrderProducerTest {
    
    @Autowired
    private OrderProducer orderProducer;
    
    @Test
    void testOrderSequence() {
        // 测试顺序性
        String orderId = "TEST-ORDER-001";
        
        // 发送顺序消息
        orderProducer.sendOrderedMessage(orderId, "创建订单", 1);
        orderProducer.sendOrderedMessage(orderId, "支付", 2);
        orderProducer.sendOrderedMessage(orderId, "发货", 3);
        
        // 等待消费完成
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

10. 关键配置说明

保证顺序性的关键点:

  1. 生产者端

    • max.in.flight.requests.per.connection=1:同一连接最多只能有一个请求在途

    • acks=all:确保消息被所有副本接收

    • enable.idempotence=true:启用幂等性

    • 使用订单ID作为Key,确保相同订单进入同一分区

  2. 消费者端

    • 每个分区一个消费者实例(concurrency=100

    • 手动提交offset

    • 业务层做顺序验证和幂等处理

    • 单个消费者组内保证分区消费顺序

  3. Topic创建(需要预先创建):

bash

# 创建100个分区的Topic
kafka-topics.sh --create \
  --bootstrap-server localhost:9092 \
  --replication-factor 3 \
  --partitions 100 \
  --topic order-topic

11. 监控和管理

java

// 可选的监控组件
@Component
@Slf4j
public class KafkaMetrics {
    
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;
    
    // 定期检查生产者状态
    @Scheduled(fixedDelay = 60000)
    public void checkProducerHealth() {
        Map<Object, Object> metrics = kafkaTemplate.metrics();
        log.info("Kafka Producer Metrics: {}", metrics);
    }
}

这个方案通过 Kafka 的分区机制保证了相同订单 ID 的消息被发送到同一个分区,从而保证了顺序性。同时考虑了高并发、容错和监控等生产级需求。

<think>我们正在讨论如何在SpringBoot中向Kafkatopic推送消息。根据引用[3],我们可以使用SpringKafka提供的KafkaTemplate来发送消息。此外,引用[1]和引用[2]都提到了SpringBootKafka的内置支持,简化了配置和使用。在SpringBoot中,通常有两种主要方式来推送消息到Kafkatopic:1.使用SpringKafka提供的KafkaTemplate(推荐方式)2.直接使用KafkaProducerAPI(不常用,因为SpringKafka已经做了很好的封装)下面我们重点介绍使用KafkaTemplate的方式,因为这是SpringBoot集成Kafka的标准做法。###步骤1:添加Maven依赖首先,在pom.xml中添加SpringKafka的依赖:```xml<dependency><groupId>org.springframework.kafka</groupId><artifactId>spring-kafka</artifactId></dependency>```###步骤2:配置Kafka生产者在application.properties或application.yml中配置Kafka生产者的相关属,例如:```propertiesspring.kafka.bootstrap-servers=localhost:9092spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializerspring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer#可选:设置重试次数spring.kafka.producer.retries=3#可选:设置批量大小spring.kafka.producer.batch-size=16384#可选:设置缓冲区大小spring.kafka.producer.buffer-memory=33554432```###步骤3:使用KafkaTemplate发送消息创建一个服务类,注入KafkaTemplate,然后使用它来发送消息。示例代码:```javaimportorg.springframework.kafka.core.KafkaTemplate;importorg.springframework.stereotype.Service;@ServicepublicclassKafkaProducerService{privatefinalKafkaTemplate<String,String>kafkaTemplate;publicKafkaProducerService(KafkaTemplate<String,String>kafkaTemplate){this.kafkaTemplate=kafkaTemplate;}publicvoidsendMessage(Stringtopic,Stringmessage){kafkaTemplate.send(topic,message);//如果需要,可以添加回调/*kafkaTemplate.send(topic,message).addCallback(result->{if(result!=null){System.out.println("Sentmessage=["+message+"]withoffset=["+result.getRecordMetadata().offset()+"]");}},ex->System.out.println("Unabletosendmessage=["+message+"]dueto:"+ex.getMessage()));*/}}```在上面的代码中,我们通过`kafkaTemplate.send(topic,message)`发送消息。如果需要处理发送结果,可以添加回调。###步骤4:在控制器或业务逻辑中调用发送方法然后,在需要发送消息的地方(如控制器)注入`KafkaProducerService`调用`sendMessage`方法。示例控制器:```javaimportorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.PostMapping;importorg.springframework.web.bind.annotation.RequestParam;importorg.springframework.web.bind.annotation.RestController;@RestControllerpublicclassKafkaProducerController{@AutowiredprivateKafkaProducerServicekafkaProducerService;@PostMapping("/send")publicStringsendMessageToKafka(@RequestParam("message")Stringmessage){kafkaProducerService.sendMessage("myTopic",message);return"Messagesent:"+message;}}```###直接使用KafkaProducerAPI(不推荐)虽然也可以直接使用Kafka原生的ProducerAPI,但在SpringBoot应用中,我们通常不这么做,因为SpringKafka已经为我们封装好了线程安全的KafkaTemplate,且与Spring的生命周期管理集成。不过,如果确实需要,可以像引用[3]中的示例那样创建Producer实例,但请注意,这种方式需要手动管理Producer的创建和关闭,而且没有利用Spring的依赖注入和自动配置。引用[3]中的示例(注意:该示例中使用KafkaTemplate同时又创建了一个新的Producer,这通常不是必要且不推荐):```javapublicvoidsendMessage(Stringmessage){//注意:这里直接创建了一个KafkaProducer,但实际中我们更推荐使用注入的KafkaTemplateProducer<String,String>producer=newKafkaProducer<>(bootstrapServers,newStringSerializer(),newStringSerializer());try{producer.send(newProducerRecord<>(topic,message));}catch(Exceptione){e.printStackTrace();}finally{producer.close();}}```但是,这种方式没有利用SpringBoot的自动配置,而且每次发送消息都创建和关闭Producer效率很低。因此,我们强烈推荐使用KafkaTemplate。总结:在SpringBoot中推送消息到Kafkatopic使用KafkaTemplate是最简单、最推荐的方式。相关问题:1.如何配置SpringBoot中的Kafka生产者属?2.使用KafkaTemplate发送消息时如何确保消息发送成功?3.如何发送带key的消息到Kafkatopic?4.在SpringBoot中如何实现Kafka消息的异步发送?5.如何为KafkaTemplate配置自定义序列化器?</think>在 Spring Boot 中向 Kafka topic 推送消息主要有以下两种推荐方式,均基于 Spring Kafka 库实现: ### 1. 使用 KafkaTemplate(推荐方式)[^1][^3] 这是 Spring Kafka 提供的核心工具,简化了消息发送过程。配置步骤如下: **步骤 1:添加 Maven 依赖** ```xml <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> </dependency> ``` **步骤 2:配置生产者属** ```properties # application.properties spring.kafka.bootstrap-servers=localhost:9092 spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer ``` **步骤 3:注入使用 KafkaTemplate** ```java import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Service; @Service public class KafkaProducerService { private final KafkaTemplate<String, String> kafkaTemplate; public KafkaProducerService(KafkaTemplate<String, String> kafkaTemplate) { this.kafkaTemplate = kafkaTemplate; } public void sendMessage(String topic, String message) { kafkaTemplate.send(topic, message); // 带回调的发送 /* kafkaTemplate.send(topic, message).addCallback( result -> System.out.println("发送成功: " + message), ex -> System.err.println("发送失败: " + ex.getMessage()) ); */ } } ``` **特点:** - 自动管理连接池和资源 - 支持同步/异步发送 - 内置消息序列化 - 支持事务消息[^3] ### 2. 使用 KafkaProducer API(原生方式) 直接使用 Kafka 原生 API,适用于需要精细控制的场景[^3]: ```java import org.apache.kafka.clients.producer.*; import javax.annotation.PreDestroy; import java.util.Properties; @Component public class NativeKafkaProducer { private Producer<String, String> producer; @Value("${spring.kafka.bootstrap-servers}") private String bootstrapServers; @PostConstruct public void init() { Properties props = new Properties(); props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer"); props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer"); this.producer = new KafkaProducer<>(props); } public void send(String topic, String message) { producer.send(new ProducerRecord<>(topic, message), (metadata, ex) -> { if (ex != null) { System.err.println("发送失败: " + ex.getMessage()); } else { System.out.println("发送成功, 分区: " + metadata.partition()); } }); } @PreDestroy public void close() { producer.close(); // 确保资源释放 } } ``` ### 两种方式对比 | **特** | **KafkaTemplate** | **原生 KafkaProducer** | |------------------------|---------------------------------------|-----------------------------| | 易用 | ⭐⭐⭐⭐ (自动配置) | ⭐⭐ (需手动管理) | | 资源管理 | 自动管理连接池 | 需手动创建/关闭实例 | | 与 Spring 集成 | 深度集成 | 需自行处理生命周期 | | 精细控制 | 基础控制 | 完全控制所有参数 | | 事务支持 | 内置支持 | 需手动实现 | | 适用场景 | 常规消息发送 | 特殊定制需求 | ### 最佳实践建议 1. **首选 KafkaTemplate**:满足大多数场景需求,简化开发 2. **异步发送**:使用 `addCallback()` 处理发送结果,避免阻塞 3. **错误处理**:实现 `ProducerListener` 接口统一处理异常 4. **批量发送**:配置 `batch.size` 和 `linger.ms` 提升吞吐量 5. **事务消息**:使用 `@Transactional` 注解管理消息事务[^3] > 示例:带事务的消息发送 > ```java > @Transactional > public void transactionalSend(String topic, String message) { > kafkaTemplate.send(topic, message); > // 其他数据库操作将和消息发送在同一个事务中 > } > ``` --- ### 相关问题 1. **如何配置 Spring Boot 中的 Kafka 生产者属?** [涉及:`spring.kafka.producer.*` 配置项详解,序列化器选择,重试机制等] 2. **KafkaTemplate 和原生 KafkaProducer 的主要区别是什么?** [对比资源管理、事务支持、错误处理机制等核心差异] 3. **如何实现 Kafka 消息的异步发送和回调处理?** [详解 `ListenableFuture` 和 `ProducerCallback` 的使用] 4. **在 Spring Boot 中如何保证 Kafka 消息的可靠投递?** [探讨 ACK 机制、幂等生产、事务消息等可靠方案] 5. **如何为 KafkaTemplate 配置自定义序列化器?** [实现自定义序列化逻辑集成到 Spring Kafka]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值