RocketMQ入门进阶全面总结

生产者消费者基本实现

新建maven工程并引入依赖

   <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-client</artifactId>
            <version>4.8.0</version>
        </dependency>

生产者

public class Producer {
    public static void main(String[] args) throws Exception {
        /**
         1. 谁来发?
         2. 发给谁?
         3. 怎么发?
         4. 发什么?
         5. 发的结果是什么?
         6. 打扫战场
         **/

        //1.创建一个发送消息的对象Producer
        DefaultMQProducer producer = new DefaultMQProducer("group2");
        //2.设定发送的命名服务器地址
        producer.setNamesrvAddr("39.99.141.194:9876");
        //3.1启动发送的服务
        producer.start();
        //4.创建要发送的消息对象,指定topic,指定内容body
        Message msg = new Message("topic1", "你好,Acerola".getBytes("UTF-8"));
        //3.2发送消息
        SendResult result = producer.send(msg);
        System.out.println("返回结果:" + result);
        //5.关闭连接
        producer.shutdown();
    }
}

消费者

public class Consumer {
    public static void main(String[] args) throws Exception {
        /**
         1. 谁来发?
         2. 发给谁?
         3. 怎么发?
         4. 发什么?
         5. 发的结果是什么?
         6. 打扫战场
         **/

        //1.创建一个接收消息的对象Consumer
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
        //2.设定接收的命名服务器地址
        consumer.setNamesrvAddr("39.99.141.194:9876");
        //3.设置接收消息对应的topic,对应的sub标签为任意
        consumer.subscribe("topic1", "*");
        //3.开启监听,用于接收消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                //遍历消息
                for (MessageExt msg : list) {
                    byte[] body = msg.getBody();
                    System.out.println(new String(body));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        //4.启动接收消息的服务
        consumer.start();
        System.out.println("接受消息服务已经开启!");
        //5 不要关闭消费者!
    }
}

集群模式

即集群模式下,同个主题对于不同组是“广播模式”,每个组都会消费同个消息;会重复消费;对于同个组是“集群模式”,每个消费者平坦消息,不会重复消费;

消息模式

同步消息

特征:即时性较强,重要的消息,且必须有回执的消息,例如短信,通知(转账成功)

SendResult result = producer.send(msg);

异步消息

特征:即时性较弱,但需要有回执的消息,例如订单中的某些信息

        //1.创建一个发送消息的对象Producer
        DefaultMQProducer producer = new DefaultMQProducer("group1");
        //2.设定发送的命名服务器地址
        producer.setNamesrvAddr("localhost:9876");
        //3.1启动发送的服务
        producer.start();
        for (int i = 0; i < 10; i++) {
            //4.创建要发送的消息对象,指定topic,指定内容body
            Message msg = new Message("topic1", ("hello rocketmq"+i).getBytes("UTF-8"));
            //3.2 同步消息
            //SendResult result = producer.send(msg);
            //System.out.println("返回结果:" + result);

            //异步消息
            producer.send(msg, new SendCallback() {
                //表示成功返回结果
                @Override
                public void onSuccess(SendResult sendResult) {
                    System.out.println(sendResult);
                }
                //表示发送消息失败
                @Override
                public void onException(Throwable throwable) {
                    System.out.println(throwable);
                }
            });
            
            System.out.println("消息"+i+"发完了,做业务逻辑去了!");
        }
        //休眠10秒
        TimeUnit.SECONDS.sleep(10);
        //5.关闭连接
        producer.shutdown();

单向消息

特征:不需要有回执的消息,例如日志类消息

producer.sendOneway(msg);

延时消息

消息发送时并不直接发送到消息服务器,而是根据设定的等待时间到达,起到延时到达的缓冲作用

Message msg = new Message("topic3",("延时消息:hello rocketmq "+i).getBytes("UTF-8"));
//设置延时等级3,这个消息将在10s之后发送(现在只支持固定的几个时间,详看delayTimeLevel)
msg.setDelayTimeLevel(3);
SendResult result = producer.send(msg);
System.out.println("返回结果:"+result);

目前支持的消息时间

private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";

批量消息

批量发送消息能显著提高传递小消息的性能.

       List<Message> msgList = new ArrayList<Message>();
        Message msg1 = new Message("topic1", ("hello rocketmq1").getBytes("UTF-8"));
        Message msg2 = new Message("topic1", ("hello rocketmq2").getBytes("UTF-8"));
        Message msg3 = new Message("topic1", ("hello rocketmq3").getBytes("UTF-8"));

