SpringBoot整合RabbitMQ实战

  • Java 代码演示MQ的实现

RabbitMQ(Java环境案例)

前言

博客直接参考RabbitMQ的官方文档案例,并且简化这个案例,将和RabbitMQ关联最为紧密的代码上带上注释并且删除了冗杂的代码,让案例更加通俗易懂。并且其中也会对MQ的一些工作方式进行讲解。

前五个案例源码

git clone https://github.com/YeZhiyue/Rabbitmq-SpringBootTuts.git


案例一:HelloWorld

依赖引入

返回目录

主要是RabbitMQ依赖引入

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

基本配置

返回目录

spring:
  rabbitmq:
    host: 59.110.213.92
    port: 5672
    username: guest
    password: guest
    listener:
      simple:
        # 并发消费者的初始化值
        concurrency: 10
        # 并发消费者的最大值,同时处理消息的消费者的最大数量
        max-concurrency: 20
        # 这个消费者每次监听时可拉取处理的消息数量。
        prefetch: 5

  # 不同的tut这里需要进行变更,表示我们配置生效的作用域
  profiles:
    active: tut1

logging:
  level:
    org: INFO

配置注意点

返回目录

1.1 @EnableScheduling 需要使能我们的定时任务

我们这里的发送者是定时发送消息到MQ队列的,所以不要忘记这个的使能

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

1.2 @Profile(“tut1”) 这里需要制定配置生效的作用域

由于这个案例工程中有多个例子,所以这里就配置了作用域防止配置冲突

@Profile("tut1")
@Configuration
public class Tut1Config {

	@Bean // 创建我们的队列并且返回名称
	public Queue hello() {
		return new Queue("tut1");
	}

	@Bean // 注册我们的消费者,也就是初始化其中的参数
	public Tut1Receiver receiver() {
		return new Tut1Receiver();
	}


	@Bean // 注册我们的生产者,也就是初始化其中的部分参数
	public Tut1Sender sender() {
		return new Tut1Sender();
	}
}

3个主要文件: RabbitMQ初始化配置 -> 生产者代码 -> 消费者代码

返回目录

1.1 RabbitMQ初始化配置

@Profile("tut1")
@Configuration
public class Tut1Config {

	@Bean // 创建我们的队列并且返回名称
	public Queue hello() {
		return new Queue("tut1");
	}

	@Bean // 注册我们的消费者,也就是初始化其中的参数
	public Tut1Receiver receiver() {
		return new Tut1Receiver();
	}


	@Bean // 注册我们的生产者,也就是初始化其中的部分参数
	public Tut1Sender sender() {
		return new Tut1Sender();
	}
}

1.2 生产者代码

public class Tut1Sender {

    @Autowired
    private RabbitTemplate template;

    @Autowired
    private Queue queue;

    @Scheduled(fixedDelay = 1000, initialDelay = 500)
    public void send() {
        this.template.convertAndSend(queue.getName(), "msg");
    }
}

1.3 消费者代码

@RabbitListener(queues = "tut1")
public class Tut1Receiver {
	@RabbitHandler
	public void receive(String in) {
		System.out.println(" [x] Received '" + in + "'");
	}
}

结果展示

返回目录

1.1 控制台接收消息

 [x] Received 'msg'
 [x] Received 'msg'
 [x] Received 'msg'
 [x] Received 'msg'
 [x] Received 'msg'

1.2 MQ界面

1.2.1 队列情况

1.2.2 交换机情况


案例二:任务队列

说明

返回目录

案例一中是一个生产者对应一个消费者,但是在实际的生产生活中通常会是一个生产者对应多个消费者,也就是在MQ服务器上的队列里面会有一堆消息等待处理,这个时候就需要有多个消费者一起同步处理这些消息效率会更加高。案例二中就是一个生产者对应多个消费者。并且一些基本配置和依赖引入就不做讲解。就是 application.yml 文件中注意修改配置作用域。

  ... 
  profiles:
    active: tut2

3个主要文件: RabbitMQ初始化配置 -> 生产者代码 -> 消费者代码

返回目录

1.1 RabbitMQ初始化配置

