Redis基础1

Redis数据类型

redis一共有几种数据类型?(注意我们说的是数据类型,不是数据结构)
String、Hash、Set、List、Zset、bitmaps、HyperlogLog、Geo、Streams

String最基本的数据类型

存储类型:
用来存储int、float、String
eg:

127.0.0.1:6379> set a 6379
OK
127.0.0.1:6379> get a
"6379"
127.0.0.1:6379> type a
string
127.0.0.1:6379> exists a
(integer) 1
127.0.0.1:6379> set b 3.14
OK
127.0.0.1:6379> type b
string
127.0.0.1:6379> set c helloword
OK
127.0.0.1:6379> type c
string

可以看到a=6379、b=3.14、c=helloword,它们的type都是String。

批量设置string操作:

127.0.0.1:6379> mget a b c
1) "6379"
2) "3.14"
3) "helloword"
127.0.0.1:6379> mset e hello f world
OK
127.0.0.1:6379> mget e f
1) "hello"
2) "world"

setnx命令

setnx 设置值,如果key存在,则不成功。

127.0.0.1:6379> setnx f 3
(integer) 0           # f已经存在,设置失败
127.0.0.1:6379> setnx g 1234
(integer) 1           # 设置成功

基于此我们可以实现分布式锁,用del key释放锁。
但是如果释放锁失败了,导致其它节点永远获取不到锁,怎么办?
需要加一个过期时间。
但是单独加过期时间也失败了,怎么办?
我们需要保证setnx和expire命令的原子性。
set key value EX 5 NX
eg:

127.0.0.1:6379> set k1 123 ex 10 nx
OK
127.0.0.1:6379> get k1
"123"

ex 10 代表10秒过期。

redis存储实现原理

redis是KV的数据库,key-value我们一般会用什么数据结构来存储它?哈希表。redis的最外层确实是通过hashtable实现的。(我们把这个叫做外层hash)
在redis里面,这个哈希表怎么实现的呢?我们看一下C语言源码,每个键值对都是一个dicEntry,通过指针指向key的存储结构和value的存储结构,而且next存储了指向下一个键值对的指针

/*
 * hash节点
 */
typedef struct dictEntry {
    //键
    void *key;
    //值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    //指向下一个节点
    struct dictEntry *next;
} dictEntry;

这个是存储,实际上最外层是redisDb,redisDb里面放的是dict。源码server.h 661行截图
在这里插入图片描述
以set hello world为例,因为key是字符串,redis自己实现了一个字符串类型,叫做SDS,所以hello指向一个SDS的结构。
在这里插入图片描述
value=world,同样也是一个字符串,是不是也用SDS存储呢?
当value存储一个字符串的时候,Redis并没有直接使用SDS存储,而是存储在redisObject中。实际上5种常用的数据类型的任何一种value,都是通过redisObject来存储的。
最终redisObject再通过一个指针指向实际的数据结构。比如字符串或者其它。
redisObject源码定义:

/*src/redis.h/redisObject */
typedef struct redisObject {
    // 刚刚好32 bits   
    unsigned type:4; // 对象的类型,包括:字符串/列表/set/哈希表/OBJ_ZSET
    // 未使用的两个位
    unsigned notused:2; /* Not used */
    // 编码的方式,Redis 为了节省空间,提供多种方式来保存一个数据
    // 譬如:“123456789” 会被存储为整数123456789
    unsigned encoding:4;
    // 当内存紧张,淘汰数据的时候用到
    unsigned lru:22; /* lru time (relative to server.lruclock) */    
    int refcount;  // 引用计数  
    void *ptr;// 数据指针,指向真正的数据
} robj; 

用type命令可以看到的类型就是type内容。

127.0.0.1:6379> type c
string

SDS是什么

redis中字符串的实现,Simple Dynamic String简单动态字符串。(简称SDS)

typedef char *sds;
struct sdshdr {
        // 记录 buf 数组中已使用字节的数量
        // 等于 SDS 所保存字符串的长度
        int len;
        // 记录 buf 数组中未使用字节的数量
        int free;
        // 字节数组,用于保存字符串
        char buf[];
};