        msgList.add(msg1);
        msgList.add(msg2);
        msgList.add(msg3);


        SendResult result = producer.send(msgList);
  1. 这些批量消息应该有相同的topic

  2. 相同的waitStoreMsgOK

  3. 不能是延时消息

  4. 消息内容总长度不超过4M

消息内容总长度包含如下:

  • topic(字符串字节数)
  • body (字节数组长度)
  • 消息追加的属性(key与value对应字符串字节数)
  • 日志(固定20字节)

消息过滤

分类过滤(Tag)

按照tag过滤信息

生产者

Message msg = new Message("topic6","tag2",("消息过滤按照tag:hello rocketmq 2").getBytes("UTF-8"));

消费者

//接收消息的时候,除了制定topic,还可以指定接收的tag,*代表任意tag
consumer.subscribe("topic6","tag1 || tag2");
语法过滤(属性过滤/语法过滤/SQL过滤,都是一个意思)

基本语法

  • 数值比较,比如:>,>=,<,<=,BETWEEN,=;
  • 字符比较,比如:=,<>,IN;
  • IS NULL 或者 IS NOT NULL;
  • 逻辑符号 AND,OR,NOT;

常量支持类型为:

  • 数值,比如:123,3.1415;
  • 字符,比如:‘abc’,必须用单引号包裹起来;
  • NULL,特殊的常量
  • 布尔值,TRUEFALSE

生产者

//为消息添加属性
msg.putUserProperty("vip","1");
msg.putUserProperty("age","20");

消费者

//使用消息选择器来过滤对应的属性,语法格式为类SQL语法
consumer.subscribe("topic7", MessageSelector.bySql("age >= 18"));
consumer.subscribe("topic6", MessageSelector.bySql("name = 'litiedan'"));

注意:SQL过滤需要依赖服务器的功能支持,在broker.conf配置文件中添加对应的功能项,并开启对应功能

enablePropertyFilter=true

重启broker,控制台查看开启与否

image-20221027231933540

springboot整合

环境搭建

引包
<dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <version>2.0.3</version>
        </dependency>
配置文件
server.port=8888
rocketmq.name-server=39.99.141.194:9876
rocketmq.producer.group=demo_producer
实体类
public class User implements Serializable {
    String userName;
    String userId;

    public User() {

    }

    public User(String userName, String userId) {
        this.userName = userName;
        this.userId = userId;
    }

    @Override
    public String toString() {
        return "demoEntity{" +
                "userName='" + userName + '\'' +
                ", userId='" + userId + '\'' +
                '}';
    }
}

springboot整合生产者

生产者类及各种消息类型
@RestController
public class DemoProducers {
    @Autowired
    private RocketMQTemplate template;

    @RequestMapping("/producer")
    public String producersMessage() {
        User user = new User("sharfine", "123456789");
        template.convertAndSend("demo-topic", user);
        //同步消息
        template.syncSend("demo-topic",user);
        //异步消息
        template.asyncSend("demo-topic", user, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                System.out.println(sendResult);
            }

            @Override
            public void onException(Throwable throwable) {
                System.out.println(throwable);
            }
        });
        //单向消息
        template.sendOneWay("demo-topic",user);
        //延时消息
        template.syncSend("topic9", MessageBuilder.withPayload("test delay").build(),2000,2);
        //批量消息
        List<Message> msgList = new ArrayList<>();
        msgList.add(new Message("topic6", "tag1", "msg1".getBytes()));
        msgList.add(new Message("topic6", "tag1", "msg2".getBytes()));
        msgList.add(new Message("topic6", "tag1", "msg3".getBytes()));
        template.syncSend("topic8",msgList,1000);

        return JSON.toJSONString(user);
    }
}

springboot整合消费者

消费者类
@Service
@RocketMQMessageListener(topic = "demo-topic", consumerGroup = "demo_consumer")
public class DemoConsumers1 implements RocketMQListener<User> {
    @Override
    public void onMessage(User user) {
        System.out.println("Consumers1接收消息:" + user.toString());
    }
}

消息过滤

tag过滤

生产者

SendResult result = rocketMQTemplate.syncSend("topic1:tag1", paload);

消费者