@Profile("tut2")
@Configuration
public class Tut2Config {

	@Bean
	public Queue hello() {
		return new Queue("tut2");
	}

// 注册你的工作者,分担任务压力
// 注意:这里可以注册很多工作者,其实可以理解为注册了多个线程同时工作
private static class ReceiverConfig {

	@Bean
	public Tut2Receiver receiver1() {
		return new Tut2Receiver(1);
	}

	@Bean
	public Tut2Receiver receiver2() {
		return new Tut2Receiver(2);
	}
}

	@Bean
	public Tut2Sender sender() {
		return new Tut2Sender();
	}
}

1.2 生产者代码

public class Tut2Sender {

	@Autowired
	private RabbitTemplate template;

	@Autowired
	private Queue queue;

	AtomicInteger dots = new AtomicInteger(0);

	AtomicInteger count = new AtomicInteger(0);

	@Scheduled(fixedDelay = 1000, initialDelay = 500)
	public void send() {
		StringBuilder builder = new StringBuilder("Hello");
		if (dots.getAndIncrement() == 4) {
			dots.set(1);
		}
		for (int i = 0; i < dots.get(); i++) {
			builder.append('.');
		}
		builder.append(count.incrementAndGet());
		String message = builder.toString();
		template.convertAndSend(queue.getName(), message);
		System.out.println(" [x] Sent '" + message + "'");
	}
}

1.3 消费者代码

@RabbitListener(queues = "tut2")
public class Tut2Receiver {

	// 区分不同的工作者
	private final int instance;

	public Tut2Receiver(int i) {
		this.instance = i;
	}

	@RabbitHandler
	public void receive(String in) throws InterruptedException {
		// 简单的定时器
		StopWatch watch = new StopWatch();
		watch.start();
		System.out.println("instance " + this.instance + " [x] Received '" + in + "'");
		doWork(in);
		System.out.println("instance " + this.instance + " [x] Done in " + watch.getTotalTimeSeconds() + "s");
	}

	private void doWork(String in) throws InterruptedException {
		for (char ch : in.toCharArray()) {
			if (ch == '.') {
				Thread.sleep(1000);
			}
		}
	}
}

结果展示

返回目录

1.1 控制台接收消息

下面 instance 1 代表消费者一号,instance 2 代表消费者二号。可以看到两个消费者同时处理MQ发送过来的消息,可见这个就是一个任务队列。

instance 1 [x] Done in 0.0s
instance 1 [x] Done in 0.0s
 [x] Sent 'Hello...391'
instance 2 [x] Received 'Hello...391'
 [x] Sent 'Hello....392'
instance 1 [x] Done in 0.0s
instance 1 [x] Done in 0.0s
instance 2 [x] Received 'Hello....392'
 [x] Sent 'Hello.393'
instance 2 [x] Received 'Hello.393'

1.2 MQ界面

1.2.1 队列情况

1.2.2 交换机情况

案例三:FanoutExchange 基础交换机

说明

返回目录

案例一和案例二中都是通过直接指定Queue的名称来和MQ进行连接,案例三中会通过Exchange交换机进行和队列的绑定。交换机就是消息的中转站,用于接收分发消息。其中有 fanout、direct、topic、headers 四种

  ... 
  profiles:
    active: tut2

3个主要文件: RabbitMQ初始化配置 -> 生产者代码 -> 消费者代码

返回目录

1.1 RabbitMQ初始化配置

/**
 * 配置FanoutExchange
 * 配置随机队列
 * 配置队列和Exchange之间的绑定
 *
 * 注册接受者和发送者
 */
@Profile("tut3")
@Configuration
public class Tut3Config {

	@Bean // 注册Exchange
	public FanoutExchange fanout() {
		return new FanoutExchange("tut3");
	}