本质上其实还是字符串数组。
为什么redis要用SDS实现字符串?
因为c语言本身没有字符串类型,只能用字符数组char[]实现。
使用数组必须给目标变量分配足够的空间,否则可能会溢出。
虽然value值最终都是String,但是出现了3种不同的编码。
这3种编码的区别:
1)int,存储8个字节的长整型(long,2^63 -1)
2)embstr,代表embstr格式的SDS,存储小于44字节的字符串。
3)raw,存储大于44字节的字符串。

redis应用场景

缓存,String类型,用于缓存热点数据。
分布式数据共享:
String类型,因为Redis是分布式的独立服务,可以在多个应用之间数据共享。例如:分布式session

<dependency>
	<groupId>org.springframework.session</groupId>
	<artifactId>spring-session-data-redis</artifactId>
</dependency>

分布式锁:
string类型的setnx方法,只有不存在时才能添加成功,返回true。
全局ID
int类型,incrby,利用原子性。
计数器
int类型,incr方法
例如:文章阅读量,微博点赞数,允许一定的延迟,先写入redis再定时同步数据库。
限流
int类型,incr方法
以访问者的IP或者其他信息作为key,访问次数加一计数,超过次数返回false。

Hash哈希

hash用来存储多个无序的键值对。。最大存储2^32-1(40亿左右)
我们前面所说redis所有的KV本身就是键值对,用dictEntry实现的,叫做外层的哈希。现在所说的是内层的哈希。
在这里插入图片描述
注意:Hash的value只能是字符串,不能是其它嵌套类型。比如hash或list。
同样是存储字符串,Hash和String的主要区别?
1、把所有相关的值聚集到1个key中,节省内存空间。
2、只是用一个key,减少key冲突
3、当需要批量获取值得时候,只需要一个命令,减少内存IO和cpu的消耗。
Hash不适合的场景:
1)field不能单独设置过期时间
2)需要考虑分布的问题(field非常多的时候,无法分布到多个节点)

hset    h1 f 6
hset    h1 e 5
hmset   h1 a 1 b 2 c 3

Hash存储实现原理

redis的hash本身也是一个KV结构,是不是跟外层一样,用dicEntry实现的?
内层的哈希底层可以使用2种数据结构实现:

ziplist:压缩列表
hashtable:哈希表

ziplist压缩列表

ziplist是一个经过特殊编码,由连续内存块组成的双向链表
它的特殊在于---->它不存储指向上一个链表的节点和指向下一个链表节点的指针,而是存储上一个节点长度和当前节点的长度。这样读写可能会慢一些,因为你要去算长度,但是节省了内存,是一种时间换空间的思想。

ziplist的内部结构图:

在这里插入图片描述
我们关心的entry内容什么样?
在这里插入图片描述
previous_entry_length 字段表示前一个元素的字节长度
encoding 字段表示当前元素key的编码,记录了节点的 content 字段所保存数据的类型以及长度
content 字段存储节点的值value,节点值可以是一个字节数组或者整数,值的类型和长度由节点的 encoding 属性决定。

什么时候使用ziplist存储?

当满足下面2个条件,使用ziplist编码
1)哈希对象的键值对数量<512个
2)所有键值对的键和值的字符串长度都<64byte(一个英文字母一个字节)
如果不满足这两个条件任何一个,存储结构就会转换成hashtable。

hashtable

redis的KV结构是通过一个dicEntry来实现的。
在hashtable中,又对dicEntry进行了多层封装。
它是一个数组+链表的结构。
但是这里它定义了2个哈希表,为什么定义2个?
原因是为了扩容,在redis里这种操作叫做rehash。另一个空的hash表就是为扩容做准备的。

hash应用场景

存储对象类型数据:
比如对象或者一张表的数据,比String节省更多key空间,便于集中管理。
eg:
比如购物车,key=用户id,field:商品id,value:商品数量。

List列表

存储有序的字符串,元素可重复。最大存储2^32-1(40亿左右)
早期redis版本,数据量小的时候使用ziplist,达到临界值时候转换为linkedlist。
3.2版本以后,统一用quicklist来存储。quicklist存储一个双向链表,每个节点都是ziplist,所以是ziplist和linkedlist的结合体。
在这里插入图片描述

list应用场景

