Redis缓存学习

Redis

缓存机制的介绍

说明:使用缓存机制主要的目的就是为了降低用户访问物理设备的频次.从缓存服务器中直接获取数据,快速的响应用户,提高整体的查询速度.用户体验更好.
如何实现:
1.缓存机制应该采用什么样的数据结构 进行构建? K-V结构 K必须唯一
2.应该使用什么语言进行开发? C语言
3.缓存的运行环境是哪? 内存
4.内存断电即擦除, 如何保证数据的安全性?? 实现持久化(写入磁盘)操作
5.内存中的数据如何进行优化 (不能一直存? ) 内存优化的算法 LRU算法

Redis介绍

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets)
与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability).
速度快:
tomcat: 150-220/秒
nginx: 3-5万/秒
redis: 写 8.6万/秒 读 11.2万/秒 ~ 平均10万次/秒

Redis安装

上传Redis在这里插入图片描述
解压Redis

在这里插入图片描述

编译安装Redis

1.执行编译操作 make
在这里插入图片描述
2.安装redis make install
在这里插入图片描述

修改redis配置文件

1.修改IP绑定
在这里插入图片描述
2.取消保护模式
在这里插入图片描述
3.开启后台启动

在这里插入图片描述

Redis常规命令

要求:进入redis根目录中执行.
特点:redis每次启动时都会读取配置文件. 如果需要准备多台redis则需要准备多个配置文件
1.启动命令

redis-server redis.conf

没有开启后台运行的效果
在这里插入图片描述
开启后台运行的效果
在这里插入图片描述
2.进入redis客户端中

redis-cli -p 6379

3.关闭redis

方式一:redis-cli  -p 6379 shutdown

方式二:
在这里插入图片描述

redis入门案例

1 添加jar包
<!--spring整合redis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
        </dependency>
2 编辑测试API
package com.test;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;

//@SpringBootTest //需要依赖spring容器,从容器中动态的获取对象
public class TestRedis {

    /**
     * 完成Redis入门案例测试
     * 报错说明:
     *      1.检查Linux防火墙
     *      2.检查Redis3项配置
     *      3.重启redis.
     */
    @Test
    public void test01(){
        Jedis jedis = new Jedis("192.168.126.129",6379);
        //1.向redis中保存数据
        jedis.set("keytest01", "value哈哈哈");
        //2.从redis中获取数据
        String value = jedis.get("keytest01");
        System.out.println(value);
    }