	private static class ReceiverConfig {
		@Bean // 表示匿名的,非耐用性,排他性,自动删除队列。
		public Queue autoDeleteQueue1() {
			return new AnonymousQueue();
		}
		@Bean
		public Queue autoDeleteQueue2() {
			return new AnonymousQueue();
		}
		@Bean // 直接通过上面注册的组件作为依赖注入方法
		public Binding binding1(FanoutExchange fanout, Queue autoDeleteQueue1) {
			return BindingBuilder.bind(autoDeleteQueue1).to(fanout);
		}
		@Bean
		public Binding binding2(FanoutExchange fanout, Queue autoDeleteQueue2) {
			return BindingBuilder.bind(autoDeleteQueue2).to(fanout);
		}
		@Bean
		public Tut3Receiver receiver() {
			return new Tut3Receiver();
		}
	}
	@Bean
	public Tut3Sender sender() {
		return new Tut3Sender();
	}
}

1.2 生产者代码

public class Tut3Sender {

	@Autowired
	private RabbitTemplate template;

	@Autowired
	private FanoutExchange fanout;

	AtomicInteger dots = new AtomicInteger(0);

	AtomicInteger count = new AtomicInteger(0);

	@Scheduled(fixedDelay = 1000, initialDelay = 500)
	public void send() {
		StringBuilder builder = new StringBuilder("Hello");
		if (dots.getAndIncrement() == 3) {
			dots.set(1);
		}
		for (int i = 0; i < dots.get(); i++) {
			builder.append('.');
		}
		builder.append(count.incrementAndGet());
		String message = builder.toString();
		// 通过Exchange来发送消息
		template.convertAndSend(fanout.getName(), "", message);
		System.out.println(" [x] Sent '" + message + "'");
	}

}

1.3 消费者代码

public class Tut3Receiver {

	// 通过 fanout 来随机绑定两个对象
	@RabbitListener(queues = "#{autoDeleteQueue1.name}")
	public void receive1(String in) throws InterruptedException {
		receive(in, 1);
	}

	@RabbitListener(queues = "#{autoDeleteQueue2.name}")
	public void receive2(String in) throws InterruptedException {
		receive(in, 2);
	}

	public void receive(String in, int receiver) throws InterruptedException {
		StopWatch watch = new StopWatch();
		watch.start();
		System.out.println("instance " + receiver + " [x] Received '" + in + "'");
		doWork(in);
		watch.stop();
		System.out.println("instance " + receiver + " [x] Done in " + watch.getTotalTimeSeconds() + "s");
	}

	private void doWork(String in) throws InterruptedException {
		for (char ch : in.toCharArray()) {
			if (ch == '.') {
				Thread.sleep(1000);
			}
		}
	}
}

结果展示

返回目录

1.1 控制台接收消息

下面 instance 1 代表消费者一号,instance 2 代表消费者二号。

instance 2 [x] Done in 2.002s
instance 1 [x] Done in 2.002s
instance 1 [x] Received 'Hello.3403'
instance 2 [x] Received 'Hello.3403'
 [x] Sent 'Hello..3404'
instance 1 [x] Done in 1.0s
instance 2 [x] Done in 1.0s

1.2 MQ界面

1.2.1 队列情况

1.2.2 交换机情况

案例四:DirectExchange 直连交换机

说明

返回目录

可以通过指定RoutKey去和多个消息队列进行匹配。而不是像FanoutExchange去使用默认的RoutingKey去连接队列,更加方便我们的管理。

3个主要文件: RabbitMQ初始化配置 -> 生产者代码 -> 消费者代码

返回目录

1.1 RabbitMQ初始化配置

@Profile("tut4")
@Configuration
public class Tut4Config {

	@Bean
	public DirectExchange direct() {
		return new DirectExchange("tut4");
	}

	private static class ReceiverConfig {

		@Bean
		public Queue autoDeleteQueue1() {
			return new AnonymousQueue();
		}

		@Bean
		public Queue autoDeleteQueue2() {
			return new AnonymousQueue();
		}

		@Bean
		public Binding binding1a(DirectExchange direct, Queue autoDeleteQueue1) {
			return BindingBuilder.bind(autoDeleteQueue1).to(direct).with("orange");
		}

		@Bean
		public Binding binding1b(DirectExchange direct, Queue autoDeleteQueue1) {
			return BindingBuilder.bind(autoDeleteQueue1).to(direct).with("black");
		}