list主要用于存储有序内容的场景。
eg:
例如用户的消息列表,活动列表、评论列表。
队列/栈
list可以分布式环境用来做队列/栈使用
list提供了2个阻塞弹出的操作:blpop/brpop,可以设置超时时间。

127.0.0.1:6379> rpush books a b c
(integer) 3
127.0.0.1:6379> lpop books
"a"
127.0.0.1:6379> lpop books
"b"
127.0.0.1:6379> lpop books
"c"
127.0.0.1:6379> lpop books
(nil)

作为栈:

127.0.0.1:6379> rpush books 1 2 3
(integer) 3
127.0.0.1:6379> rpop books
"3"
127.0.0.1:6379> rpop books
"2"
127.0.0.1:6379> rpop books
"1"

set集合

存储String类型的无序不重复集合,最大存储2^32-1(40亿左右)
操作命令eg:

127.0.0.1:6379> sadd myset a b c d e f //添加set元素
(integer) 6
127.0.0.1:6379> sadd myset a c g //添加set元素
(integer) 1
127.0.0.1:6379> smembers myset//查看set元素
1) "b"
2) "e"
3) "c"
4) "d"
5) "g"
6) "a"
7) "f"
127.0.0.1:6379> scard myset//统计set元素
(integer) 7
127.0.0.1:6379> sismember myset a //查看某个元素是否存在
(integer) 1

redis用inset或hashtable来存储set。如果元素都是整数,就用inset存储。
如果不是整数,就用hashtable。
如果整数超过512,也用hashtable来存储。

set应用场景

点赞、签到、打卡
eg:
微博id是t1001,用户id是u2001
用like:t1001来维护记录点赞用户。
点赞这条微博:sadd like:t1001 u3001
取消点赞:sadd like:t1001 u3001
点赞的所有用户:smembers like:t1001
点赞数统计: scard like:t1001

zset有序集合

默认使用ziplist编码,在内部使用score排序递增。
如果元素大于128个,或者任一member长度大于64字节,使用skiplist+dict来存储。

skiplist

在这里插入图片描述
在一个链表中,比如我们要查找一个数据,需要从头开始比较。那么时间复杂度为O(n)。二分法只适合数组,不适合链表。
假设我们每2个节点增加一个指针,这样就形成了一个新的链表。我们查找的时候沿着这个新的链表查找比较。当碰到元素大的时候,再往下个节点查找。

应用场景

顺序会动态变化的场景。

redis其它数据结构

BitMaps

bitmaps是在字符串类型上面定义的位操作。一个字节由8个二进制位组成。
在这里插入图片描述

127.0.0.1:6379> setbit liyang 0 1
(integer) 0
127.0.0.1:6379> setbit liyang 1 1
(integer) 0
127.0.0.1:6379> setbit liyang 2 1
(integer) 0
127.0.0.1:6379> getbit liyang 1
(integer) 1
127.0.0.1:6379> bitcount liyang   //统计1的个数
(integer) 3

因为bit非常节省空间(1MB=8388608bit),可以用来做大数据量的统计。

应用场景

在线用户的统计、用户访问统计

HyperlogLogs

HyperlogLogs提供了不太精确的基数统计方法,用来统计集合中不重复的元素个数。比如网站的UV。

Geo

redis的Geo用来保存用户地理位置数据,并且提供了计算2个位置距离的方法。

127.0.0.1:6379> geoadd location 112.881953 28.238426 beijing
(integer) 1
127.0.0.1:6379> geopos location beijing
1) 1) "112.8819546103477478"
   2) "28.23842480810194644"

Streams

5.0推出的新的数据类型。

Redis高级原理

前面说过通过rpush和blpop可以实现消息队列,没有任何元素弹出的时候,连接会被阻塞。
但是基于list实现的消息队列,不支持一对多的消息发放,相当于只有1个消费者。
如果要实现一对多的消息发放,怎么办?

发布订阅模式

除了通过list实现消息队列之外,Redis还提供了发布订阅功能。
生产者和消费者是不同的客户端,连接到一个redis服务上。通过什么把生产者和消费者关联起来呢?
在RabbitMq中叫Queue,在kafka中叫Topic。Redis模型里面叫channel(频道)
订阅者可以订阅一个或者多个channel。消息发送者可以给指定的channel发布消息。只要有消息达到了channel,所有订阅这个channel的订阅者就会收到这条消息。
在这里插入图片描述