    @Test
    public void test02(){
        Jedis jedis = new Jedis("192.168.126.129",6379);

        //1.判断redis中是否存在key
        if(jedis.exists("keytest01")){
            //2.如果存在则设定超时时间
            jedis.expire("keytest01", 100);
            //3.线程暂停2秒
            try {
                Thread.sleep(2000);
                //4.获取剩余的存活时间
                Long time = jedis.ttl("keytest01");
                System.out.println("还能活:"+time);
                //5.撤销删除操作
                jedis.persist("keytest01");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 需求说明:
     *  1.redis.set操作,后面的操作会将之前的value覆盖
     *  要求:如果key已经存在,则不允许赋值.
     *  注意环境: 检查redis中是否有改数据.
     *  */
    @Test
    public void test03() {
        //A  潘石屹    B 马云
        Jedis jedis = new Jedis("192.168.126.129", 6379);
        jedis.flushAll();   //清空redis缓存
        //如果key存在则不执行赋值操作.
        jedis.setnx("boss", "潘石屹");
        jedis.setnx("boss", "马云");
        System.out.println(jedis.get("boss"));
    }

    /**
     * 为数据添加超时时间.保证原子性操作
     * 原子性: 一起完成一起回滚
     * 锁机制: 保证原子性   死锁!!!!
     * 小结: setnx 如果key存在则不赋值
     *       setex  保证原子性操作,并且添加超时时间.
     * * */
    @Test
    public void test04() {
        Jedis jedis = new Jedis("192.168.126.129", 6379);
        jedis.setex("aaa", 20, "xxxxx"); //满足原子性需求.
    }

    /**
     * 需求:
     *  1.要求赋值操作时,如果数据已经存在则不允许赋值.
     *  2.同时要求添加超时时间 并且满足原子性要求.
     *   private static final String XX = "xx";   只有key存在时,才能赋值
     *   private static final String NX = "nx";   只有key不存在时,赋值
     *   private static final String PX = "px";   毫秒
     *   private static final String EX = "ex";   秒
     */
    @Test
    public void test05() {
        Jedis jedis = new Jedis("192.168.126.129", 6379);
        SetParams setParams = new SetParams();
        setParams.nx().ex(20);
        jedis.set("AAA", "CCCCC", setParams); //原子性要求
        System.out.println(jedis.get("AAA"));
    }

	 /*测试hash数据类型*/
    @Test
    public void testHash() {
        Jedis jedis = new Jedis("192.168.126.129", 6379);
        jedis.hset("person", "name", "tomcat");
        jedis.hset("person", "age", "100");
        Map<String,String> map = jedis.hgetAll("person");
        System.out.println(map);
    }
	
	@Test
    public void testList() {
        Jedis jedis = new Jedis("192.168.126.129", 6379);
        jedis.lpush("list", "1","2","3","4");
        String value = jedis.rpop("list");
        System.out.println(value);
    }

	 /**
     * 实现Redis 事务控制
     */
    @Test
    public void testMulti() {
        Jedis jedis = new Jedis("192.168.126.129", 6379);
        Transaction transaction = jedis.multi();
        try {
            transaction.set("AAA", "XXX");
            transaction.set("BBB", "XXX");
            transaction.exec(); //提交事务
        }catch (Exception exception){
            exception.printStackTrace();
            transaction.discard();//回滚事务
        }

    }
}

SpringBoot整合Redis

1 编辑配置文件
# 配置单台redis
redis.host=192.168.126.129
redis.port=6379
2 编辑配置类
package com.config;

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 redis.clients.jedis.Jedis;

@Configuration  //标识我是配置类
@PropertySource("classpath:/properties/redis.properties")
public class RedisConfig {

    @Value("${redis.host}")
    private String  host;
    @Value("${redis.port}")
    private Integer port;

    @Bean
    public Jedis jedis(){
        return new Jedis(host,port);
    }
}
3 编辑测试类

说明:校验springBoot整合是否正确

@SpringBootTest //需要依赖spring容器,从容器中动态的获取对象
public class TestRedis {

    @Autowired
    private Jedis jedis;
    
    @Test
    public void test01(){
        //Jedis jedis = new Jedis("192.168.126.129",6379);
        //1.向redis中保存数据
        jedis.set("keytest01", "value哈哈哈");
        //2.从redis中获取数据
        String value = jedis.get("keytest01");
        System.out.println(value);
    }
}

数据如何保存到Redis中

1 业务说明

说明:由于redis中一般使用String数据类型保存业务数据.但是代码中java对象Redis没办法直接保存,所以需要中间的转化的过程.使用JSON方式进行数据中转.
List java对象 --------- JSON ------------ Redis中 使用String数据类型保存.

2 ObjectMapper API介绍
package com.test;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pojo.ItemDesc;
import org.junit.jupiter.api.Test;

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

public class TestObjectMapper {

    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Test
    public void test01(){
        ItemDesc itemDesc = new ItemDesc();
        itemDesc.setItemId(100L);
        itemDesc.setItemDesc("测试JSON转化");
        itemDesc.setCreated(new Date());
        itemDesc.setUpdated(itemDesc.getCreated());

        try {
            //1.将java对象转化为JSON
            String json = MAPPER.writeValueAsString(itemDesc);
            System.out.println(json);
            //2.将JSON转化为对象 利用反射机制实例化对象 利用get/set方法为对象赋值
            ItemDesc itemDesc2 = MAPPER.readValue(json, ItemDesc.class);
            System.out.println(itemDesc2.toString()); //只输出当前对象的数据

            //3.将集合信息转化为JSON  List
            List<ItemDesc> list = new ArrayList<>();
            list.add(itemDesc);
            String listJSON = MAPPER.writeValueAsString(list);
            System.out.println(listJSON);
            //将json转化为List集合
            List<ItemDesc> list2 = MAPPER.readValue(listJSON,list.getClass());
            System.out.println(list2);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
    }
}
3 编辑工具API

说明:改工具API主要负责将用户参数转化为JSON,或者将JSON串转化为对象. 简化客户端调用.

package com.unit;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class ObjectMapperUtil {

    //1.创建工具API对象
    private static final ObjectMapper MAPPER = new ObjectMapper();

    //2.封装API 将对象转化为JSON
    public static String toJSON(Object object){
        if(object == null){
            throw new RuntimeException("传入对象不能为null");
        }

        try {
            String json = MAPPER.writeValueAsString(object);
            return json;
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }
    }

    /**
     * 3.将JSON串转化为对象  用户传递什么类型,则返回什么对象
     *                      user.class        User对象
     *
     */
    public static <T> T toObject(String json,Class<T> target){
        //1.校验参数是否有效
        if(json == null || "".equals(json) || target == null){
            //参数有误.
            throw new RuntimeException("参数不能为null");
        }
        
        //2.执行业务处理
        try {
            T t = MAPPER.readValue(json, target);
            return t;
        } catch (JsonProcessingException e) {
            //将报错信息通知其他人
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }
    }
}

redis常见问题

1 缓存穿透

特点: 用户高并发环境下,访问数据库中根本不存在的数据.
影响:由于用户高并发访问,则数据库可能存在宕机的风险.
在这里插入图片描述

2 缓存击穿

说明: 由于用户高并发的访问. 访问的数据刚开始有缓存,但是由于特殊原有 导致缓存失效.(数据’‘单个’’)
在这里插入图片描述

3 缓存雪崩

说明: 由于高并发的环境下.大量的用户访问服务器. redis中有大量的数据在同一时间超时(删除).
解决方案:不要同一时间删除数据.
在这里插入图片描述

4 持久化问题
问题说明

说明:Redis中的数据都保存在内存中.如果服务关闭或者宕机则内存资源直接丢失.导致缓存失效.

持久化原理说明

说明:Redis中有自己的持久化策略.Redis启动时根据配置文件中指定的持久化方式进行持久化操作. Redis中默认的持久化的方式为RDB模式.

3 RDB模式

特点说明:
1.RDB模式采用定期持久化的方式. 风险:可能丢失数据.
2.RDB模式记录的是当前Redis的内存记录快照. 只记录当前状态. 持久化效率最高的
3.RDB模式是默认的持久化方式.

持久化命令:
命令1: save 同步操作. 要求记录马上持久化. 可能对现有的操作造成阻塞
命令2: bgsave 异步操作. 开启单独的线程实现持久化任务.

持久化周期:
save 900 1 在900秒内,如果执行一次更新操作,则持久化一次.
save 300 10 在300秒内,如果执行10次更新操作,则持久化一次.
save 60 10000 在60秒内,如果执行10000次更新操作,则持久化一次.
save 1 1 ???不可以 容易阻塞 性能太低.不建议使用.
用户操作越频繁,则持久化周期越短.

4 AOF模式

特点:
1.AOF模式默认是关闭状态 如果需要则手动开启.
2.AOF能够记录程序的执行过程 可以实现数据的实时持久化. AOF文件占用的空间较大.回复数据的速度较慢.
3.AOF模式开启之后.RDB模式将不生效.

AOF配置:
在这里插入图片描述
持久化周期配置:
appendfsync always 实时持久化.
appendfsync everysec 每秒持久化一次 略低于rdb模式
appendfsync no 自己不主动持久化(被动:由操作系统解决)

5 redis中如何选择持久化方式

思路: 如果允许数据少量的丢失,则首选RDB.(快),如果不允许数据丢失则使用AOF模式.

6 情景题

双11前夜误操作将Redis服务器执行了flushAll命令. 如解决?
解决方案:
修改aof文件中的命令.删除flushAll之后重启redis即可.

Redis内存优化策略
1 修改Redis内存

在这里插入图片描述
修改内存大小:
在这里插入图片描述

2 场景说明

Redis运行的空间是内存.内存的资源比较紧缺.所以应该维护redis内存数据,将改让redis保留热点数据.

3 LRU算法

LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。
维度: 自上一次使用的时间T
最为理想的内存置换算法.

4 LFU算法

LFU(least frequently used (LFU) page-replacement algorithm)。即最不经常使用页置换算法,要求在页置换时置换引用计数最小的页,因为经常使用的页应该有一个较大的引用次数。但是有些页在开始时使用次数很多,但以后就不再使用,这类页将会长时间留在内存中,因此可以将引用计数寄存器定时右移一位,形成指数衰减的平均使用次数。
least frequently used (LFU) page-replacement algorithm
即最不经常使用页置换算法,要求在页置换时置换引用计数最小的页,因为经常使用的页应该有一个较大的引用次数。但是有些页在开始时使用次数很多,但以后就不再使用,这类页将会长时间留在内存中,因此可以将引用计数寄存器定时右移一位,形成指数衰减的平均使用次数。
维度: 引用次数

5 RANDOM算法

随机算法

内存策略优化

1.volatile-lru 在设定了超时时间的数据, 采用lru算法进行删除.
2.allkeys-lru 所有数据采用lru算法
3.volatile-lfu 在设定了超时时间的数据, 采用LFU算法进行删除.
4.allkeys-lfu 所有数据采用LFU算法
5.volatile-random 设定超时时间数据采用随机算法
6.allkeys-random 所有数据采用随机算法
7.volatile-ttl 设定了超时时间的数据 根据ttl规则删除. 将剩余时间少的提前删除
8.noeviction 内存满了 不做任何操作.报错返回.
在这里插入图片描述

Redis分片机制

分片机制说明

前提说明: redis可以通过修改内存的大小 实现数据的保存.但是内存的资源不易设置的过大,因为很多的时间都浪费在内存的寻址中.
需求: 如果有海量的数据,需要redis存储 问:应该如何处理?
解决方案: 可以采用Redis分片机制 实现内存数据的扩容.
知识点: 采用redis分片 只要的目的就是为了实现内存扩容.从而解决海量数据存储的问题
在这里插入图片描述

Redis分片搭建步骤
1.分片节点

说明: 端口号分别为 6379/6380/6381

2 关闭原有的Redis

在这里插入图片描述

3 准备多个配置文件

1.创建shards目录
在这里插入图片描述
2.复制3份配置文件
在这里插入图片描述
3.修改各自的端口号
在这里插入图片描述
3.1.修改端口号:
在这里插入图片描述
3.2.关闭AOF模式
在这里插入图片描述
3.3.修改RDB模式名称
在这里插入图片描述
分别修改6380/6381 改为各自对应的端口号.
然后启动3台redis

[root@localhost shards]# redis-server 6379.conf
[root@localhost shards]# redis-server 6380.conf
[root@localhost shards]# redis-server 6381.conf

启动效果:
在这里插入图片描述

4 Redis分片入门案例
public class TestShards { //该类表示测试redis分片机制

    /**
     * 说明:在Linux中有3台redis.需要通过程序进行动态链接.
     * 实现数据的存储.
     * 思考: 数据保存到了哪台redis中???
     */
    @Test
    public void test01(){
        List<JedisShardInfo> shards = new ArrayList<>();
        shards.add(new JedisShardInfo("192.168.126.129", 6379));
        shards.add(new JedisShardInfo("192.168.126.129", 6380));
        shards.add(new JedisShardInfo("192.168.126.129", 6381));
        //分片的API
        ShardedJedis shardedJedis = new ShardedJedis(shards);
        shardedJedis.set("key001", "valuejinx");
        System.out.println(shardedJedis.get("key001"));
    }
}
一致性hash算法
一致性hash说明

一致性哈希算法在1997年由麻省理工学院提出,是一种特殊的哈希算法,目的是解决分布式缓存的问题。 [1] 在移除或者添加一个服务器时,能够尽可能小地改变已存在的服务请求与处理请求服务器之间的映射关系。一致性哈希解决了简单哈希算法在分布式哈希表( Distributed Hash Table,DHT) 中存在的动态伸缩等问题 [2] 。

一致性hash原理

说明:

  1. 一般的hash由8位16进制数组成的. 共有2^32种可能性!!!
  2. hash算法对相同的数据进行hash运算时 结果必然相同.
    在这里插入图片描述
一致性hash 特性

1平衡性
①平衡性是指hash的结果应该平均分配到各个节点,这样从算法上解决了负载均衡问题
说明:引入虚拟节点 实现数据的平衡 但是平衡是相对的.不是绝对的.
在这里插入图片描述

2 单调性

②单调性是指在新增或者删减节点时,不影响系统正常运行 。
在这里插入图片描述

3 分散性

③分散性是指数据应该分散地存放在分布式集群中的各个节点(节点自己可以有备份),不必每个节点都存储所有的数据.

Spring整合redis分片机制
1 准备Redis.pro文件
# 配置单台redis
#redis.host=192.168.126.129
#redis.port=6379

# 配置多台redis
redis.nodes=192.168.126.129:6379,192.168.126.129:6380,192.168.126.129:6381
2 编辑配置类
@Configuration  //标识我是配置类
@PropertySource("classpath:/properties/redis.properties")
public class RedisConfig {

    @Value("${redis.nodes}")
    private String nodes;   //node,node,node

    /**
     * spring整合redis分片机制
     */
    @Bean
    public ShardedJedis shardedJedis(){
        //1.获取每个节点的信息
        String[] strNodes = nodes.split(",");
        List<JedisShardInfo> shards = new ArrayList<>();
        //2.遍历所有node.为list集合赋值
        for(String node :strNodes){ //node=ip:port
            String host = node.split(":")[0];
            int port = Integer.parseInt(node.split(":")[1]);
            JedisShardInfo info = new JedisShardInfo(host,port);
            shards.add(info);
        }

        ShardedJedis shardedJedis = new ShardedJedis(shards);
        return shardedJedis;
    }

  /*  @Value("${redis.host}")
    private String  host;
    @Value("${redis.port}")
    private Integer port;

    //SpringBoot管理  Spring框架的优化的API
    @Bean
    public Jedis jedis(){

        return new Jedis(host,port);
    }*/
}

Redis哨兵机制

1.redis分片机制存在的问题

说明:Redis分片机制可以实现内存数据的扩容.但是如果Redis服务器发生了宕机的现象,则会影响整个分片使用.
问题:Redis分片机制没有实现高可用. 当主机宕机之后.由从机自动的实现故障迁移.用户访问不受任何影响.

2.Redis主从搭建

前提条件: 如果要实现redis高可用机制,则必须首先实现主从搭建.
主从关系设定: 6379当做主机-M 6380/6381 从机-S

2.1准备哨兵工作目录

1).关闭分片的Redis配置.在这里插入图片描述
2).准备redis哨兵配置 复制配置文件目录在这里插入图片描述
3).删除持久化文件
在这里插入图片描述
4).准备3台redis服务器即可
重启redis
在这里插入图片描述