		@Bean
		public Binding binding2a(DirectExchange direct, Queue autoDeleteQueue2) {
			return BindingBuilder.bind(autoDeleteQueue2).to(direct).with("green");
		}

		@Bean
		public Binding binding2b(DirectExchange direct, Queue autoDeleteQueue2) {
			return BindingBuilder.bind(autoDeleteQueue2).to(direct).with("black");
		}

		@Bean
		public Tut4Receiver receiver() {
	 	 	return new Tut4Receiver();
		}

	}

	@Bean
	public Tut4Sender sender() {
		return new Tut4Sender();
	}

}

1.2 生产者代码

public class Tut4Sender {

	@Autowired
	private RabbitTemplate template;

	@Autowired
	private DirectExchange direct;

	AtomicInteger index = new AtomicInteger(0);

	AtomicInteger count = new AtomicInteger(0);

	private final String[] keys = {"orange", "black", "green"};

	@Scheduled(fixedDelay = 1000, initialDelay = 500)
	public void send() {
		StringBuilder builder = new StringBuilder("Hello to ");
		if (this.index.incrementAndGet() == 3) {
			this.index.set(0);
		}
		String key = keys[this.index.get()];
		builder.append(key).append(' ');
		builder.append(this.count.incrementAndGet());
		String message = builder.toString();
		template.convertAndSend(direct.getName(), key, message);
		System.out.println(" [x] Sent '" + message + "'");
	}
}

1.3 消费者代码

public class Tut4Receiver {

	@RabbitListener(queues = "#{autoDeleteQueue1.name}")
	public void receive1(String in) throws InterruptedException {
		receive(in, 1);
	}

	@RabbitListener(queues = "#{autoDeleteQueue2.name}")
	public void receive2(String in) throws InterruptedException {
		receive(in, 2);
	}

	public void receive(String in, int receiver) throws InterruptedException {
		StopWatch watch = new StopWatch();
		watch.start();
		System.out.println("instance " + receiver + " [x] Received '" + in + "'");
		doWork(in);
		watch.stop();
		System.out.println("instance " + receiver + " [x] Done in " + watch.getTotalTimeSeconds() + "s");
	}

	private void doWork(String in) throws InterruptedException {
		for (char ch : in.toCharArray()) {
			if (ch == '.') {
				Thread.sleep(1000);
			}
		}
	}
}

结果展示

返回目录

1.1 控制台接收消息

下面 instance 1 代表消费者一号,instance 2 代表消费者二号。

instance 1 [x] Done in 2.001s
 [x] Sent 'Hello to lazy.brown.fox 171'
instance 1 [x] Done in 2.001s
instance 2 [x] Done in 2.001s
instance 2 [x] Received 'Hello to lazy.brown.fox 171'
 [x] Sent 'Hello to lazy.pink.rabbit 172'
instance 1 [x] Done in 2.001s

1.2 MQ界面

1.2.1 队列情况

1.2.2 交换机情况

案例五:TopicExchange 可以使用通配符配置RoutingKey的交换机

说明

返回目录

TopicExchange和前面的交换机相比更加复杂,就是可以使用通配符配置RoutingKey的交换机。其中 # 代表单个单词, * 代表多个单词。注意了,这里代表的是单词,而不是字母。如:red.apple.* -> 可以适配 red.apple.person.man、red.apple.person、red.apple.hello.peson.man , 但是 red.apple.# 只能适配 red.apple.person,其他的red.apple.hello.peson.man、red.apple.person.man不能匹配。

3个主要文件: RabbitMQ初始化配置 -> 生产者代码 -> 消费者代码

返回目录

1.1 RabbitMQ初始化配置

@Profile("tut5")
@Configuration
public class Tut5Config {

	@Bean
	public TopicExchange topic() {
		return new TopicExchange("tut5");
	}

	private static class ReceiverConfig {

		@Bean
		public Tut5Receiver receiver() {
	 	 	return new Tut5Receiver();
		}

		@Bean
		public Queue autoDeleteQueue1() {
			return new AnonymousQueue();
		}

		@Bean
		public Queue autoDeleteQueue2() {
			return new AnonymousQueue();
		}

