java之学习记录 7 - 2 - Spring整合RabbitMQ/消息成功确认机制/消费端限流/过期时间TTL/死信队列/延迟队列

Spring整合RabbitMQ

  • 五种消息模型,在企业中应用最广泛的就是最后一种:定向匹配topic
  • Spring AMQP 是基于 Spring 框架的AMQP消息解决方案,提供模板化的发送和接收消息的抽象层,提供基于消息驱动的 POJO的消息监听等,简化了我们对于RabbitMQ相关程序的开发。

1 生产端工程

  • 依赖
    <dependencies>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit</artifactId>
            <version>2.0.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.25</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.9</version>
        </dependency>
    </dependencies>
  • spring-rabbitmq-producer.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/rabbit
       http://www.springframework.org/schema/rabbit/spring-rabbit.xsd
">

    <!--配置连接-->
    <rabbit:connection-factory id="connectionFactory"
                               host="192.168.58.222"
                               port="5672" username="mzj"
                               password="123456"
                               virtual-host="/lagou"
                               publisher-confirms="true"/>

    <!--配置队列-->
    <rabbit:queue name="test_spring_queue_1"/>
  

    <!--配置rabbitAdmin:主要用于在java代码中对队列的管理,用来创建,绑定,删除队列与交换机,发送消息等-->
    <rabbit:admin connection-factory="connectionFactory"/>

    <!--配置交换机,topic类型-->
    <rabbit:topic-exchange name="spring_topic_exchange">
        <rabbit:bindings>
            <!--绑定队列-->
            <rabbit:binding pattern="msg.#" queue="test_spring_queue_1"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:topic-exchange>

    <!--配置json转换的工具-->
    <bean id="jsonMessageConverter" class="org.springframework.amqp.support.converter.Jackson2JsonMessageConverter"/>

    <!--配置rabbitmq的模板-->
    <rabbit:template id="rabbitTemplate"
                     connection-factory="connectionFactory"
                     exchange="spring_topic_exchange"
                     message-converter="jsonMessageConverter"
    />
</beans>
  • 发消息
package test;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.support.ClassPathXmlApplicationContext;

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

/*
* 生产者
* */
public class Sender {
    public static void main(String[] args) {
        // 创建spring容器
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/spring-rabbitmq-producer.xml");
        // 从容器中获得rabbit模板对象
        RabbitTemplate rabbitTemplate = context.getBean(RabbitTemplate.class);
        // 发消息
        Map<String,String> map = new HashMap<String, String>();
        map.put("name","小马");
        map.put("email","aa@qq.com");
//        for (int i = 1; i <= 10; i++){
            rabbitTemplate.convertAndSend("msg.user",map);
            System.out.println("消息已发出");
//        }

        context.close();
    }
}

2 消费端工程

  • 依赖与生产者一致
  • spring-rabbitmq-consumer.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/rabbit
       http://www.springframework.org/schema/rabbit/spring-rabbit.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
">

    <!--配置连接-->
    <rabbit:connection-factory id="connectionFactory" host="192.168.58.222" port="5672" username="mzj" password="123456" virtual-host="/lagou" />

    <!--配置队列-->
    <rabbit:queue name="test_spring_queue_1" />

    <!--配置rabbitAdmin:主要用于在java代码中对队列的管理,用来创建,绑定,删除队列与交换机,发送消息等-->
    <rabbit:admin connection-factory="connectionFactory"/>

    <!--注解扫描包(springIOC)-->
    <context:component-scan base-package="listener" />

    <!--配置监听-->
    <rabbit:listener-container connection-factory="connectionFactory">
        <rabbit:listener ref="consumerListener" queue-names="test_spring_queue_1" />
    </rabbit:listener-container>
</beans>
  • 消费者
    • MessageListener接口用于spring容器接收到消息后处理消息
    • 如果需要使用自己定义的类型 来实现 处理消息时,必须实现该接口,并重写onMessage()方法
    • 当spring容器接收消息后,会自动交由onMessage进行处理