@RocketMQMessageListener(topic = "topic9",consumerGroup = "group1",selectorExpression = "tag1")

sql过滤

@RocketMQMessageListener(topic = "topic9",consumerGroup = "group1",selectorExpression = "age>18"
        ,selectorType= SelectorType.SQL92)
消息模式

改广播模式

@RocketMQMessageListener(topic = "topic9",consumerGroup = "group1",messageModel = MessageModel.BROADCASTING)

RocketMQ进阶

消息的特殊处理

错乱的消息顺序

原因

消息有序指的是可以按照消息的发送顺序来消费(FIFO)。RocketMQ可以严格的保证消息有序,可以分为分区有序或者全局有序。

顺序消费的原理解析,在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列);而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。

下面用订单进行分区有序的示例。一个订单的顺序流程是:创建、付款、推送、完成。订单号相同的消息会被先后发送到同一个队列中,消费时,同一个OrderId获取到的肯定是同一个队列。

image-20221027231944291

先读到第一个订单的创建和完成消息

image-20221027231950469

想要的效果

image-20221027231956597

顺序消息

订单步骤实体类

package com.ydl.order.domain;

/**
 * 订单的步骤
 */
public class OrderStep {
    private long orderId;
    private String desc;

    public long getOrderId() {
        return orderId;
    }

    public void setOrderId(long orderId) {
        this.orderId = orderId;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    @Override
    public String toString() {
        return "OrderStep{" +
                "orderId=" + orderId +
                ", desc='" + desc + '\'' +
                '}';
    }
}

发送消息

package com.ydl.order;

import com.ydl.order.domain.OrderStep;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class Producer {
    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("group1");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();

        List<OrderStep> orderList = new Producer().buildOrders();

        //设置消息进入到指定的消息队列中
        for (final OrderStep order : orderList) {
            Message msg = new Message("topic1", order.toString().getBytes());
            //发送时要指定对应的消息队列选择器
            SendResult result = producer.send(msg, new MessageQueueSelector() {
                //设置当前消息发送时使用哪一个消息队列
                public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
                    //根据发送的信息不同,选择不同的消息队列
                    //根据id来选择一个消息队列的对象,并返回->id得到int值
                    long orderId = order.getOrderId();
                    long mqIndex = orderId % list.size();
                    return list.get((int) mqIndex);
                }
            }, null);
            System.out.println(result);
        }

        producer.shutdown();


