浅“尝”RocketMQ(二)

一.顺序发送

  • 概念:

顺序消息是一种对消息发送和消费顺序有严格要求的消息。

对于一个指定的Topic,消息严格按照先进先出(FIFO)的原则进行消息发布和消费,即先发布的消息先消费,后发布的消息后消费。在 Apache RocketMQ 中支持分区顺序消息,如下图所示。我们可以按照某一个标准对消息进行分区,同一个ShardingKey的消息会被分配到同一个队列中,并按照顺序被消费。

需要注意的是 RocketMQ 消息的顺序性分为两部分,生产顺序性和消费顺序性。只有同时满足了生产顺序性和消费顺序性才能达到上述的FIFO效果。

  • 理解:

        可以理解为微信或者QQ发送消息的场景,当他人给我们发送消息时,那一定是一条条发的,先发送过来的我们就会先接受,后发送过来的后接受。当同时有两个人一起给我们发送微信时,我们并不在意谁的消息先到,只在意每个人发送的消息和接受的顺序保持一致即可。

  • 顺序发送生产者实例代码:
@Test
    public void OrderProducer() throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("OrderProducer");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();

        //利用两次循环测试生产者消息是顺序发布的
        for (int i = 0; i < 5; i++) {
            for (int j = 0; j < 6; j++) {
                Message message = new Message("Order", "TagA", ("order_" + i + "_number_" + j).getBytes(StandardCharsets.UTF_8));
                SendResult sendResult = producer.send(message, new MessageQueueSelector() {
                    @Override
                    //send()方法接受三个参数:message(要发送的消息对象),selector(消息队列选择器),arg(选择器参数)。
                    // 在这个示例中,使用了匿名内部类实现了MessageQueueSelector接口,并重写了select()方法。
                    // select()方法根据选择器参数i,计算出消息应该发送到哪个消息队列,并返回对应的消息队列
                    public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
                        Integer id = (Integer) o;
                        int index = id % list.size();
                        return list.get(index);
                    }
                }, i);
                System.out.println("消息发送成功" + sendResult);
            }

        }
        producer.shutdown();
    }
  • 顺序消费实例代码:
     @Test
        public void OrderConsumer() throws MQClientException {
            // 创建一个默认的消息消费者
            DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("SimpleConsumer");
            // 设置NameServer地址
            consumer.setNamesrvAddr("localhost:9876");
            // 订阅主题和标签,消费所有消息
            consumer.subscribe("Order", "*");
            // 设置消息监听器
            consumer.setMessageListener(new MessageListenerOrderly() {
                @Override
                public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) {
                    for (int i = 0; i < list.size(); i++) {
                        System.out.println(i+"消息消费成功"+new String(list.get(i).getBody()));
                    }
                    return ConsumeOrderlyStatus.SUCCESS;
                }
            });
            //启动消费者
            consumer.start();
            System.out.println("启动成功");
            while (true);
        }

    二.延迟发送

  • 延迟消息发送是指消息发送到Apache RocketMQ后,并不期望立马投递这条消息,而是延迟一定时间后才投递到Consumer进行消费。

    在分布式定时调度触发、任务超时处理等场景,需要实现精准、可靠的延时事件触发。使用 RocketMQ 的延时消息可以简化定时调度任务的开发逻辑,实现高性能、可扩展、高可靠的定时触发能力。

  • 延迟发送与普通发送代码上不同的就在于多了一个延迟,消费者等待一段时间之后才能接收到。

  • 延迟发送代码示例:
public void sendTest3() throws MQBrokerException, RemotingException, InterruptedException, MQClientException {
        //本次代码实现延迟发送
        //准备一个生产者对象,并对其分组命名
        DefaultMQProducer producer = new DefaultMQProducer("sndTest4");
        //连接nameserver,同启动broker时的 mqbroker.cmd -n localhost:9876的地址保持一致
        producer.setNamesrvAddr("localhost:9876");
        //生产者producer于nameServer建立连接
        producer.start();

        //将消息同步发送
        for (int i=0;i<2;i++){
            //创建一个消息对象,主题topic按业务起名即可,
            //tag对消息进行在分类,RocketMQ可以在消费端对tag进行过滤。
            //携带消息的body
            Message message =
                    new Message("Simple4",i+"sndTest4","延迟发送消息".getBytes(StandardCharsets.UTF_8));
            //延时发送等级
            message.setDelayTimeLevel(3);

            //通过 SendResult 对象获取发送结果和相关信息
            producer.send(message);
            System.out.println(i+"_消息发送成功"+ LocalTime.now());


        }
        //关闭 Producer 的主要原因有以下几点:
        //资源释放:关闭 Producer 可以释放占用的资源,包括网络连接、线程等。这样可以避免资源的浪费和占用。
        //优雅退出:关闭 Producer 可以保证程序的正常退出。在关闭 Producer 之前,可以先发送一个特殊的消息,
        // 通知 Consumer 停止消费,然后再关闭 Producer。这样可以避免消息丢失和消费者无法正常停止的问题。
        //避免内存泄漏:如果不关闭 Producer,可能会导致内存泄漏问题。
        // Producer 内部可能会缓存一些数据,如果不及时释放,可能会导致内存占用过高。
        producer.shutdown();
    }

三.批量发送

  • 在对吞吐率有一定要求的情况下,A可以将一些消息聚成一批以后进行发送,可以增加吞吐率,并减少API和网络调用次数。
  • RocketMQ对于批量消息有大小限制,默认情况下,批量消息的总大小不能超过4MB。如果批量消息的总大小超过了限制,将会导致发送失败。发送的顺序也是无序的。相同的Topic。
  • 实现起来将消息打包成 Collection<Message> msgs 传入方法中即可。
  • 代码示例:
public class SimpleBatchProducer {

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

        
        String topic = "BatchTest";
        //定义一个集合,将要发送的消息放到集合中统一发送
        List<Message> messages = new ArrayList<>();
        messages.add(new Message(topic, "Tag", "OrderID001", "Hello world 0".getBytes()));
        messages.add(new Message(topic, "Tag", "OrderID002", "Hello world 1".getBytes()));
        messages.add(new Message(topic, "Tag", "OrderID003", "Hello world 2".getBytes()));

        producer.send(messages);
    }
}
  • 当消息超出时需要进行拆分 
  • 代码示例:
 @Test
    public void testExecute() throws MQClientException, MQBrokerException, RemotingException, InterruptedException {


        DefaultMQProducer producer = new DefaultMQProducer("BatchProducerGroupName");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();


        String topic = "BatchTest";
        //定义一个集合,将要发送的消息放到集合中统一发送
        List<Message> messages = new ArrayList<>();
        messages.add(new

                Message(topic, "Tag", "OrderID001", "Hello world 0".getBytes()));
        messages.add(new

                Message(topic, "Tag", "OrderID002", "Hello world 1".getBytes()));
        messages.add(new

                Message(topic, "Tag", "OrderID003", "Hello world 2".getBytes()));

        //进行切割
        ListSplitter listSplitter = new ListSplitter(messages);
        while (listSplitter.hasNext()) {
            producer.send(messages);
        }

        producer.shutdown();
    }

    class ListSplitter implements Iterator<List<Message>> {

        //消息最大容量
        private static final int SIZE_LIMIT = 40 * 1000;
        //带发送消息泪列表
        private final List<Message> messages;
        //当前切割的下标
        private int curryIndex;

        ListSplitter(List<Message> messages) {
            this.messages = messages;
        }


        @Override
        public boolean hasNext() {
            //当前切割位置小于集合长度
            return curryIndex < messages.size();
        }

        @Override
        public List<Message> next() {
            int nextIndex = curryIndex;
            //消息大小
            int totalSize = 0;
            for (; nextIndex < messages.size(); nextIndex++) {
                //当前消息
                Message message = messages.get(nextIndex);
                //消息长度
                int messageSize = message.getBody().length + message.getTopic().length();
                Map<String, String> properties = message.getProperties();
                //迭代
                Iterator<Map.Entry<String, String>> iterator =
                        properties.entrySet().iterator();
                while (iterator.hasNext()) {
                    //计算消息的大小,包括消息体和消息属性的大小
                    messageSize += iterator.next().getKey().length() + iterator.next().getValue().length();
                }
                //考虑消息的一些额外开销或者预留一些空间。
                messageSize = messageSize + 20;
                //大于限制
                if (messageSize > SIZE_LIMIT) {
                    //第一次发送超过限制,直接跳过这条消息继续扫描
                    if(nextIndex - curryIndex == 0){
                        nextIndex++;
                    }
                    break;
                }
                //如果当前发送列表超出限制,暂时添加消息
                if (messageSize + totalSize > SIZE_LIMIT) {
                    break;
                }else {
                    totalSize += messageSize;
                }

            }
            //subList()方法获取当前切割列表,并将nextIndex赋值给curryIndex,
            // 表示下一次切割的起始下标。最后返回切割列表。
            List<Message> messages1 = messages.subList(curryIndex, nextIndex);
            curryIndex = nextIndex;
            return messages1;
        }
    }

