ActiveMQ 使用

一、消息中间件

1. 消息中间件产生的背景:
在传统的客户端与服务器进行通讯时。客户端调用后,必须等待服务对象完成处理返回结果才能继续执行。在此过程中客户与服务器对象的生命周期紧密耦合,客户进程和服务对象进程都都必须正常运行;如果由于服务对象崩溃或者网络故障导致用户的请求不可达,客户会受到异常

2. 什么是消息中间件
百度百科
消息中间件利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息排队模型,它可以在分布式环境下扩展进程间的通信。

发送者将消息发送给消息服务器,消息服务器将消感存放在若千队列中,在合适的时候再将消息转发给接收者。这种模式下,发送和接收是异步的,发送者无需等待; 二者的生命周期未必相同: 发送消息的时候接收者不一定运行,接收消息的时候发送者也不一定运行;一对多通信: 对于一个消息可以有多个接收者。

注: ActiveMQ 的配置与启动 : https://blog.csdn.net/liuyuanq123/article/details/79109218

二、JMS

JMS (Java Message Service) : java消息服务。JMS是java的消息服务,JMS的客户端之间可以通过JMS服务进行异步的消息传输。
AMQP (Advanced Message Queuing Protocol ) : 高级消息队列协议

1. 点对点 消息模型
点对点模式: 每一条消息都有一个发送者和一个接收者。在实际过程中,通常可以使用几个接收者来处理队列中的消息。不过每个接收者都会处理到自己所接收到的消息。但是我们无法时得知某条特定的消息会由哪一个接收者来处理。
在这里插入图片描述
P2P的特点:

  1. 每个消息只有一个消费者(Consumer)(即一旦被消费,消息就不再在消息队列中)
  2. 发送者和接收者之间在时间上没有依赖性,也就是说当发送者发送了消息之后,不管接收者有没有正在运行,它不会影响到消息被发送到队列
  3. 接收者在成功接收消息之后需向队列应答成功
    如果你希望发送的每个消息都应该被成功处理的话,那么你需要P2P模式。

2. 发布-订阅 消息模型
发布-订阅模式: 对于异步消息来说,发布者并不知道神订阅了他的消息,发布者只知道需要将他的消息发送到一个特定的主题。但是并不知道谁在监听这个消息,也不知道订阅者会如何处理这个消息。
在这里插入图片描述
Pub/Sub的特点 :

  1. 每个消息可以有多个消费者
  2. 发布者和订阅者之间有时间上的依赖性。针对某个主题(Topic)的订阅者,它必须创建一个订阅者之后,才能消费发布者的消息,而且为了消费消息,订阅者必须保持运行的状态。为了缓和这样严格的时间相关性,JMS允许订阅者创建一个可持久化的订阅。这样,即使订阅者没有被激活(运行),它也能接收到发布者的消息。
  3. 如果你希望发送的消息可以不被做任何处理、或者被一个消息者处理、或者可以被多个消费者处理的话,那么可以采用Pub/Sub模型
  4. 消息的消费
    在JMS中,消息的产生和消息是异步的。对于消费来说,JMS的消息者可以通过两种方式来消费消息。
    ○ 同步
    订阅者或接收者调用receive方法来接收消息,receive方法在能够接收到消息之前(或超时之前)将一直阻塞
    ○ 异步
    订阅者或接收者可以注册为一个消息监听器。当消息到达之后,系统自动调用监听器的onMessage方法。

三、MQ 产品分析(本文中使用ActiveMQ)