package listener;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
import org.springframework.amqp.rabbit.listener.adapter.AbstractAdaptableMessageListener;
import org.springframework.stereotype.Component;

import java.io.IOException;

/*
* 消费者监听队列
* */
@Component
public class ConsumerListener extends AbstractAdaptableMessageListener {

    // jackson提供序列化和反序列中使用最多的类,用来转换json的
    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        try {
            // 将message对象转换成json
            JsonNode jsonNode = MAPPER.readTree(message.getBody());
            String name = jsonNode.get("name").asText();
            String email = jsonNode.get("email").asText();
            System.out.println("从队列中获取:【"+name+"】的邮箱是:【"+email+"】");
           
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 启动项目
package test;

import org.springframework.context.support.ClassPathXmlApplicationContext;

/*
* 运行项目
* */
public class TestRunner {
    public static void main(String[] args) throws Exception {
        // 获得容器
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/spring-rabbitmq-consumer.xml");
        // 让程序一直运行,别终止
        System.in.read();
    }
}

消息成功确认机制

在实际场景下,有的生产者发送的消息是必须保证成功发送到消息队列中,那么如何保证成功投递呢?

  • 事务机制
  • 发布确认机制

1 事务机制

  • AMQP协议提供的一种保证消息成功投递的方式,通过信道开启 transactional 模式
  • 并利用信道 的三个方法来实现以事务方式 发送消息,若发送失败,通过异常处理回滚事务,确保消息成功投递
    • channel.txSelect(): 开启事务
    • channel.txCommit() :提交事务
    • channel.txRollback() :回滚事务
  • Spring已经对上面三个方法进行了封装,所以我们只能使用原始的代码演示

1.1 生产者

package transaction;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties;
import util.ConnectionUtil;

import java.io.IOException;

/*
* 消息生产者
* */
public class Sender {
    public static void main(String[] args) throws Exception {
        // 获得连接
        Connection connection = ConnectionUtil.getConnection();
        // 在连接中创建通道(信道)
        Channel channel = connection.createChannel();

        // 声明路由(路由名,路由类型)
        // topic:模糊匹配的定向分发
        channel.exchangeDeclare("test_transaction","topic");
        // 开启事务
        channel.txSelect();
        try {
            channel.basicPublish("test_transaction","product.price", null,"商品1降价".getBytes());
            //System.out.println(1/0); // 模拟异常
            channel.basicPublish("test_transaction","product.price", null,"商品2降价".getBytes());
            // 提交事务
            channel.txCommit();
            System.out.println("生产者:消息已发送");
        } catch (IOException e) {
            System.out.println("消息全部撤销");
            // 事务回滚
            channel.txRollback();
            e.printStackTrace();
        } finally {
            // 释放资源
            channel.close();
            connection.close();
        }

    }
}

1.2 消费者

package transaction;

import com.rabbitmq.client.*;
import util.ConnectionUtil;

import java.io.IOException;

/*
* 消费者
* */
public class Recer1 {
    public static void main(String[] args) throws Exception {
        // 获得连接
        Connection connection = ConnectionUtil.getConnection();
        // 在连接中创建通道(信道)
        Channel channel = connection.createChannel();
        // 声明队列(第二个参数为true:支持持久化)
        channel.queueDeclare("test_transaction_queue",false,false,false,null);
        // 绑定路由(绑定用户相关的消息)
        channel.queueBind("test_transaction_queue","test_transaction","product.#");
        // 从信道中获得消息
        DefaultConsumer consumer = new DefaultConsumer(channel){

            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                // body就是从队列中获取的消息
                String s = new String(body);
                System.out.println("【消费者】 = "+ s);
            }
        };
        // 监听队列 true 自动消息确认
        channel.basicConsume("test_transaction_queue",true,consumer);
    }
}

2 Confirm发布确认机制