Redis事务

Redis单个命令是原子性的,不存在并发干扰的问题。
如果涉及多个redis命令的时候,要把多个命令视为不可分割的处理程序,就必须依赖redis的功能特性来实现了。
Redis提供了事务的特性,可以把一组命令一起执行。

Redis事务用法

redis事务涉及4个命令:multi(开启事务)、exec(执行事务)、discard(取消事务)、watch(监视)
Redis从2.6版本引入了Lua脚本,也就是说可以利用Lua脚本来执行redis命令。

Lua脚本

lua是一种轻量级脚本,它是C语言编写的。
使用lua脚本执行redis的好处:
1)一次可以发送多个命令,减少网络开销
2)Redis会将整个lua脚本作为一个整体执行,不会被其它请求打断,保持了原子性
3)对于复杂的命令,可以放在文件中,可以实现命令的复用。

lua脚本的缓存

如果lua脚本比较长的话,每次把整个lua脚本传到redis服务端,会产生比较大的网络开销。为了解决这个问题,Redis可以缓存lua脚本并生成SHA1摘要码,后面只需要通过摘要码来执行lua脚本。

Redis为什么这么快

测试发现Redis每秒处理13万多次set请求。按照这个测试结果,说明redis的QPS10万还是比较准确的。
redis之所以这么快,总结起来3点:

1、纯内存结构
2、请求处理单线程
3、多路复用机制

内存
KV结构的内存数据库,时间复杂度O(1)。
单线程
单线程其实指的是处理客户端请求是单线程的,可以把它叫做主线程。还引入了一些线程处理其它事情,比如无用的连接释放、大key的删除、清理脏数据。

把处理请求的主线程设置成单线程有什么好处?

1、没有创建线程、销毁线程带来的消耗
2、避免了上下文切换的CPU消耗
3、避免了线程之间带来的竞争问题,例如加锁释放锁

单线程确实有这些好处,但是会不会白白浪费cpu资源?也就是说只用到了单核。
官方文档解释这样的:
在Redis里单线程已经够用了,CPU不是redis的瓶颈。redis的瓶颈是内存和网络带宽。
注意,因为请求处理是单线程的,不要再生产环境运行长命令,比如keys、flushdb。否则会导致请求被阻塞

内存

计算机里面的内存,我们叫做主存。
主存可以看做是一个很长的数组,一个字节一个单元,每一个字节有一个唯一的地址,这个地址叫做物理地址(Physical Address)
早期的计算机中,如果CPU需要内存,使用物理寻址,直接访问主存器。
在这里插入图片描述
看起来合乎情理,但是这种方式有几个弊端:

1、一般的操作系统都是多用户多任务的,所有的进程共享主存。如果每个进程单独占用一块物理地址空间,主存很快就会被用完。
我们希望在不同的时刻,不同的进程可以共用同一块物理地址空间。
2、如果所有的进程都是直接访问物理内存,那么一个进程就可以修改其它进程的内存数据,导致物理地址空间的破坏,程序运行就会出现异常。

咋办呢?对于物理内存的使用,应该有一个角色协调指挥。工程师们想了一个办法在主存和CPU之间增加一个中间层。CPU不再使用物理地址访问主存,而是访问一个虚拟地址,再由这个中间层把虚拟地址转换成物理地址,最终获取数据。这个中间层叫做MMU(Memory Management Unit),内存管理单元。
在这里插入图片描述
我们访问MMU和访问物理内存一样,所以把虚拟出来的地址叫做虚拟内存(Virtual Memory)。
在每个进程创建的时候,都会分配一段虚拟地址,然后通过虚拟地址和物理地址映射来获取真实数据,这样进程就不会直接接触到物理地址,甚至不知道用的哪块物理地址的数据。
实际物理内存可能远远小于虚拟内存的大小。
总结:引入虚拟内存的作用:

1、通过把同一块物理内存映射到不同的虚拟地址空间实现内存共享
2、对物理内存进行隔离,不同的进程操作互不影响。
3、虚拟内存可以通过更大的地址空间,并且地址空间是连续的,使得程序编写、链接更加简单。