四.事务发送

1.事务简介:

事务通常具有四个特性,即ACID: 原子性(Atomicity):事务中的所有操作要么全部成功,要么全部失败回滚,不允许出现部分操作成功的情况。 一致性(Consistency):事务执行前和执行后,数据库始终处于一致的状态,即数据的完整性约束没有被破坏。 隔离性(Isolation):多个事务并发执行时,每个事务的执行都不会受到其他事务的影响。 持久性(Durability):事务一旦提交,其结果就是永久性的,即使系统发生故障也不会丢失。 综上所述,事务是一组操作被视为一个不可分割的单元,要么全部执行成功,要么全部失败回滚。事务的目的是保证数据的一致性和完整性,通常具有四个特性:原子性、一致性、隔离性和持久性。

2.事务发送执行流程:

  •  介绍半事务消息:

半事务消息(Half-Transaction Message)是一种消息传递模式,用于在分布式系统中实现最终一致性。它结合了事务消息和可靠消息传递的特性。

在传统的事务消息中,消息发送方和消息接收方之间通过事务来保证消息的可靠传递和处理。但是,在分布式系统中,跨多个服务的事务管理变得复杂且性能开销较大。

半事务消息的思想是将事务拆分为两个阶段:发送阶段和确认阶段。在发送阶段,消息发送方将消息发送到消息中间件,但并不立即提交事务。而是等待接收方的确认。在确认阶段,接收方接收到消息后进行处理,并向消息中间件发送确认消息。一旦接收方发送了确认消息,消息中间件就会将消息标记为已确认,并提交事务。如果接收方在一定时间内没有发送确认消息,消息中间件会将消息标记为未确认,并进行相应的处理(例如重试或补偿)。

半事务消息的优点是可以在分布式系统中实现最终一致性,同时减少了事务的开销和复杂性。它适用于需要保证消息可靠传递和处理的场景,如订单支付、库存扣减等。常见的半事务消息的实现方式包括消息队列、分布式事务框架等。

  • 事务消息发送步骤如下:

1.生产者将半事务消息发送至 RocketMQ Broker

2.RocketMQ Broker 将消息持久化成功之后,向生产者返回 Ack 确认消息已经发送成功,此时消息暂不能投递,为半事务消息。

3.生产者开始执行本地事务逻辑。

4.生产者根据本地事务执行结果向服务端提交二次确认结果(Commit或是Rollback),服务端收到确认结果后处理逻辑如下:

  • 二次确认结果为Commit:服务端将半事务消息标记为可投递,并投递给消费者。
  • 二次确认结果为Rollback:服务端将回滚事务,不会将半事务消息投递给消费者。

 