  • RabbitMQ为了保证消息的成功投递,采用通过AMQP协议层面为我们提供事务机制的方案,但是采用事务会大大降低消息的吞吐量
  • 本机SSD硬盘测试结果10w条消息未开启事务,大约8s发送完毕;而开启了事务后,需要将近310s,差了30多倍。
  • 接着翻阅官网,发现官网中已标注

Using standard AMQP 0-9-1, the only way to guarantee that a message isn’t lost is by

using transactions – make the channel transactional then for each message or set of

messages publish, commit. In this case, transactions are unnecessarily heavyweight

and decrease throughput by a factor of 250. To remedy this, a confirmation

mechanism was introduced. It mimics the consumer acknowledgements mechanism

already present in the protocol.

关键性译文:开启事务性能最大损失超过250倍

  • 那么有没有更加高效的解决方式呢?答案就是采用Confirm模式。
  • 事务效率为什么会这么低呢?试想一下:10条消息,前9条成功,如果第10条失败,那么9条消息要全部撤销回滚。太太太浪费
  • 而confirm模式则采用补发第10条的措施来完成10条消息的送达

2.1 spring中应用

  • spring-rabbitmq-producer.xml(新增配置文件中的参数)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/rabbit
       http://www.springframework.org/schema/rabbit/spring-rabbit.xsd
">

    <!--配置连接-->
    <rabbit:connection-factory id="connectionFactory"
                               host="192.168.58.222"
                               port="5672" username="mzj"
                               password="123456"
                               virtual-host="/lagou"
                               publisher-confirms="true"/>

    <!--配置rabbitmq的模板-->
    <rabbit:template id="rabbitTemplate"
                     connection-factory="connectionFactory"
                     exchange="spring_topic_exchange"
                     message-converter="jsonMessageConverter"
                     confirm-callback="messageConfirm"
    />

    <!--确认机制的处理类-->
    <bean id="messageConfirm" class="confirm.MessageConfirm"></bean>
</beans>
  • 消息确认处理类
package confirm;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;

/*
* 消息确认处理
* */
public class MessageConfirm implements RabbitTemplate.ConfirmCallback {

    /*
    * correlationData:消息相关的数据对象(封装了消息的唯一id)
    * b:消息是否确认成功
    * s:异常信息
    * */
    @Override
    public void confirm(CorrelationData correlationData, boolean b, String s) {
        if (b){
            System.out.println("消息确认成功a");
        } else {
            System.out.println("消息确认失败");

        }
    }
}
  • 发送消息
package test;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.support.ClassPathXmlApplicationContext;

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

/*
* 生产者
* */
public class Sender {
    public static void main(String[] args) {
        // 创建spring容器
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/spring-rabbitmq-producer.xml");
        // 从容器中获得rabbit模板对象
        RabbitTemplate rabbitTemplate = context.getBean(RabbitTemplate.class);
        // 发消息
        Map<String,String> map = new HashMap<String, String>();
        map.put("name","小马");
        map.put("email","aa@qq.com");
        // 第一个参数是路由名称, 
        // 不写,则使用spring容器中创建的路由 
        // 乱写一个,因为路由名错误导致报错,则进入消息确认失败流程
        rabbitTemplate.convertAndSend("x","msg.user",map);
        System.out.println("消息已发出");

        context.close();
    }
}

消费端限流

  • 在沙漠中行走,3天不喝水,突然喝水,如果使劲喝,容易猝死,要一口一口慢慢喝
  • 我们 Rabbitmq 服务器积压了成千上万条未处理的消息,然后随便打开一个消费者客户端,就会出现这样的情况: 巨量的消息瞬间全部喷涌推送过来,但是单个客户端无法同时处理这么多数据,就会被压垮崩溃
  • 所以,当数据量特别大的时候,我们对生产端限流肯定是不科学的,因为有时候并发量就是特别大,有时候并发量又特别少,这是用户的行为,我们是无法约束的
  • 所以我们应该对消费端限流,用于保持消费端的稳定
  • 例如:汽车企业不停的生产汽车,4S店有好多库存车卖不出去,但是也不会降价处理,就是要保证市值的稳定,如果生产多少台,就卖多少台,不管价格的话,市场就乱了,所以我们要用不变的价格来稳住消费者购车,才能平稳发展
  • RabbitMQ 提供了一种 Qos (Quality of Service,服务质量)服务质量保证功能
    • 即在非自动确认消息的前提下,如果一定数目的消息未被确认前,不再进行消费新的消息
  • 生产者使用循环发出多条消息
for (int i = 1; i <= 10; i++){
    rabbitTemplate.convertAndSend("msg.user",map);
    System.out.println("消息已发出");
}
  • 生产10条堆积未处理的消息