2.2 Redis主从配置

1).检查Redis默认条件下的主从关系
在这里插入图片描述
2).实现主从配置
命令:

slave of   主机IP 主机端口

在这里插入图片描述
3).用户操作Redis主机,之后检查从机是否实现数据同步即可.
数据同步正常.
在这里插入图片描述

2.3 主从配置问题说明

说明:当redis服务器已经配置了主从结构之后.如果将服务器宕机.之后再次重启.则发现从服务器又会变为主机!!!
问题说明: 执行了主从挂载命令 该命令一直保存在内存中.当redis服务器重启之后,该命令失效.如果想要一直保持主从的关系.则必须将主从的结构写入Redis.conf的配置文件中.
在这里插入图片描述

3. 哨兵机制原理说明

在这里插入图片描述
1.首先启动Redis哨兵.由哨兵监控整个Redis主从状态. 主要监控M主机. 同时获取其从机的信息.
2.哨兵利用心跳检测机制(PING-PONG)的方式监控主机是否宕机. 如果连续3次主机没有响应.则哨兵判断主机宕机.
之后开始进行选举.
3.根据从主机中获取的从机信息.之后利用 选举机制算法.挑选新的主机.
4.之后将剩余的redis修改为当前主机的的从.并且修改配置文件.

4.Redis实现哨兵机制
4.1复制Redis哨兵配置文件