Linux/GNU的虚拟内存又进一步划分了两块:

用户空间和内核空间

一部分是内核空间(Kernel Space),一部分是用户空间(User Space)
在这里插入图片描述
这两块空间的区别是什么呢?
进程在用户空间中存放的是用户程序的代码和数据,内核空间中存放的是内核代码和数据。不管是内核空间还是用户空间,他们都处于虚拟内存空间中,都是对物理地址的映射。
当进程运行在内核空间时就处于内核态,而进程运行在用户空间则处于用户态。
进程在内核空间可以访问受保护的内存空间,也可以访问底层硬件设备,也就是可以执行任意命令,调用系统的一切资源。在用户空间只能执行简单的运算,不能直接调用系统资源,必须通过系统接口(又称 system call),才能向内核发出指令。
所以,这样划分的目的是为了避免用户进程直接操作内核,保证内核的安全。

进程切换(上下文切换)

多任务操作系统是怎么实现运行远大于CPU数量的任务呢?当然,这些任务并不是真的在同时运行,而是因为系统通过时间片算法,在很短时间内,将CPU轮流分配给它们,造成多任务同时运行的错觉。

在这个交替运行的过程里面,为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,以及恢复以前挂起的某个进程的执行。这种行为被称为进程切换

什么叫上下文(context)?
在每个任务运行之前,CPU都需要知道任务从哪里加载,又从哪里运行。需要系统事先帮它设置好CPU寄存器和程序计数器(Program Counter),这个叫做CPU的上下文。
而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证原来的状态不受影响,让任务看起来还是连续运行。

进程的阻塞

正在运行的进程由于提出系统服务请求(如I/O操作),但因为某种原因未得到操作系统的立即相应,该进程只能把自己变成阻塞状态,等待响应的事件出现后才被唤醒,进程阻塞转态下不占用CPU资源。

文件描述符FD

Linux系统中一切皆为文件。
Linux系统将所有设备都当做文件来处理,而Linux用文件描述符来标识每个文件对象。
文件描述符(File Descriptor)是内核为了高效管理已被打开的文件所创建的索引,用于指向被打开的文件。所有执行I/O操作的系统调用都通过文件描述符。
文件描述符是一个简单的非负整数,用以表明每个被进程打开的文件。Linux系统有3个标准的文件描述符。
0:标准输入(键盘);
1:标准输出(显示器)
2:标准错误输出(显示器)

传统I/O数据拷贝

当用户执行read系统调用读取文件描述符FD的时候,如果这块数据以及存在于用户进程的页内存中,就直接从内存中读取数据。如果数据不存在,则先将数据从磁盘加载数据到内核缓冲区中,再从内核缓冲区拷贝到用户进程的内存中。(2次拷贝,2次user和kernel的上下文切换)
在这里插入图片描述
I/O阻塞到底阻塞在哪里?
其实就是阻塞在等待数据的时候。需要一直等待。

Blocking I/O

当使用read或write对某个文件描述符进行读写时,如果当前FD不可读,系统就不会对其他操作做出相应。从硬件设备复制数据到内核缓冲区是阻塞的,从内核拷贝到用户空间,也是阻塞的,直到copy complete,内核才返回结果,用户进程才解除block状态。
在这里插入图片描述
为了解决阻塞的问题,产生了I/O多路复用技术。

I/O多路复用

I/O指的是网络I/O
多路指的是多个TCP连接(Socket或Channel)
复用指的是复用一个或多个线程
它的基本原理就是不再由应用程序自己监视连接,而是由内核替代应用程序监视文件描述符。
客户端在操作的时候,会产生具有不同事件类型的socket。在服务端,I/O多路复用程序(I/O Multiplexing Module)会把消息放入队列中,然后通过文件事件分派器(File event Dispatcher),转发到不同的事件处理器中。
多路复用的实现有很多,有select()、poll()、epoll()。
I/O多路复用的特点就是让一个进程同时等待多个文件描述符,而这些文件描述符其中任意一个进入读就绪(readable)状态,select()就可以返回。

在这里插入图片描述
Redis本质上是一个存储系统。所有存储系统在数据量过大的时候都面临存储瓶颈。
首先要解决2个问题:

1)作为一个内存的KV系统,Redis服务肯定不能无限的使用内存,应该设置一个上限(max_memory)。
2)数据应该有过期属性,这样就可以清楚不再使用的key。

网上曾经有一个神经病面试题:Redis内存满了怎么办?不过正常的人不会这么问,这个角度有点刁钻。
我们看一下key过期怎么处理,再看内存上线怎么处理。

Redis内存回收

实现key的过期策略,有几种思路。

1)主动删除(主动淘汰)设置过期时间的key创建一个定时器,到期就会立即清除。如果key的数量非常庞大,会占用大量CPU资源去跑大量定时器。所以pass这种方案。
2)惰性删除(被动淘汰)
只有当访问一个key时,才会判断key是否过期,过期则清除。极端情况下可能出现大量key没有被访问,也不会被清除,占用大量内存。
3)定时删除 每隔一段时间,会扫描一定数量的数据库expires字典中一定数量的key,并清除。

淘汰策略

淘汰算法有以下几种

LRU,Least Recently Used:最近最少使用。保存最近使用的key,删除长时间没用的。
LFU,LeastFrequently Used,最不常用,按照最近使用频率删除。
Random,随机删除。

redis提供了volatile-lru、volatile-lfu、volatile-random以及allkeys-lru、allkeys-lfu、allkeys-random策略。
从前缀来分:volatile是针对ttl的key,allkeys是针对所有key。
另外还有noeviction、volatile-ttl。
volatile-ttl:根据ttl属性,删除将要过期的key。
noevictionredis 默认策略不删除任何数据。
建议:开启使用volatile-lru,优先删除最近最少使用的key

LRU算法

LRU是一个很常见的算法,比如InnoDB的Buffer Pool也用到了LRU。
传统的LRU:通过链表+HashMap实现。设置链表的长度,如果新增或者被访问,就移动到头结点。超过链表长度,末尾节点被删除。

LFU算法

会有一个计数器counter,在一段时间内访问就会加,直到加到counter=255。如果没有访问就逐渐开始减,直到减为counter=0.

持久化机制

redis数据保存在内存中。如果宕机,就会丢数据。为保持数据不丢失Redis提供了2种持久化方案,一种是RDB快照(Redis DataBase),一种是AOF(Append Only File)。持久化是Redis和Memcache的主要区别。

RDB

RDB是redis默认的持久化方案。。当满足一定条件的时候,会把当前内存的数据写入磁盘,生成一个dump.rdb文件。Redis重启会通过加载dump.rdb文件恢复数据。
什么时候写入RDB?

save 900 1 # 900秒至少有1个key被修改(包括添加)
save 300 10 # 300秒至少10个key被修改
save 60  10000 # 60秒至少1万个key被修改

注意以上配置是不冲突的,只要满足任意一个就会被触发。
用lastsave命令可以查看最近一次成功生成的快照时间。

RDB的优势劣势

优势:

1、RDB保存了某个时间点上的数据集。
2、生成RDB文件的时候,redis会fork()一个子进程来处理保存工作,主进程不需要进行任何磁盘IO的工作。
3、RDB在恢复大数据集的速度比AOF快。

劣势:
1、RDB没法做到实时持久化/秒级持久化。
2、在一定时间间隔做一次备份,如果redis意外down掉的话,会丢失最后一次快照后的数据。(有数据丢失)
如果数据非常重要,可以用AOF来持久化。

AOF

redis默认不开启AOF。AOF采用日志的形式来记录每个写操作,并追加到文件中。
Redis重启会根据日志文件把写指令从前到后执行一次已完成数据的恢复工作。

AOF配置

appendonly no  # 开关
appendfilename “appendonly.aof”

AOF的优势劣势

优点:

1、AOF持久化的方法提供了多种的同步频率,即使使用默认的同步频率每秒同步1次,Redis最多也就是丢失1秒的数据而已。

缺点:

1、对于相同数据的redis,AOF文件会比RDB文件大很多。
2、虽然AOF提供了多种同步频率,默认情况下,每秒同步一次也有很高性能。在高并发情况下,RDB比AOF具有更好的性能保证。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值