5.在断网或者是生产者应用重启的特殊情况下,若服务端未收到发送者提交的二次确认结果,或服务端收到的二次确认结果为Unknown未知状态,经过固定时间后,服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查。

6.:::note 需要注意的是,服务端仅仅会按照参数尝试指定次数,超过次数后事务会强制回滚,因此未决事务的回查时效性非常关键,需要按照业务的实际风险来设置 :::

事务消息回查步骤如下:

7. 生产者收到消息回查后,需要检查对应消息的本地事务执行的最终结果。

8. 生产者根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半事务消息进行处理。

3.代码示例:

  • 事务消息生产者:
 @Test
    public void TransactionProducer() throws MQClientException, InterruptedException {
        TransactionMQProducer producer = new TransactionMQProducer("TransactionProducer");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();
        //异步提交事务状态
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2000), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("xiancheng");
                return null;
            }
        });

        producer.setExecutorService(threadPoolExecutor);
        //本地事件监听器
        producer.setTransactionListener(new TransactionListImpl());
        String[] tags = new String[]{"TagA", "TagB", "TagC", "TagD", "TagE"};
        for (int i = 0; i < 5; i++) {
            Message message =
                    new Message("Transaction", tags[i % tags.length],
                            (tags[i % tags.length] + "事务消息").getBytes(StandardCharsets.UTF_8));
            TransactionSendResult transactionSendResult = producer.sendMessageInTransaction(message, null);
            System.out.println("消息发送成功_" + transactionSendResult);
            Thread.sleep(10);
        }
        Thread.sleep(200000);
        producer.shutdown();
    }
  • 本地事务处理器代码示例:
package com.tarena.csmall.rocket;

import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;

/**
 * 本地事件监听器
 * DATE = 2023/7/25 23:18
 */
public class TransactionListImpl implements TransactionListener {
    /**
     * 执行本地事务
     *
     * @param message
     * @param o
     * @return
     */
    @Override
    public LocalTransactionState executeLocalTransaction(Message message, Object o) {
        //模拟消息状态
        String tags = message.getTags();
        //用于判断字符串 tags 是否包含子字符串 "TagA"
        if (StringUtils.contains("TagA", tags)) {
            //提交
            return LocalTransactionState.COMMIT_MESSAGE;
        }
        if (StringUtils.contains("TagB", tags)) {
            //回滚
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }else {
            //无状态
            return LocalTransactionState.UNKNOW;
        }



    }

    /**
     * 回查本地事务
     * @param messageExt
     * @return
     */
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
        String tags = messageExt.getTags();
        //用于判断字符串 tags 是否包含子字符串 "TagA"
        if (StringUtils.contains("TagC", tags)) {
            //提交
            return LocalTransactionState.COMMIT_MESSAGE;
        }
        if (StringUtils.contains("TagD", tags)) {
            //回滚
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }else {
            //无状态
            return LocalTransactionState.UNKNOW;
        }
    }
}
  • 以上代码逻辑:根据不同的Tags中的消息,提交到本地事务,根据Tags判断状态,来模拟事务发送

五.RocketMQ面试题

