t-io实战训练

  • 上一讲我们了解了请求分发的一点小思路,这一讲进行实战训练,项目搭建过程忽略,如果需要源码,去公众号回复"tio实战源码",后续会分享一些个人在项目方面的思考,在不同的业务下,项目的构建思路,闲言少叙

  • 这个项目是因为有个朋友需要搞个socket项目,然后让我帮忙搞一下,本着偷懒的原则然后试着引入了t-io,相关资料自行百度(https://www.tiocloud.com/2/product/tio.html, https://www.oschina.net/p/t-io),t-io作者说如果网络编程很痛苦,那一定是没用 t-io,经过使用,确实比netty要感觉简单很多

  • 首先看下项目结构
    项目结构图在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

tio-demo-common 模块放一些公共的工具类以及bean
tio-demo-socket socket放socket相关的核心类
tio-demo-socket-biz biz放业务类

  • 结构比较简单

-请求接收bean

  • 消息包:
/**
* socket消息包
*
* @author
*/
public class TioDemoPacket extends Packet {
   /**
    * 消息头的长度
    */
   public static final int HEADER_LENGTH = 4;
   private byte[] body;
   public byte[] getBody() {
       return body;
   }
   public void setBody(byte[] body) {
       this.body = body;
   }
}
  • 接收参数解码:
    /**
    * 解码:把接收到的ByteBuffer,解码成应用可以识别的业务消息包
    * 总的消息结构:消息头 + 消息体
    * 消息头结构:    4个字节,存储消息体的长度
    * 消息体结构:   对象的json串的byte[]
    * 根据ByteBuffer解码成业务需要的Packet对象.
    * 如果收到的数据不全,导致解码失败,请返回null,在下次消息来时框架层会自动续上前面的收到的数据
    *
    * @param buffer         参与本次希望解码的ByteBuffer
    * @param limit          ByteBuffer的limit
    * @param position       ByteBuffer的position,不一定是0哦
    * @param readableLength ByteBuffer参与本次解码的有效数据(= limit - position)
    * @param channelContext
    * @return
    * @throws AioDecodeException
    */

   @Override
   public TioDemoPacket decode(ByteBuffer buffer, int limit, int position, int readableLength, ChannelContext channelContext) throws AioDecodeException {
       if (buffer.remaining() < TioDemoPacket.HEADER_LENGTH) {
           return null;
       } else {
           TioDemoPacket imPacket = new TioDemoPacket();
           if (buffer.remaining() < TioDemoPacket.HEADER_LENGTH) {
               return null;
           } else {
               byte[] bytes = new byte[limit];
               buffer.get(bytes);
               imPacket.setBody(bytes);
               return imPacket;
           }
       }
   }
  • 接收参数之后的解析
  TioDemoPacket packingPacket = (TioDemoPacket) packet;
      byte[] body = packingPacket.getBody();
      if (body != null) {
          String reqJson = new String(body);
          if (logPrint) {
              log.info("接收到服务器的请求信息:{},{},收到消息:{}", cc.getId(), cc.getToken(), reqJson);
          }
          if (!JSONUtil.isTypeJSON(reqJson)) {
              log.error("参数不是json");
              return;
          }
          JSONObject jb = JSONUtil.parseObj(reqJson);
          String service = jb.getStr("service");
          if (StrUtil.isEmpty(service)) {
              log.error("接口参数缺失");
              return;
          }
          if (!InterfaceNameEnum.interfaceNameIsExist(service)) {
              log.error("接口未接入");
              return;
          }
          RequestMessageHandler requestMessageHandler = requestMessageHandlerContainer.getMessageHandler(service);
          // 获得  MessageHandler 处理器 的消息类
          Class<? extends RequestMessage> messageClass = RequestMessageHandlerContainer.getMessageClass(requestMessageHandler);
          // 解析消息
          RequestMessage requestMessage = JacksonUtils.toBean(reqJson, messageClass);
          //线程池执行
          tioDemoTaskExecutor.submit(() -> {
              //先判断是否是认证接口
              if (InterfaceNameEnum.CHECK_KEY.getName().equals(service)) {
                  requestMessageHandler.execute(cc, requestMessage);
                  return;
              }
              String token = cc.getToken();
              if (StrUtil.isEmpty(token)) {
                  log.error("调用的接口未经认证,不能调用:{}", service);
                  Tio.close(cc, token);
                  return;
              }
              requestMessageHandler.execute(cc, requestMessage);

          });
          return;
      }
  • 响应参数编码:

    /**
     * 编码:把业务消息包编码为可以发送的ByteBuffer
     * 总的消息结构:消息头 + 消息体
     * 消息头结构:    4个字节,存储消息体的长度
     * 消息体结构:   对象的json串的byte[]
     */
    @Override
    public ByteBuffer encode(Packet packet, TioConfig tioConfig, ChannelContext channelContext) {
        TioDemoPacket serverPacket = (TioDemoPacket) packet;
        byte[] body = serverPacket.getBody();
        int bodyLen = 0;
        if (body != null) {
            bodyLen = body.length;
        }
        //bytebuffer的总长度是 = 消息头的长度 + 消息体的长度
        int allLen = bodyLen;
        //创建一个新的bytebuffer
        ByteBuffer buffer = ByteBuffer.allocate(allLen);
        //设置字节序
        buffer.order(tioConfig.getByteOrder());
        //写入消息头----消息头的内容就是消息体的长度
        //写入消息体
        if (body != null) {
            buffer.put(body);
        }
        return buffer;
    }

  • listener 在监听到关闭以后清理token
 /**
     * 连接关闭前触发本方法
     *
     * @param cc        the channelcontext
     * @param throwable the throwable 有可能为空
     * @param remark    the remark 有可能为空
     * @param isRemove
     * @throws Exception
     */
    @Override
    public void onBeforeClose(ChannelContext cc, Throwable throwable, String remark, boolean isRemove) throws Exception {
        log.info("server onBeforeClose");
        log.info("关闭后清除认证信息");
        Tio.unbindToken(cc);
    }

    /**
     * @param cc
     * @param interval              已经多久没有收发消息了,单位:毫秒
     * @param heartbeatTimeoutCount 心跳超时次数,第一次超时此值是1,以此类推。此值被保存在:channelContext.stat.heartbeatTimeoutCount
     * @return 返回true,那么服务器则不关闭此连接;返回false,服务器将按心跳超时关闭该连接
     */
    @Override
    public boolean onHeartbeatTimeout(ChannelContext cc,
                                      Long interval,
                                      int heartbeatTimeoutCount) {
        Tio.unbindToken(cc);
        return false;
    }

  • 进行认证:
package cn.tio.demo.biz.auth;

import cn.tio.demo.socket.RequestMessageHandler;
import cn.tio.demo.socket.SocketUtil;
import com.tio.common.enums.InterfaceNameEnum;
import com.tio.common.enums.ResultCodeEnum;
import com.tio.common.socket.request.AuthRequestMessage;
import com.tio.common.socket.response.AuthResponseMessage;
import com.tio.common.util.JacksonUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.tio.core.ChannelContext;
import org.tio.core.Tio;

import javax.validation.ConstraintViolation;
import java.util.Objects;
import java.util.Set;

/**
 * 客户端认证请求
 *
 * @author
 */
@Slf4j
@Component
public class AuthRequestMessageHandler implements RequestMessageHandler<AuthRequestMessage> {

    @Value("${parking.parkId}")
    private String parkId;

    @Value("${parking.parkKey}")
    private String parkKey;

    @Value(("${parking.logPrint:false}"))
    private Boolean logPrint;

    @Override
    public String getService() {
        return InterfaceNameEnum.CHECK_KEY.getName();
    }

    @Override
    public void execute(ChannelContext cc, AuthRequestMessage message) {
        if (logPrint) {
            log.info("客户端认证信息:{}", message);
        }
        AuthResponseMessage authResponseMessage = new AuthResponseMessage();
        authResponseMessage.setService(InterfaceNameEnum.CHECK_KEY.getName());
        Set<ConstraintViolation<AuthRequestMessage>> constraintViolationSet = validParam(message);
        if (Objects.isNull(constraintViolationSet) || !constraintViolationSet.isEmpty()) {
            authResponseMessage.setResultCode(ResultCodeEnum.FAIL_2.getCode());
            authResponseMessage.setMessage("参数不能为空");
            SocketUtil.sendMsg(cc, JacksonUtils.toJson(authResponseMessage));
            return;
        }

        String reParkId = message.getParkId();
        String reParkKey = message.getParkKey();
        if (!reParkId.equals(parkId) || !reParkKey.equals(parkKey)) {
            authResponseMessage.setResultCode(ResultCodeEnum.FAIL_1.getCode());
            authResponseMessage.setMessage("认证失败,无此车场");
            SocketUtil.sendMsg(cc, JacksonUtils.toJson(authResponseMessage));
            return;
        }
        //绑定用户
        Tio.bindToken(cc, parkId);
        authResponseMessage.setResultCode(ResultCodeEnum.SUCCESS.getCode());
        authResponseMessage.setMessage("认证成功");
        SocketUtil.sendMsg(cc, JacksonUtils.toJson(authResponseMessage));

    }


}

  • 心跳
package cn.tio.demo.biz.heartbeat;


import cn.tio.demo.socket.RequestMessageHandler;
import cn.tio.demo.socket.SocketUtil;
import com.tio.common.enums.InterfaceNameEnum;
import com.tio.common.enums.ResultCodeEnum;
import com.tio.common.socket.request.HeartBeatRequestMessage;
import com.tio.common.socket.response.HeartbeatResponseMessage;
import com.tio.common.util.JacksonUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.tio.core.ChannelContext;

import javax.validation.ConstraintViolation;
import java.util.Objects;
import java.util.Set;

/**
 * 服务器心跳响应结果
 *
 * @author
 */
@Slf4j
@Component
public class HeartBeatRequestMessageHandler implements RequestMessageHandler<HeartBeatRequestMessage> {

    @Value(("${parking.logPrint:false}"))
    private Boolean logPrint;

    @Override
    public String getService() {
        return InterfaceNameEnum.HEART_BEAT.getName();
    }


    @Override
    public void execute(ChannelContext cc, HeartBeatRequestMessage message) {
        if (logPrint) {
            log.info("接收到的客户端心跳结果:{}", message);
        }
        HeartbeatResponseMessage responseMessage = new HeartbeatResponseMessage();
        responseMessage.setService(InterfaceNameEnum.HEART_BEAT.getName());
        Set<ConstraintViolation<HeartBeatRequestMessage>> constraintViolationSet = validParam(message);
        if (Objects.isNull(constraintViolationSet) || !constraintViolationSet.isEmpty()) {
            responseMessage.setResultCode(ResultCodeEnum.FAIL_2.getCode());
            responseMessage.setMessage("参数不能为空");
            SocketUtil.sendMsg(cc, JacksonUtils.toJson(responseMessage));
            return;
        }
        SocketUtil.connect = cc;
        responseMessage.setResultCode(ResultCodeEnum.SUCCESS.getCode());
        responseMessage.setMessage("在线");
        SocketUtil.sendMsg(cc, JacksonUtils.toJson(responseMessage));
    }


}

  • 一个简单的骨架就搭建好了,试着跑一下:
    运行效果
  • 关键代码已经贴出来了,还是不懂得可以留言,看到就会回复
  • 这是我的微信公众号二维码,你的关注是我持续更新的动力,谢谢
    我的公众号
<think>好的,我现在要解决用户关于Redis缓存高频访问与三大缓存问题的面试题答案请求。首先,我需要回顾用户之前提供的20个问题,这些问题分为基础原理层、进阶设计层、综合实战层和深度扩展题。用户现在希望得到这些问题的详细答案,所以我的任务是为每个问题提供准确、清晰的回答,同时遵循用户给定的格式和LaTeX要求。 首先,我需要确保理解每个问题的考察要点。例如,基础原理层的问题主要测试对缓存穿透、雪崩、击穿的基本概念和解决方法的理解。比如第一个问题,三者的核心区别是什么,我需要明确区分它们的定义和不同场景。同时,用户提到要使用中文回答,并且注意LaTeX格式的正确使用,如行内公式用$...$,独立公式用$$...$$,并且正确应用技术术语。 接下来,进阶设计层的问题更侧重于系统设计,比如如何设计带熔断机制的缓存系统,或者逻辑过期方案中的异步更新流程。这里需要结合具体的解决方案,可能涉及架构图或步骤说明,但用户要求中没有提到需要图表,所以需要用文字详细描述。同时要注意使用正确的技术术语,如SETNX、布隆过滤器、双缓存策略等。 综合实战层的问题需要结合实际应用场景,例如设计电商秒杀系统的缓存架构,这时候需要考虑到高并发、热点数据、库存扣减等实际问题。回答时要分步骤说明,可能需要提到多级缓存、分布式锁、限流策略等。同时,用户提到要结构清晰,逐步解决问题,所以每个答案需要有条理地展开。 另外,用户之前给出的示例答案中,每个问题都有明确的要点,我需要确保覆盖这些要点。例如,在缓存空对象如何处理数据更新的问题中,需要提到设置较短的过期时间和异步更新策略。对于每个问题,我需要先回忆相关的知识点,再组织语言,确保回答准确且全面。 同时,需要注意避免技术错误。例如,在回答布隆过滤器如何应对数据库扩容时,需要说明重建过滤器的必要性以及可扩展布隆过滤器的使用,这可能涉及到动态布隆过滤器或分片布隆过滤器的概念,需要确保这些信息准确无误。 还要考虑用户可能的深层需求。虽然用户直接要求的是答案,但作为面试准备,候选人不仅需要知道答案,还需要能够结合实际项目经验说明,并分析方案的优缺点。因此,在答案中可以适当加入优缺点分析,或者实际应用中的注意事项,以提升回答的实用性。 在格式方面,用户要求结构清晰,所以每个问题的答案可能需要分点列出,或者用步骤化的方式呈现。例如,设计带熔断机制的缓存系统时,分请求监控、降级策略、恢复机制等部分说明。同时,注意使用加粗关键词,帮助读者快速抓住重点,如用户示例中的“恶意访问不存在数据”等。 另外,关于LaTeX的使用,需要检查所有数学表达式是否正确。例如,随机过期时间的公式是否正确应用了$$...$$格式,变量是否斜体,运算符是否有空格等细节问题。确保没有语法错误,公式呈现正确。 最后,综合所有问题,我需要逐一处理每个问题,确保覆盖所有20个题目,并且每个答案符合用户的要求:结构清晰、逐步解决、正确可靠。可能需要分层次处理,先处理基础问题,再进阶,最后实战和扩展题,确保逻辑连贯,内容详实。 过程中可能会遇到不确定的知识点,例如Redis 6.0的新特性对缓存优化的影响,需要确认多线程IO和客户端缓存的具体细节,确保信息准确。此外,机器学习预测缓存失效是一个较新的点,可能需要简要说明思路,如基于历史访问模式调整过期时间,而不必深入算法细节。 总结来说,我需要系统地整理每个问题的答案,确保每个要点都被覆盖,格式正确,语言清晰,并且技术内容准确无误。同时,保持回答的结构化,便于用户理解和面试准备。</think>以下是针对上述20个⾯试题的详细答案及解析,按层级分类组织: --- ### ⼀、基础原理层(5题) #### 1. **缓存穿透/雪崩/击穿三者的核⼼区别是什么?** - **缓存穿透**:恶意请求访问**数据库中不存在的数据**(如负⼤量查询ID=-1的数据),导致绕过缓存直接冲击数据库。 - **缓存雪崩**:⼤量缓存数据**同时过期失效**(例如全局设置相同TTL),引发数据库瞬时⾼并发查询。 - **缓存击穿**:**某个热点Key过期失效**(如秒杀商品库存),导致瞬间⼤量并发请求穿透到数据库。 #### 2. **描述布隆过滤器防穿透的⼯作原理** - **核心机制**: 1. 使⽤**位数组(Bit Array)**和**多个哈希函数**,将数据映射到位数组的不同位置。 2. 查询时若所有哈希位均为1,则**可能存在**;若任⼀位为0,则**⼀定不存在**。 - **特点**: - 优点:**空间效率极⾼**(⽐哈希表节省90%以上内存)。 - 缺点:**存在误判率**(可通过增加哈希函数或扩容降低概率)。 #### 3. **缓存空对象⽅案如何处理数据更新问题?** - **解决步骤**: 1. 对查询不到的Key,缓存⼀个**空值(如`NULL`)**并设置**较短过期时间**(例如5分钟)。 2. 当数据库新增该Key时,通过**消息队列异步清除**缓存中的空值标记。 3. 或采⽤**双写机制**:更新数据库后,主动删除缓存中的空值。 #### 4. **缓存雪崩的随机过期时间⽅案如何实现?** - **公式设计**: $$ T_{real} = T_{base} + random(0, T_{variance}) $$ 例如:基础过期时间$T_{base}=60$分钟,随机浮动$T_{variance}=10$分钟,实际过期时间在60-70分钟之间随机分布。 - **实现⽅式**:在设置缓存时,调⽤`Redis`的`SETEX`命令并动态计算TTL。 #### 5. **互斥锁解决击穿问题时,为什么要⽤SETNX⽽不是普通的加锁?** - **关键原因**: - `SETNX`(SET if Not eXists)是**原⼦性操作**,可确保分布式环境下多个客户端**竞争锁的安全性**。 - 普通锁(如Java的`synchronized`)仅适⽤于单机环境,⽆法解决分布式系统锁竞争问题。 - **补充**:推荐使⽤`Redisson`等框架实现**可重⼊锁+⾃动续期**功能。 --- ### ⼆、进阶设计层(8题) #### 6. **如何设计⼀个带熔断机制的缓存系统?** ```text 1. 监控层: - 实时统计数据库请求量/错误率(如使⽤Hystrix或Sentinel) 2. 熔断策略: - 当错误率超过阈值(如50%)或QPS超限时,触发熔断 - 直接返回默认值或降级数据(如库存显示“服务繁忙”) 3. 恢复机制: - 设置熔断时间窗(如10秒),之后进⼊半开状态试探性恢复 ``` #### 7. **逻辑过期⽅案中,如何实现后台异步更新?** - **步骤流程**: 1. 缓存数据中增加**逻辑过期字段**(如`expire_time`)。 2. 线程A发现数据逻辑过期后,获取**分布式锁**(防⽌重复更新)。 3. 提交**异步任务**到线程池,由后台线程更新数据库并刷新缓存。 4. 线程A**直接返回旧数据**,保证服务可⽤性。 #### 8. **布隆过滤器如何应对数据库扩容?** - **传统⽅案缺陷**:新增数据需要**重建布隆过滤器**,否则新数据可能被误判为不存在。 - **优化⽅法**: - 使⽤**可扩展布隆过滤器**(Scalable Bloom Filter):通过多个布隆过滤器分层存储,动态扩展。 - 采⽤**定时任务**:定期合并旧过滤器数据到新过滤器。 #### 9. **热点Key发现⽅案有哪些?** - **客户端埋点**:统计接⼝调⽤频率(需聚合分析)。 - **代理层监控**:如使⽤Nginx记录URL访问频次。 - **Redis内置命令**:`redis-cli --hotkeys`(需开启`maxmemory-policy`为LFU)。 - **流式计算**:通过Flink实时分析Redis监控日志。 #### 10. **如何通过双缓存策略防击穿?** ```text 主缓存:设置正常过期时间(如30分钟) 备份缓存:设置更⻓过期时间(如1⼩时),但存储相同数据 - 当主缓存失效时: 1. 先返回备份缓存数据 2. 异步触发主缓存重建 3. 更新备份缓存时间 ``` #### 11. **Redis持久化策略对缓存恢复的影响?** - **RDB**: - 优点:**恢复速度快**(二进制快照直接加载)。 - 缺点:可能丢失最后⼀次快照后的数据(如宕机前未触发`SAVE`)。 - **AOF**: - 优点:**数据完整性⾼**(记录所有写操作)。 - 缺点:恢复速度慢(需重放命令)、⽂件体积⼤。 #### 12. **缓存预热的具体实施⽅案?** - **数据来源**: 1. 历史访问日志分析(ELK统计Top N热点Key) 2. 业务预测(如⼤促前提前加载商品信息) - **加载⽅式**: - 服务启动时调⽤`Jedis.set()`批量写⼊ - 使⽤`Redis`的`Pipeline`减少⽹络开销 #### 13. **如何通过限流保护数据库?** - **令牌桶算法**(Redis-Cell模块): ```shell # 示例:每秒10个令牌,桶容量20 CL.THROTTLE user_query 20 10 60 1 ``` - **漏桶算法**:保证请求以恒定速率访问数据库。 - **拒绝策略**:超限请求直接返回“系统繁忙”提示。 --- ### 三、综合实战层(7题) #### 14. **设计⼀个电商秒杀系统的缓存架构** ```text 1. 库存缓存: - 使⽤Redis Hash存储商品ID与剩余库存量 - 预扣库存:DECR命令原⼦性扣减 2. 多级缓存: - 客户端本地缓存(如Guava Cache)→ Redis集群 → 数据库 3. 限流削峰: - 库存扣减请求进⼊MQ队列,按批次处理 4. 防⽌超卖: - Lua脚本保证“查询+扣减”原⼦性 ``` #### 15. **缓存与数据库⼀致性冲突的取舍** - **CP场景**(如⽀付系统): - 采⽤**强⼀致性⽅案**:如Redisson分布式锁,先更新数据库再删缓存。 - **AP场景**(如商品详情): - 采⽤**最终⼀致性⽅案**:设置缓存过期时间 + 延迟双删。 - **折中⽅案**:通过Binlog订阅(如Canal)异步更新缓存。 #### 16. **优化⼤Value缓存的性能问题** - **数据压缩**:使⽤Snappy或GZIP压缩JSON数据。 - **分片存储**:将⼤对象拆分为多个Key(如`user:1001:profile_base`, `user:1001:orders`)。 - **数据结构优化**:例如使⽤Hash代替JSON字符串存储对象。 #### 17. **处理缓存脏数据的常⽤⼿段** ```text 1. 写操作流程: - 先更新数据库,再删除缓存(Cache-Aside Pattern) 2. 删除缓存后: - 延迟500ms再次删除(防⽌旧数据被重新加载) 3. 数据版本号: - 缓存值与版本号绑定,查询时校验数据库版本 ``` #### 18. **实现缓存资源的动态扩容** - **Redis Cluster槽位迁移**: 1. 新增节点后,执⾏`CLUSTER ADDSLOTS`分配槽位。 2. 使⽤`MIGRATE`命令将数据迁移到新节点。 3. 更新客户端路由表(可借助Smart Client⾃动感知)。 - **注意事项**:迁移期间可能存在部分请求重定向(ASK/MOVED响应)。 #### 19. **实时监控缓存健康度的系统设计** ```text 监控指标 ⽬标值 报警阈值 --------------- -------------------- ------------ 命中率 >90% <80% 平均响应时间 <10ms >50ms 内存使⽤率 <70% >85% Key淘汰率 <100次/分钟 >500次/分钟 - 实现⼯具:Prometheus收集指标 + Grafana可视化 + AlertManager告警 ``` #### 20. **缓存命中率持续下降的排查思路** ```text 1. 检查热点Key分布: - redis-cli --hotkeys 或 monitor 命令采样 2. 验证过期策略: - 是否未设置TTL?是否大量使⽤了EXPIRE 3. 分析查询模式: - 是否存在⼤量⾼频全表扫描(如KEYS *) 4. 内存碎⽚: - INFO memory查看mem_fragmentation_ratio > 1.5时需重启或使⽤mem-purge清理 ``` --- ### 四、深度扩展题(附加) #### **Redis6.0新特性对缓存优化的影响** - **多线程IO**:提升⼤数据量读写性能(但命令执⾏仍是单线程)。 - **客户端缓存**(Client-side caching): - 客户端可本地缓存热点数据,通过RESP3协议接收失效通知。 - ⽰例:`CLIENT TRACKING ON REDIRECT 1234`。 #### **机器学习预测缓存失效** - **训练特征**:历史访问时间、频率、业务关联性(如节假⽇促销)。 - **模型输出**:动态调整TTL(⾼概率访问的数据延⻓过期时间)。 - **实现框架**:TensorFlow Serving + Redis自定义模块。 --- 以上答案需结合**业务场景灵活调整**,重点理解设计思路⽽⾮死记硬背。建议通过画流程图、编写伪代码等⽅式加深记忆。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值