名称介绍
RabbitMQ是使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正是如此,使的它变的非常重量级,更适合于企业级的开发。同时实现了一个经纪人(Broker)构架,这意味着消息在发送给客户端时先在中心队列排队。对路由(Routing),负载均衡(Load balance)或者数据持久化都有很好的支持。
Redis是一个Key-Value的NoSQL数据库,开发维护很活跃,虽然它是一个Key-Value数据库存储系统,但它本身支持MQ功能,所以完全可以当做一个轻量级的队列服务来使用。对于RabbitMQ和Redis的入队和出队操作,各执行100万次,每10万次记录一次执行时间。测试数据分为128Bytes、512Bytes、1K和10K四个不同大小的数据。实验表明:入队时,当数据比较小时Redis的性能要高于RabbitMQ,而如果数据大小超过了10K,Redis则慢的无法忍受;出队时,无论数据大小,Redis都表现出非常好的性能,而RabbitMQ的出队性能则远低于Redis。
ZeroMQ号称最快的消息队列系统,尤其针对大吞吐量的需求场景。ZMQ能够实现RabbitMQ不擅长的高级/复杂的队列,但是开发人员需要自己组合多种技术框架,技术上的复杂度是对这MQ能够应用成功的挑战。ZeroMQ具有一个独特的非中间件的模式,你不需要安装和运行一个消息服务器或中间件,因为你的应用程序将扮演了这个服务角色。你只需要简单的引用ZeroMQ程序库,可以使用NuGet安装,然后你就可以愉快的在应用程序之间发送消息了。但是ZeroMQ仅提供非持久性的队列,也就是说如果down机,数据将会丢失。其中,Twitter的Storm中使用ZeroMQ作为数据流的传输。
ActiveMQ是Apache下的一个子项目。 类似于ZeroMQ,它能够以代理人和点对点的技术实现队列。同时类似于RabbitMQ,它少量代码就可以高效地实现高级应用场景。RabbitMQ、ZeroMQ、ActiveMQ均支持常用的多种语言客户端 C++、Java、.Net,、Python、 Php、 Ruby等。
Jafka/KafkaKafka是Apache下的一个子项目,是一个高性能跨语言分布式Publish/Subscribe消息队列系统,而Jafka是在Kafka之上孵化而来的,即Kafka的一个升级版。具有以下特性:快速持久化,可以在O(1)的系统开销下进行消息持久化;高吞吐,在一台普通的服务器上既可以达到10W/s的吞吐速率;完全的分布式系统,Broker、Producer、Consumer都原生自动支持分布式,自动实现复杂均衡;支持Hadoop数据并行加载,对于像Hadoop的一样的日志数据和离线分析系统,但又要求实时处理的限制,这是一个可行的解决方案。Kafka通过Hadoop的并行加载机制来统一了在线和离线的消息处理,这一点也是本课题所研究系统所看重的。Apache Kafka相对于ActiveMQ是一个非常轻量级的消息系统,除了性能非常好之外,还是一个工作良好的分布式系统。

四、ActiveMQ简单使用

1. 点对点模式(p2p)

消息发布者

public class JMSP2pProducer {
    public static void main(String[] args) throws JMSException {
        // 创建工厂类
        ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(ActiveMQConnectionFactory.DEFAULT_USER, ActiveMQConnectionFactory.DEFAULT_PASSWORD, ActiveMQConnectionFactory.DEFAULT_BROKER_BIND_URL);
        // 创建连接并启动
        Connection connection = connectionFactory.createConnection();
        connection.start();
        // 创建session
        Session session = connection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE);
        // 创建消息队列,指定目标地址
        Destination  destination = session.createQueue("kingfish");
        // 创建生产者
        MessageProducer producer = session.createProducer(destination);
        //   设置不持久化到本地
        producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);

        // 发送消息
        for (int i = 0; i < 10; i++) {
            TextMessage message = new ActiveMQTextMessage();
            message.setText("p2p 生产者生产 : " + i);
            System.out.println("p2p 生产者生产 : " + i);
            producer.send(destination, message);
        }
        connection.close();
    }
}

消息接收者

 public class JMSP2pConsumer {

    public static void main(String[] args) throws JMSException {
        // 创建工厂类
        ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(ActiveMQConnectionFactory.DEFAULT_USER, ActiveMQConnectionFactory.DEFAULT_PASSWORD, ActiveMQConnectionFactory.DEFAULT_BROKER_BIND_URL);

        Connection connection = connectionFactory.createConnection();
        connection.start();
        // 创建session
        Session session = connection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE);
        // 创建消息队列,指定目标地址
        Destination destination = session.createQueue("address_name");
        // 创建消费者
        MessageConsumer consumer = session.createConsumer(destination);
        while (true) {
            TextMessage textMessage = (TextMessage)consumer.receive();
            if (textMessage == null) break;
            System.out.println("p2p 消费者,消费了 : " + textMessage.getText());
        }

        connection.close();
        }
    }