在这里插入图片描述

4.2修改配置文件

1.命令: 在sentinel目录下

vim sentinel.conf

在这里插入图片描述
2.修改哨兵监控配置
在这里插入图片描述
3.修改哨兵选举的时间
当redis主机宕机之后,哨兵多久开始选举.
在这里插入图片描述
4.选举超时时间设定
说明:如果当选主机的服务器在规定的时间之内,没有完成切换.则哨兵重新选举.在这里插入图片描述

4.3 启动哨兵检查高可用性

命令:

redis-sentinel sentinel.conf

在这里插入图片描述
测试步骤:
1.将6379主机宕机.
在这里插入图片描述
2.等待10秒之后.检查从机是否当选主机.
在这里插入图片描述
3.重启6379服务器.检查是否为当前主机的从.
在这里插入图片描述
重启服务之后,检查是否为从.!
在这里插入图片描述

Spring整合Redis哨兵
入门案例
package com.test;

import org.junit.jupiter.api.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisSentinelPool;

import java.util.HashSet;
import java.util.Set;

public class TestSentinel { //主要完成哨兵测试

    /**
     * 参数说明:
     *   masterName: 主机名称
     *   sentinels:  哨兵节点信息.
     */
    @Test
    public void test01(){
        Set<String> sentinels = new HashSet<>();
        String node = "192.168.126.129:26379";
        sentinels.add(node);
        JedisSentinelPool sentinelPool =
                new JedisSentinelPool("mymaster", sentinels);

        Jedis jedis = sentinelPool.getResource(); //获取资源
        jedis.set("sentinel", "redis哨兵机制配置成功!!!!");
        System.out.println(jedis.get("sentinel"));
    }
}
搭建Redis集群
Redis分片/Redis哨兵总结

说明:
1).分片可以实现Redis内存数据的扩容.可以存储海量的内存数据. Redis分片机制没有实现高可用.如果分片中一个节点宕机,则直接影响整个服务的运行.
2).哨兵可以实现Redis节点的高可用.但是Redis中的数据不能实现内存的扩容.
哨兵服务本身没有实现高可用.如果哨兵发生了异常则直接影响用户使用.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值