1.RocketMQ如何保证消息不丢失:

  • 消息丢失情况:
  • 1.生产者发送消息可能出现丢失,网络中断等
  • 2.消费者消费异常,因提前发挥ACK,如果消费异常了,Broker以为成功
  • 3.Broker存储阶段,异步刷盘出现问题
  1. 上述问题1---解决:同步发送,实时返回结果,拿到结果后,可以去查询检查是否发送成功。发送失败,可以重试。异步发送有两种回调机制:onSuccess发送成功,可以查询检查是否成功,可以进行重试;onException发送失败,进行重试。
  2. 上述问题2--解决:先消息消费成功后,返回ACK(用于确认接收到数据的信号或消息),维护偏移量
  3. 上述问题3--解决:
  1. 配置同步刷盘:将Broker的刷盘模式从异步刷盘改为同步刷盘。这样在消息写入到磁盘之前,会等待确认写入成功后再返回响应,确保数据的持久化。

  2. 增加刷盘频率:增加Broker的刷盘频率,可以通过调整刷盘策略的参数来实现。例如,可以将刷盘间隔时间缩短,或者增加每次刷盘的数据量,以提高数据的持久化效率。

  3. 使用高可靠性存储设备:使用高可靠性的存储设备,如RAID阵列或SSD硬盘,可以提高数据的持久化能力,减少数据丢失的风险。

  4. 数据备份和冗余:定期进行数据备份,并在多个节点上进行数据冗余存储,以防止单点故障导致的数据丢失。

  5. 监控和报警:建立监控系统,实时监测Broker的刷盘状态和性能指标,及时发现异常情况并进行报警处理。

  6. 异常处理和数据恢复:当发生消息丢失时,及时进行异常处理和数据恢复。可以通过日志分析、数据重放等方式来尽可能地恢复丢失的消息。

2.消息持久化机制

  1. 写入磁盘:

    • 当消息发送到RocketMQ的Broker节点时,会将消息写入磁盘。RocketMQ使用顺序写入的方式将消息持久化到磁盘上,以提高写入性能。
    • RocketMQ将消息分为CommitLog和ConsumeQueue两部分进行持久化。CommitLog是消息的主要存储,用于存储消息的内容和元数据。ConsumeQueue用于存储消息的消费进度和索引信息。
  2. 刷盘机制:

    • RocketMQ采用了异步刷盘的方式进行消息持久化。当消息写入到内存中后,会异步地将消息刷写到磁盘上。这样可以提高写入性能,但也会增加一定的消息丢失的风险。
    • RocketMQ使用了内存映射文件(MappedFile)的方式进行刷盘操作。当消息写入到内存中后,会将内存中的数据刷写到MappedFile中,并通过文件同步(fsync)操作将数据刷写到磁盘上。
  3. 主从复制:

    • RocketMQ支持主从复制机制,即将消息存储在主节点上,并将消息复制到多个从节点上。这样即使主节点发生故障,也可以从从节点上恢复消息。
    • 主从复制采用了同步复制的方式,即主节点将消息写入磁盘后,会通知从节点进行复制操作。从节点会从主节点拉取消息,并将消息写入磁盘进行持久化。
  4. 定期检查点:

    • RocketMQ会定期生成检查点文件,记录消息的偏移量和索引信息。这样在Broker节点重启后,可以通过检查点文件来恢复消息。
    • 检查点文件记录了CommitLog和ConsumeQueue的偏移量,以及消息的索引信息。在Broker节点重启后,RocketMQ会根据检查点文件来恢复消息的消费进度和索引信息。

3.如何保证消息顺序

此处不理解可以参考顺序发送代码!!!!

  1. Producer端顺序发送:

    • 在发送消息时,可以指定消息的顺序标识(OrderKey),相同顺序标识的消息会被发送到同一个队列中。
    • Producer端可以通过自定义消息队列选择器(MessageQueueSelector)来保证相同顺序标识的消息发送到同一个队列中。
  2. Broker端顺序存储:

    • RocketMQ的消息存储单元是一个个的CommitLog(顺序写入的日志文件,用于存储消息的内容和元数据;包括消息的主题、标签、键、消息体等;消息的偏移量、存储时间戳、消息长度等;包括消息的状态标识,如已提交、待提交等)文件,每个CommitLog文件包含多个消息。
    • 当消息写入到CommitLog文件时,RocketMQ会根据消息的顺序标识将消息存储到对应的文件中,保证相同顺序标识的消息按顺序存储。
  3. Consumer端顺序消费:

    • 在消费消息时,可以指定消费模式为顺序消费(Orderly),这样消费者会按照消息的顺序标识来顺序消费消息。
    • 消费者可以通过实现MessageListenerOrderly接口来处理顺序消费逻辑,确保相同顺序标识的消息按顺序处理。