		@Bean
		public Binding binding1a(TopicExchange topic, Queue autoDeleteQueue1) {
			return BindingBuilder.bind(autoDeleteQueue1).to(topic).with("*.orange.*");
		}

		@Bean
		public Binding binding1b(TopicExchange topic, Queue autoDeleteQueue1) {
			return BindingBuilder.bind(autoDeleteQueue1).to(topic).with("*.*.rabbit");
		}

		@Bean
		public Binding binding2a(TopicExchange topic, Queue autoDeleteQueue2) {
			return BindingBuilder.bind(autoDeleteQueue2).to(topic).with("lazy.#");
		}

	}

	@Bean
	public Tut5Sender sender() {
		return new Tut5Sender();
	}
}

1.2 生产者代码

public class Tut5Sender {

	@Autowired
	private RabbitTemplate template;

	@Autowired
	private TopicExchange topic;

	AtomicInteger index = new AtomicInteger(0);

	AtomicInteger count = new AtomicInteger(0);

	private final String[] keys = {"quick.orange.rabbit", "lazy.orange.elephant", "quick.orange.fox",
			"lazy.brown.fox", "lazy.pink.rabbit", "quick.brown.fox"};

	@Scheduled(fixedDelay = 1000, initialDelay = 500)
	public void send() {
		StringBuilder builder = new StringBuilder("Hello to ");
		if (this.index.incrementAndGet() == keys.length) {
			this.index.set(0);
		}
		String key = keys[this.index.get()];
		builder.append(key).append(' ');
		builder.append(this.count.incrementAndGet());
		String message = builder.toString();
		template.convertAndSend(topic.getName(), key, message);
		System.out.println(" [x] Sent '" + message + "'");
	}
}

1.3 消费者代码

public class Tut5Receiver {

	@RabbitListener(queues = "#{autoDeleteQueue1.name}")
	public void receive1(String in) throws InterruptedException {
		receive(in, 1);
	}

	@RabbitListener(queues = "#{autoDeleteQueue2.name}")
	public void receive2(String in) throws InterruptedException {
		receive(in, 2);
	}

	public void receive(String in, int receiver) throws InterruptedException {
		StopWatch watch = new StopWatch();
		watch.start();
		System.out.println("instance " + receiver + " [x] Received '" + in + "'");
		doWork(in);
		watch.stop();
		System.out.println("instance " + receiver + " [x] Done in " + watch.getTotalTimeSeconds() + "s");
	}

	private void doWork(String in) throws InterruptedException {
		for (char ch : in.toCharArray()) {
			if (ch == '.') {
				Thread.sleep(1000);
			}
		}
	}
}

结果展示

返回目录

1.1 控制台接收消息

下面 instance 1 代表消费者一号,instance 2 代表消费者二号。

instance 1 [x] Done in 2.001s
instance 2 [x] Received 'Hello to lazy.pink.rabbit 268'
instance 1 [x] Received 'Hello to lazy.pink.rabbit 268'
 [x] Sent 'Hello to quick.brown.fox 269'
instance 2 [x] Done in 2.001s
 [x] Sent 'Hello to quick.orange.rabbit 270'
instance 2 [x] Done in 2.001s
instance 1 [x] Done in 2.001s

1.2 MQ界面

1.2.1 队列情况

1.2.2 交换机情况

案例六:模拟RPC框架

说明

返回目录

和前面几个案例不同,模拟RPC需要我们搭建两个服务。可以下载源码调试。

RPC示例

git clone https://github.com/YeZhiyue/RabbitMQ-RPC.git

Sender服务配置(生产者)

返回目录

1.1 注意点

在生产者中需要配置我们的队列的名称,因为我们在另外的消费者服务中是不知道队列的信息的,所以需要在这里指定队列名称。方便我们消费者服务器接收消息。

1.2 配置发送者

@Configuration
public class SenderMQConfig {

    @Bean // topic类型Exchange,可以使用通配符定义规则
    public TopicExchange topic() {
        return new TopicExchange("cooperate");
    }

    /// 建立队列
    @Bean
    public Queue autoDeleteQueue1() {
        // 普通队列,方便RPC
        return new Queue("topicQueue1");
    }