**2. 发布-订阅模式(p2s)**
消息发布者
```java
public class JMSP2SProducer {
    public static void main(String[] args) throws Exception{
        // 创建工厂类
        ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(ActiveMQConnectionFactory.DEFAULT_USER, ActiveMQConnectionFactory.DEFAULT_PASSWORD, ActiveMQConnectionFactory.DEFAULT_BROKER_BIND_URL);
        // 创建连接并启动
        Connection connection = connectionFactory.createConnection();
        connection.start();
        // 创建session
        Session session = connection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE);

        // 创建主题队列,指定目标地址
        Destination destination = session.createTopic("t2p_address_name");
        // 创建生产者
        MessageProducer producer = session.createProducer(destination);
        //   设置不持久化到本地
        producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);

        // 发送消息
        for (int i = 0; i < 10; i++) {
            TextMessage message = new ActiveMQTextMessage();
            message.setText("t2p 生产者生产 : " + i);
            System.out.println("t2p 生产者生产 : " + i);
            producer.send(destination, message);
        }
        connection.close();
    }
}

消息订阅者

public class JMSP2SConsumer {

    public static void main(String[] args) throws Exception {
        // 创建工厂类
        ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(ActiveMQConnectionFactory.DEFAULT_USER, ActiveMQConnectionFactory.DEFAULT_PASSWORD, ActiveMQConnectionFactory.DEFAULT_BROKER_BIND_URL);
        // 创建连接并启动
        Connection connection = connectionFactory.createConnection();
        connection.start();
        // 创建session
        Session session = connection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE);

        // 创建主题队列,指定目标地址
        Destination destination = session.createTopic("t2p_address_name");
        // 创建消费者
        MessageConsumer consumer = session.createConsumer(destination);
        while (true) {
            TextMessage textMessage = (TextMessage) consumer.receive();
            System.out.println("p2c 消费者,消费了 : " + textMessage.getText());
        }
    }
}

注:
1. 生产者可通过 MessageProducer.setDeliveryMode(int deliveryMode) 设置消息的持久化
2. connection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE); 中第一个参数代表是否启用事务。true则启动,fasle则不启用。第二个参数代表签收机制(下面细讲)。

五、ActiveMQ的签收机制

ActiveMQ消息签收机制: 客戶端成功接收一条消息的标志是一条消息被签收,成功应答。

消息的签收机制分为两种:
1. 带事务的session: 如果session带有事务,并且事务成功提交,则消息被自动签收。如果事务回滚,则消息会被再次传送。即生产者发送消息完成后必须提交事务给队列。消费者获取到事务消息,若消费者未提交事务,则认为其未被消费。MQ会按照自动重试机制重试。这种模式下无论是生产消息还是消费消息都需要进行事务的提交,才可以提交到队列中。
eg: 生产者消费者的处理:
生产者
在这里插入图片描述
注: 从源码可以看出: 当开启事务时,其 acknowledgeMode 已经固定为 Session.SESSION_TRANSACTED 在这里插入图片描述
2.不带事务的session:
不带事务的session的签收方式,取决于session的配置。
ActiveMQ 支持一下三种模式:
Session.AUTO_ACKNOWLEDGE : 消息自动签收。即队列将消息发送给消费者后,默认完成此次消息消费,即使消费者处理消息出现异常或出现其他问题。
Session.CLIENT_ACKNOWLEDGE : 客戶端调用acknowledge方法手动签收。否则认为消费者未消费消息,会按照默认重试机制重试。
Session.DUPS_OK_ACKNOWLEDGE : 不是必须签收,消息可能会重复发送。此确认模式指示会话懒惰地确认消息的传递。这可能会导致:如果JMS提供程序失败,则会交付一些重复消息,因此应该只由能够容忍重复消息的使用者使用。使用此模式可以通过最小化会话为防止重复所做的工作来减少会话开销。

eg : Session.CLIENT_ACKNOWLEDGE 下的消费者
在这里插入图片描述

六、 Spring ActiveMQ 整合

1. 创建spring-activemq.xml 配置文件配置 ActivceMQ 。xml方式

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:jms="http://www.springframework.org/schema/jms"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p" 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/jms
        http://www.springframework.org/schema/jms/spring-jms.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:property-placeholder location="classpath:activemq.properties"/>

    <!-- 1. 配置连接工厂 -->
    <bean id="amqConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory"
          p:brokerURL="${activemq.brokerURL}"></bean>
    <!-- 2. 进行缓存配置 -->
    <bean id="cachedConnectionFactory" class="org.springframework.jms.connection.CachingConnectionFactory"
          p:targetConnectionFactory-ref="amqConnectionFactory"
          p:sessionCacheSize="${activemq.sessionCacheSize}"></bean>

    <!--  3. 创建消息队列-->
    <bean id="destination" class="org.apache.activemq.command.ActiveMQQueue">
        <!--消息队列名称, 用于标识-->
        <constructor-arg value="${activemq.destination}"/>
    </bean>

    <!-- 4. 构建 JMS 模板 -->
    <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate"
          p:connectionFactory-ref="cachedConnectionFactory"
          p:defaultDestination-ref="destination"
          p:sessionAcknowledgeMode="2"
          p:deliveryMode="2"></bean>

    <!--5.  配置消息监听器 -->
    <bean id="jmsHandler" class="com.kingfish.pojo.handler.JmsHandler"></bean>
    <jms:listener-container container-type="default" connection-factory="amqConnectionFactory" acknowledge="auto">
        <!-- 标识一个bean和一个可以处理消息的方法,当消息到达destination配置的队列时,method锁指定的方法会被触发 -->
        <jms:listener destination="kingfish" ref="jmsHandler" method="onMessage"></jms:listener>
    </jms:listener-container>
</beans>

在这里插入图片描述

解释:
1. 配置连接工厂
实际使用中,我们需要借助 连接工程 通过消息代理发送消息,因为选择了ActiveMQ作为我们的消息代理,所以配置ActiveMQ连接工厂。 p:brokerURL 配置了 连接的地址。默认情况下, ActiveMQConnectionFactory 会假设 ActiveMQ 代理监听localhost 的61616 端口。
2. 进行缓存配置

对上步创建的ConnectionFactory进行缓存包装,这样做的目的是提升性能,对sessions, connectionsproducers进行缓存复用,减少开销。

注:
ConnectionFactorySend操作之间,有三个中间对象被创建和销毁。 为了优化资源使用并提高性能,官方提供了两个ConnectionFactory的实现。

SingleConnectionFactorySpring 提供ConnectionFactory接口的一个实现,SingleConnectionFactory,它将在所有的createConnection调用中返回同一个的连接,并忽略close的调用。这在测试和独立的环境中相当有用,因为同一个连接可以被用于多个JmsTemplate调用以跨越多个事务。 SingleConnectionFactory接受一个通常来自 JNDI 的标准ConnectionFactory的引用。
CachingConnectionFactoryCachingConnectionFactory扩展了SingleConnectionFactory的功能,它添加了会话、消息生产者、消息消费者的缓存。 初始缓存大小设置为1,使用sessionCacheSize属性来增加缓存会话的数量。请注意,实际缓存会话的数量将超过该值,因为会话的缓存是基于确认模式的,因此当设置sessionCacheSize为1时,缓存的会话可能达到4个,因为每个确认模式都会缓存一个。当缓存的时候,消息生产者和消息消费者被缓存在他们自己的会话中同时也考虑到生产者和消费者的唯一属性。消息生产者基于他们的目的地被缓存,消息消费者基于目的地、选择器、非本地传送标识和持久订阅名称(假设创建持久消费者)的组合键被缓存。
  1. 创建消息队列
    即创建消息队列,通过并指定队列名。队列名称作为接收端的标识

  2. 构建 JMS 模板( 这里配置的是JmsTemplate 属性,由于本例中并未用到JmsTemplate 所以在本例中并无作用)
    指定了连接工厂和队列名称。
    sessionAcknowledgeMode : 配置了消息签收模式为手动签收
    在这里插入图片描述
    deliveryMode : 配置了开启持久化(默认开启持久化,所以不配置也行)
    在这里插入图片描述

  3. 配置消息监听器

对于 jms:listener-container 标签,他包含了jms:listener 标签。这里的 connection-factory 属性配置了对 connectionFactory 的引用。容器中的每个jms:listener都可以使用这个连接工厂进行监听。 connection-factory 的默认值是 "connectionFactory "。
acknowledge 用来配置消息签收机制。具体取值如下:
在这里插入图片描述

对于 jms:listener 标签。他标识一个bean和一个可以处理消息的方法,当消息到达destination配置的队列时,method所指定的方法会被触发

/**
 * @Des:  消息驱动bean Message Driven Bean
 */
public class JmsHandler implements MessageListener{

    @Override
    public void onMessage(Message message) {
        TextMessage textMessage = (TextMessage) message;
        try {
            System.out.println("收到的信息:" + textMessage.getText());
        } catch (JMSException e) {
        	// session.recover();// 继续重试
            e.printStackTrace();
        }
    }
}
  1. 创建一个消息发送者进行消息发送
public class JMSP2pProducer {
    public static void main(String[] args) throws JMSException {
        // 创建工厂类
        ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(ActiveMQConnectionFactory.DEFAULT_USER, ActiveMQConnectionFactory.DEFAULT_PASSWORD, ActiveMQConnectionFactory.DEFAULT_BROKER_BIND_URL);
        // 创建连接并启动
        Connection connection = connectionFactory.createConnection();
        connection.start();
        // 创建session
        Session session = connection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE);
        // 创建消息队列,指定目标地址
        Destination  destination = session.createQueue("kingfish");
        // 创建生产者
        MessageProducer producer = session.createProducer(destination);
        //   设置不持久化到本地
        producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);

        // 发送消息
        for (int i = 0; i < 10; i++) {
            TextMessage message = new ActiveMQTextMessage();
            message.setText("p2p 生产者生产 : " + i);
            System.out.println("p2p 生产者生产 : " + i);
            producer.send(destination, message);
        }
        connection.close();
    }
}
  1. 加载自定义的XML配置文件
/**
 * @Author : KingFish
 * @Email : XXX
 * @Data: 2019/8/24
 * @Des: WebApplicationInitializer  ---  Spring 容器启动之后会调用该接口的on startup方法
 */
@Configuration
public class MyWebApplicationInit implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
      
        ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("classpath:spring-activemq.xml");
    }
}

消息发送者发送的消息
生产者发送的消息

接收者收到的消息

在这里插入图片描述

七、 配置类方式配置ActiveMQ

package com.kingfish.common.config.mq;

import com.kingfish.pojo.handler.JmsHandler;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.command.ActiveMQQueue;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jms.annotation.EnableJms;
import org.springframework.jms.annotation.JmsListenerConfigurer;
import org.springframework.jms.config.JmsListenerContainerFactory;
import org.springframework.jms.config.JmsListenerEndpointRegistrar;
import org.springframework.jms.config.SimpleJmsListenerContainerFactory;
import org.springframework.jms.config.SimpleJmsListenerEndpoint;
import org.springframework.jms.connection.CachingConnectionFactory;
import org.springframework.jms.listener.adapter.MessageListenerAdapter;

import javax.jms.JMSException;

/**
 * @Data: 2019/10/11
 * @Des:
 */
@Configuration
@PropertySource("classpath:activemq.properties")
@EnableJms
public class ActiveMQConfig {
    @Value("${activemq.brokerURL}")
    private String brokerURL;

    @Value("${activemq.sessionCacheSize}")
    private int sessionCacheSize;

    @Value("${activemq.destination}")
    private String destination;


    @Bean
    public ActiveMQConnectionFactory activeMQConnectionFactory(){
        return new ActiveMQConnectionFactory(brokerURL);
    }

    @Bean
    public CachingConnectionFactory cachingConnectionFactory(ActiveMQConnectionFactory  activeMQConnectionFactory){
        CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory(activeMQConnectionFactory);
        cachingConnectionFactory.setSessionCacheSize(sessionCacheSize);
        return cachingConnectionFactory;
    }

    @Bean
    public ActiveMQQueue activeMQQueue(){
        return new ActiveMQQueue(destination);
    }

    @Bean
    public JmsListenerContainerFactory jmsListenerContainerFactory(CachingConnectionFactory cachingConnectionFactory){
        SimpleJmsListenerContainerFactory simpleJmsListenerContainerFactory = new SimpleJmsListenerContainerFactory();
        simpleJmsListenerContainerFactory.setConnectionFactory(cachingConnectionFactory);
        return simpleJmsListenerContainerFactory;
    }

    @Bean
    public JmsListenerConfigurer jmsListenerConfigurer(){
        return new JmsListenerConfigurer() {
            @Override
            public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) {
                // 方式1 : 直接处理
                SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint();
                endpoint.setId("0");
                endpoint.setDestination(destination);
                endpoint.setMessageListener(message -> {
                    // 直接在这里处理消息信息
                    try {
                        System.out.println("endpoint1处理消息的逻辑 : " +  message.getBody(String.class));
                    } catch (JMSException e) {
                        e.printStackTrace();
                    }
                });

                //方式2 使用适配器来处理消息 RabbitMQHandler 来处理信息
                SimpleJmsListenerEndpoint endpoint2 = new SimpleJmsListenerEndpoint();
                endpoint2.setId("1");
                endpoint2.setDestination(destination);
                System.out.println("endpoint2处理消息的逻辑");
                // 绑定pojo,并指定默认处理方法为onMessage
                MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(new JmsHandler());
                messageListenerAdapter.setDefaultListenerMethod("onMessage");
                endpoint2.setMessageListener(messageListenerAdapter);

                //注册endpoint
                registrar.registerEndpoint(endpoint);
                registrar.registerEndpoint(endpoint2);
            }
        };
    }
}

以上:内容参考
《Spring 实战》
https://blog.csdn.net/mgsky1/article/details/80024876
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫吻鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值