  • 消费者进行限流处理
    <!--配置监听-->
    <!-- prefetch="3" 一次性消费的消息数量。会告诉 RabbitMQ 不要同时给一个消费者推送多于 N 个消息,一旦有 N 个消息还没有ack,则该 consumer 将阻塞,直到消息被ack--> 
    <!-- acknowledge-mode: manual 手动确认-->
    <rabbit:listener-container connection-factory="connectionFactory" prefetch="3" acknowledge="manual">
        <rabbit:listener ref="consumerListener" queue-names="test_spring_queue_1" />
    </rabbit:listener-container>
package listener;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
import org.springframework.amqp.rabbit.listener.adapter.AbstractAdaptableMessageListener;
import org.springframework.stereotype.Component;

import java.io.IOException;

/*
* 消费者监听队列
* */
@Component
public class ConsumerListener extends AbstractAdaptableMessageListener {

    // jackson提供序列化和反序列中使用最多的类,用来转换json的
    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        try {
            // 将message对象转换成json
            JsonNode jsonNode = MAPPER.readTree(message.getBody());
            String name = jsonNode.get("name").asText();
            String email = jsonNode.get("email").asText();
            System.out.println("从队列中获取:【"+name+"】的邮箱是:【"+email+"】");
            // 手动确认消息
            /*
            * 参数1:RabbitMQ向该channel投递的这条消息的唯一标识ID,此ID是一个单调递增的正整数
            * 参数2:为了减少网络流量,手动确认可以被批量处理,当该参数为true时,则一次性可以确认小于等于msgId的所有消息
            * */
            long msgId = message.getMessageProperties().getDeliveryTag();
            channel.basicAck(msgId,true);
            Thread.sleep(3000); 
            System.out.println("休息三秒然后再接收消息");
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 每次确认接收3条消息

过期时间TTL

  • Time To Live:生存时间、还能活多久,单位毫秒
  • 在这个周期内,消息可以被消费者正常消费,超过这个时间,则自动删除(其实是被称为dead message并投入到死信队列,无法消费该消息)
  • RabbitMQ可以对消息队列设置TTL
    • 通过队列设置,队列中所有消息都有相同的过期时间
    • 对消息单独设置,每条消息的TTL可以不同(更颗粒化)

1 设置队列TTL

  • spring-rabbitmq-producer.xml
    <!--配置队列-->
    <rabbit:queue name="test_spring_queue_ttl" auto-declare="true">
        <rabbit:queue-arguments>
            <entry key="x-message-ttl" value-type="long" value="5000"/>
        </rabbit:queue-arguments>
    </rabbit:queue>

  • 5秒之后,消息自动删除

设置消息TTL

  • 设置某条消息的ttl,只需要在创建发送消息时指定即可
<!--2.配置队列--> 
<rabbit:queue name="test_spring_queue_ttl_2">
package test;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.support.ClassPathXmlApplicationContext;

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

/*
* 生产者
* */
public class Sender2 {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/spring-rabbitmq-producer.xml");
        RabbitTemplate rabbitTemplate = context.getBean(RabbitTemplate.class);
        // 创建消息的配置对象
        MessageProperties properties = new MessageProperties();
        // 设置过期时间3s
        properties.setExpiration("3000");
        // 创建消息
        Message message = new Message("测试过期时间".getBytes(),properties);
        rabbitTemplate.convertAndSend("msg.user",message);
        System.out.println("消息已发出");

        context.close();
    }
}
  • 如果同时设置了queue和message的TTL值,则二者中较小的才会起作用

死信队列