    @Bean
    public Queue autoDeleteQueue2() {
        return new Queue("topicQueue2");
    }

    @Bean
    public Binding binding1a(TopicExchange topic, Queue autoDeleteQueue1) {
        return BindingBuilder.bind(autoDeleteQueue1).to(topic).with("*.orange.*");
    }

    /// 配置绑定规则
    @Bean
    public Binding binding1b(TopicExchange topic, Queue autoDeleteQueue1) {
        return BindingBuilder.bind(autoDeleteQueue1).to(topic).with("*.*.rabbit");
    }

    @Bean
    public Binding binding2a(TopicExchange topic, Queue autoDeleteQueue2) {
        return BindingBuilder.bind(autoDeleteQueue2).to(topic).with("lazy.#");
    }

    @Bean
    public MQSender sender() {
        return new MQSender();
    }
}

1.3 发送者代码

public class MQSender {

    @Autowired
    private RabbitTemplate template;

    @Autowired
    private TopicExchange topic;

    AtomicInteger index = new AtomicInteger(0);

    AtomicInteger count = new AtomicInteger(0);

    private final String[] keys = {"quick.orange.rabbit", "lazy.orange.elephant", "quick.orange.fox",
            "lazy.brown.fox", "lazy.pink.rabbit", "quick.brown.fox"};

    @Scheduled(fixedDelay = 1000, initialDelay = 500)
    public void send() {
        StringBuilder builder = new StringBuilder("Hello to ");
        if (this.index.incrementAndGet() == keys.length) {
            this.index.set(0);
        }
        String key = keys[this.index.get()];
        builder.append(key).append(' ');
        builder.append(this.count.incrementAndGet());
        String message = builder.toString();
        template.convertAndSend(topic.getName(), key, message);
        System.out.println(" [x] Sent '" + message + "'");
    }
}

Receiver服务配置(消费者)

返回目录

1.1 配置接受者

@Configuration
public class RecieverConfig {
    @Bean
    public MQReceiver receiver() {
        return new MQReceiver();
    }
}

1.2 接受者代码

public class MQReceiver {

    @RabbitListener(queues = "topicQueue1")
    public void receive1(String in) throws InterruptedException {
        receive(in, 1);
    }

    @RabbitListener(queues = "topicQueue2")
    public void receive2(String in) throws InterruptedException {
        receive(in, 2);
    }

    public void receive(String in, int receiver) throws InterruptedException {
        StopWatch watch = new StopWatch();
        watch.start();
        System.out.println("instance " + receiver + " [x] Received '" + in + "'");
        doWork(in);
        watch.stop();
        System.out.println("instance " + receiver + " [x] Done in " + watch.getTotalTimeSeconds() + "s");
    }

    private void doWork(String in) throws InterruptedException {
        for (char ch : in.toCharArray()) {
            if (ch == '.') {
                Thread.sleep(1000);
            }
        }
    }
}

运行结果

返回目录

1.1 发送者

 [x] Sent 'Hello to quick.orange.rabbit 372'
 [x] Sent 'Hello to lazy.orange.elephant 373'
 [x] Sent 'Hello to quick.orange.fox 374'
 [x] Sent 'Hello to lazy.brown.fox 375'
 [x] Sent 'Hello to lazy.pink.rabbit 376'
 [x] Sent 'Hello to quick.brown.fox 377'
 [x] Sent 'Hello to quick.orange.rabbit 378'
 [x] Sent 'Hello to lazy.orange.elephant 379'
 [x] Sent 'Hello to quick.orange.fox 380'

1.2 接收者

instance 2 [x] Done in 2.0001737s
instance 2 [x] Received 'Hello to lazy.orange.elephant 343'
instance 1 [x] Done in 2.0001565s
instance 1 [x] Received 'Hello to lazy.orange.elephant 1'
instance 2 [x] Done in 2.0008724s
instance 2 [x] Received 'Hello to lazy.brown.fox 345'
instance 1 [x] Done in 2.0007671s
instance 1 [x] Received 'Hello to quick.orange.fox 2'
instance 2 [x] Done in 2.0000875s

MQ补充

队列属性值参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值