需要注意的是,RocketMQ只能保证相同顺序标识的消息在发送和存储过程中是有序的,但无法保证不同顺序标识的消息之间的顺序关系。此外,顺序消息的性能可能会受到限制,因为消息会被发送到同一个队列中,可能会导致消息的负载不均衡。

4.事务消息原理

此处不理解可以参考事务发送代码!!!

  1. Producer端发送事务消息:

    • Producer发送事务消息时,首先会将消息发送到Broker,并标记为“Half Message”(半消息)状态。
    • 半消息会被存储在Broker的CommitLog中,但不会被消费者消费。
  2. Producer端执行本地事务:

    • 在发送半消息后,Producer会执行本地事务逻辑。
    • 本地事务逻辑可以是数据库操作、文件操作等,用于保证消息发送和业务逻辑的原子性。
  3. Producer端提交或回滚事务:

    • 当本地事务逻辑执行成功后,Producer可以选择提交事务。
    • 如果本地事务逻辑执行失败或超时,Producer可以选择回滚事务。
  4. Broker端处理事务消息:

    • 当Producer提交事务时,Broker会将半消息标记为“可消费”状态。
    • 当Producer回滚事务时,Broker会将半消息标记为“不可消费”状态。
  5. Consumer端消费事务消息:

    • Consumer在消费事务消息时,会先检查消息的状态。
    • 如果消息状态为“可消费”,则将消息发送给消费者进行消费。
    • 如果消息状态为“不可消费”,则不会将消息发送给消费者。

通过以上的机制,RocketMQ实现了事务消息的原子性。如果Producer在执行本地事务时失败或超时,可以选择回滚事务,保证消息不会被消费。如果Producer成功提交事务,Broker会将消息标记为可消费状态,然后Consumer可以消费该消息。这样就保证了消息发送和消息处理的原子性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
RocketMQ是一个分布式消息中间件,而二次封装是指在RocketMQ的基础上进行二次开发,将其封装成更易用、更高效的工具。下面是RocketMQ二次封装的核心要点: 1. 封装RocketMQTemplate:RocketMQTemplate是RocketMQ的核心类,用于发送消息。在二次封装中,可以对其进行增强,例如添加消息发送前的校验、消息发送后的回调等功能。 2. 封装RocketMQListener:RocketMQListener是RocketMQ的消息监听器,用于接收消息。在二次封装中,可以对其进行封装,例如添加消息接收前的校验、消息接收后的处理等功能。 3. 广播消息的应用场景:在RocketMQ中,广播消息是指消息被所有消费者都接收到。在二次封装中,可以根据实际应用场景,对广播消息进行封装,例如添加消息过滤器、消息去重等功能。 下面是RocketMQ二次封装的代码示例: ```java // 封装RocketMQTemplate public class MyRocketMQTemplate extends RocketMQTemplate { @Override public void send(Message message) { // 添加消息发送前的校验 if (message.getPayload() == null) { throw new RuntimeException("消息体不能为空"); } super.send(message); // 添加消息发送后的回调 System.out.println("消息发送成功:" + message); } } // 封装RocketMQListener public class MyRocketMQListener implements RocketMQListener<String> { @Override public void onMessage(String message) { // 添加消息接收前的校验 if (message == null) { throw new RuntimeException("消息体不能为空"); } // 添加消息接收后的处理 System.out.println("接收到消息:" + message); } } // 封装广播消息 public class MyBroadcastMessage { private String content; private String tag; // 添加消息过滤器 public boolean filter(String tag) { return this.tag.equals(tag); } // 添加消息去重 @Override public boolean equals(Object obj) { if (obj instanceof MyBroadcastMessage) { MyBroadcastMessage other = (MyBroadcastMessage) obj; return this.content.equals(other.content) && this.tag.equals(other.tag); } return false; } } ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值