  • DLX(Dead Letter Exchanges)死信交换机/死信邮箱,当消息在队列中由于某些原因没有被及时消费而变成死信(dead message)后,这些消息就会被分发到DLX交换机中,而绑定DLX交换机的队列,称之为:“死信队列”
  • 消息没有被及时消费的原因:
    • 消息被拒绝(basic.reject/ basic.nack)并且不再重新投递 requeue=false
    • 消息超时未消费
    • 达到最大队列长度

  • spring-rabbitmq-producer-dlx.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/rabbit
       http://www.springframework.org/schema/rabbit/spring-rabbit.xsd
">

    <!--配置连接-->
    <rabbit:connection-factory id="connectionFactory"
                               host="192.168.58.66"
                               port="5672" username="mzj"
                               password="123456"
                               virtual-host="/lagou"/>

    <!--配置rabbitAdmin:主要用于在java代码中对队列的管理,用来创建,绑定,删除队列与交换机,发送消息等-->
    <rabbit:admin connection-factory="connectionFactory"/>

    <!--配置rabbitmq的模板-->
    <rabbit:template id="rabbitTemplate"
                     connection-factory="connectionFactory"
                     exchange="my_exchange"
    />
    <!--#######################################-->
    <!--定义死信队列-->
    <rabbit:queue name="dlx_queue" />
    <!--定义定向的死信交换机-->
    <rabbit:direct-exchange name="dlx_exchange">
        <rabbit:bindings>
            <rabbit:binding key="dlx_ttl" queue="dlx_queue"></rabbit:binding>
            <rabbit:binding key="dlx_max" queue="dlx_queue"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:direct-exchange>
    <!--声明定向的测试消息的交换机-->
    <rabbit:direct-exchange name="my_exchange">
        <rabbit:bindings>
            <rabbit:binding key="dlx_ttl" queue="test_ttl_queue"></rabbit:binding>
            <rabbit:binding key="dlx_max" queue="test_max_queue"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:direct-exchange>

    <!--声明测试过期的队列-->
    <rabbit:queue name="test_ttl_queue">
        <rabbit:queue-arguments>
            <!--设置队列的过期时间-->
            <entry key="x-message-ttl" value-type="long" value="10000" />
            <!--消息超时 将消息投递给死信交换机-->
            <entry key="x-dead-letter-exchange" value="dlx_exchange" />
        </rabbit:queue-arguments>
    </rabbit:queue>

    <!--声明测试超出长度的队列-->
    <rabbit:queue name="test_max_queue">
        <rabbit:queue-arguments>
            <!--设置队列的额定长度(本队列最多装2个消息)-->
            <entry key="x-max-length" value-type="long" value="2" />
            <!--消息超出长度 将消息投递给死信交换机-->
            <entry key="x-dead-letter-exchange" value="dlx_exchange" />
        </rabbit:queue-arguments>
    </rabbit:queue>
</beans>
  • 发消息进行测试
package test;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/*
* 生产者
* */
public class SenderDLX {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/spring-rabbitmq-producer-dlx.xml");
        RabbitTemplate rabbitTemplate = context.getBean(RabbitTemplate.class);
        //rabbitTemplate.convertAndSend("dlx_ttl","超时,关闭订单".getBytes());
        rabbitTemplate.convertAndSend("dlx_max","测试长度1".getBytes());
        rabbitTemplate.convertAndSend("dlx_max","测试长度2".getBytes());
        rabbitTemplate.convertAndSend("dlx_max","测试长度3".getBytes());
        System.out.println("消息已发出");

        context.close();
    }
}

延迟队列

  • 延迟队列:TTL + 死信队列的合体
  • 死信队列只是一种特殊的队列,里面的消息仍然可以消费
  • 在电商开发部分中,都会涉及到延时关闭订单,此时延迟队列正好可以解决这个问题

1 生产者

沿用上面死信队列案例的超时测试,超时时间改为订单关闭时间即可

2 消费者

  • spring-rabbitmq-consumer.xml
<!-- 监听死信队列 --> 
<rabbit:listener-container connection-factory="connectionFactory" prefetch="3" acknowledge="manual"> 
    <rabbit:listener ref="consumerListener" queue-names="dlx_queue" /> 
</rabbit:listener-container>

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值