        //for (int i = 0; i < 10; i++) {
        //    Message msg = new Message("topic1", ("hello rocketmq"+i).getBytes("UTF-8"));
        //    SendResult result = producer.send(msg);
        //    System.out.println("返回结果:" + result);
        //}
    }

    /**
     * 生成模拟订单数据
     */
    private List<OrderStep> buildOrders() {
        List<OrderStep> orderList = new ArrayList<OrderStep>();

        OrderStep orderDemo = new OrderStep();
        orderDemo.setOrderId(1L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(2L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(1L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(3L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(2L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(3L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(2L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(1L);
        orderDemo.setDesc("推送");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(3L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(1L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);

        return orderList;
    }
}

接收消息

         //使用单线程的模式从消息队列中取数据,一个线程绑定一个消息队列
		consumer.registerMessageListener(new MessageListenerOrderly() {
            //使用MessageListenerOrderly接口后,对消息队列的处理由一个消息队列多个线程服务,转化为一个消息队列一个线程服务
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) {
                for (MessageExt msg : list) {
                    System.out.println(Thread.currentThread().getName()+"。消息:" + new String(msg.getBody())+"。queueId:"+msg.getQueueId());
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });

事务

  1. 正常事务过程
  2. 事务补偿过程

image-20221027232007480

事务消息状态
  1. 提交状态:允许进入队列,此消息与非事务消息无区别
  2. 回滚状态:不允许进入队列,此消息等同于未发送过
  3. 中间状态:完成了half消息的发送,未对MQ进行二次状态确认
  4. 注意:事务消息仅与生产者有关,与消费者无关
事务消息

提交状态

//事务消息使用的生产者是TransactionMQProducer
TransactionMQProducer producer = new TransactionMQProducer("group1");
producer.setNamesrvAddr("localhost:9876");
//添加本地事务对应的监听
producer.setTransactionListener(new TransactionListener() {
//正常事务过程
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
return LocalTransactionState.COMMIT_MESSAGE;
}
//事务补偿过程
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
return null;
}
});
producer.start();
Message msg = new Message("topic8",("事务消息:hello rocketmq ").getBytes("UTF-8"));
SendResult result = producer.sendMessageInTransaction(msg,null);
System.out.println("返回结果:"+result);
producer.shutdown();

回滚状态

            producer.setTransactionListener(new TransactionListener() {
            //正常事务
            @Override
            public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
                return LocalTransactionState.ROLLBACK_MESSAGE;
            }
            //事务补偿
            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt msg) {
                return null;
            }
        });

中间状态

public static void main(String[] args) throws Exception {
        TransactionMQProducer producer=new TransactionMQProducer("group1");
        producer.setNamesrvAddr("localhost:9876");
        producer.setTransactionListener(new TransactionListener() {
            //正常事务
            @Override
            public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
                return LocalTransactionState.UNKNOW;
            }
            //事务补偿 正常执行UNKNOW才会触发
            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt msg) {
                System.out.println("事务补偿");
                return LocalTransactionState.COMMIT_MESSAGE;
            }
        });
        producer.start();
        Message msg = new Message("topic13", "hello rocketmq".getBytes("UTF-8"));
        SendResult result = producer.sendMessageInTransaction(msg, null);
        System.out.println("返回结果:" + result);

        //事务补偿生产者一定要一直启动着
        //producer.shutdown();
    }

RocketMQ集群

RocketMQ高级特性

消息的存储
  1. 消息生成者发送消息到MQ
  2. MQ返回ACK给生产者
  3. MQ push 消息给对应的消费者
  4. 消息消费者返回ACK给MQ

说明:ACK(Acknowledge character)

image-20221027232015609

消息的存储
  1. 消息生成者发送消息到MQ
  2. MQ收到消息,将消息进行持久化,存储该消息
  3. MQ返回ACK给生产者
  4. MQ push 消息给对应的消费者
  5. 消息消费者返回ACK给MQ
  6. MQ删除消息

注意:

  1. 第⑤步MQ在指定时间内接到消息消费者返回ACK,MQ认定消息消费成功,执行⑥
  2. 第⑤步MQ在指定时间内未接到消息消费者返回ACK,MQ认定消息消费失败,重新执行④⑤⑥

image-20201211162231313

消息的存储介质
  1. 数据库
    1. ActiveMQ
    2. 缺点:数据库瓶颈将成为MQ瓶颈
  2. 文件系统
    1. RocketMQ/Kafka/RabbitMQ
    2. 解决方案:采用消息刷盘机制进行数据存储
    3. 缺点:硬盘损坏的问题无法避免

image-20221027232022713

高效的消息存储与读写方式
  1. SSD(Solid State Disk)

    image-20221027232029211

    1. 随机写(100KB/s)

      image-20221027232035327

    2. 顺序写 (600MB/s)1秒1部电影

image-20221027232041928

image-20221027232048799

  1. Linux系统发送数据的方式

  2. “零拷贝”技术

    1. 数据传输由传统的4次复制简化成3次复制,减少1次复制过程
    2. Java语言中使用MappedByteBuffer类实现了该技术
    3. 要求:预留存储空间,用于保存数据(1G存储空间起步)

    image-20221027232054247

消息存储结构
  1. MQ数据存储区域包含如下内容
    1. 消息数据存储区域
      1. topic
      2. queueId
      3. message
    2. 消费逻辑队列
      1. minOffset
      2. maxOffset
      3. consumerOffset
    3. 索引
      1. key索引
      2. 创建时间索引

image-20221027232101400

刷盘机制
  1. 同步刷盘

    1. 生产者发送消息到MQ,MQ接到消息数据

    2. MQ挂起生产者发送消息的线程

    3. MQ将消息数据写入内存

    4. 内存数据写入硬盘

    5. 磁盘存储后返回SUCCESS

    6. MQ恢复挂起的生产者线程

    7. 发送ACK到生产者

      image-20221027232106121

  2. 异步刷盘

    1. 生产者发送消息到MQ,MQ接到消息数据
    2. MQ将消息数据写入内存
    3. 发送ACK到生产者

image-20221027232111377

  1. 同步刷盘:安全性高,效率低,速度慢(适用于对数据安全要求较高的业务)
  2. 异步刷盘:安全性低,效率高,速度快(适用于对数据处理速度要求较高的业务)

配置方式

#刷盘方式
#- ASYNC_FLUSH 异步刷盘
#- SYNC_FLUSH 同步刷盘
flushDiskType=SYNC_FLUSH
高可用性
  1. nameserver
    1. 无状态+全服务器注册
  2. 消息服务器
    1. 主从架构(2M-2S)
  3. 消息生产
    1. 生产者将相同的topic绑定到多个group组,保障master挂掉后,其他master仍可正常进行消 息接收
  4. 消息消费
    1. RocketMQ自身会根据master的压力确认是否由master承担消息读取的功能,当master繁忙 时候,自动切换由slave承担数据读取的工作
主从数据复制
  1. 同步复制

    1. master接到消息后,先复制到slave,然后反馈给生产者写操作成功
    2. 优点:数据安全,不丢数据,出现故障容易恢复
    3. 缺点:影响数据吞吐量,整体性能低
  2. 异步复制

    1. master接到消息后,立即返回给生产者写操作成功,当消息达到一定量后再异步复制到slave
    2. 优点:数据吞吐量大,操作延迟低,性能高
    3. 缺点:数据不安全,会出现数据丢失的现象,一旦master出现故障,从上次数据同步到故障时间的数据将丢失
  3. 配置方式

    #Broker 的角色
    #- ASYNC_MASTER 异步复制Master
    #- SYNC_MASTER 同步双写Master
    #- SLAVE
    brokerRole=SYNC_MASTER
    
负载均衡
  1. Producer负载均衡
    1. 内部实现了不同broker集群中对同一topic对应消息队列的负载均衡
  2. Consumer负载均衡
    1. 平均分配
    2. 循环平均分配
  3. 广播模式(不参与负载均衡)

image-20221027232115825

image-20221027232118911

image-20221027232121316

消息重试
  1. 当消息消费后未正常返回消费成功的信息将启动消息重试机制
  2. 消息重试机制
    1. 顺序消息
    2. 无序消息
顺序消息重试

image-20221027232125880

image-20201211163854405

  1. 当消费者消费消息失败后,RocketMQ会自动进行消息重试(每次间隔时间为 1 秒)
  2. 注意:应用会出现消息消费被阻塞的情况,因此,要对顺序消息的消费情况进行监控,避免阻塞现象的发生

image-20221027232132278

image-20221027232135445

无序消息重试
  1. 无序消息包括普通消息、定时消息、延时消息、事务消息
  2. 无序消息重试仅适用于负载均衡(集群)模型下的消息消费,不适用于广播模式下的消息消费
  3. 为保障无序消息的消费,MQ设定了合理的消息重试间隔时长

image-20221027232138523

死信队列
  1. 当消息消费重试到达了指定次数(默认16次)后,MQ将无法被正常消费的消息称为死信消息(Dead-Letter Message)
  2. 死信消息不会被直接抛弃,而是保存到了一个全新的队列中,该队列称为死信队列(Dead-Letter Queue)\
  3. 死信队列特征
    1. 归属某一个组(Gourp Id),而不归属Topic,也不归属消费者
    2. 一个死信队列中可以包含同一个组下的多个Topic中的死信消息
    3. 死信队列不会进行默认初始化,当第一个死信出现后,此队列首次初始化
  4. 死信队列中消息特征
    1. 不会被再次重复消费
    2. 死信队列中的消息有效期为3天,达到时限后将被清除
死信处理
  1. 在监控平台中,通过查找死信,获取死信的messageId,然后通过id对死信进行精准消费
消息重复消费
  1. 消息重复消费原因
    1. 生产者发送了重复的消息
      1. 网络闪断
      2. 生产者宕机
    2. 消息服务器投递了重复的消息
      1. 网络闪断
    3. 动态的负载均衡过程
      1. 网络闪断/抖动
      2. broker重启
      3. 订阅方应用重启(消费者)
      4. 客户端扩容
      5. 客户端缩容

image-20221027232142453

消息幂等
  1. 对同一条消息,无论消费多少次,结果保持一致,称为消息幂等性
  2. 解决方案
    1. 使用业务id作为消息的key
    2. 在消费消息时,客户端对key做判定,未使用过放行,使用过抛弃
  3. 注意:messageId由RocketMQ产生,messageId并不具有唯一性,不能作用幂等判定条件
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Acerola-

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

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

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

打赏作者

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

抵扣说明:

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

余额充值