Redis五种数据类型,底层存储数据结构,以及相关命令

redis提供了五种数据类型供使用者使用,分别是string类型,hash类型(类似于对象结构),list类型,set类型,以及zset类型(有序set),redis在底层对这些数据进行保存时使用了SDS(Simple Dynamic String),压缩列表,双向链表,快速列表,整数集合,hash表以及跳表,其中hash表在redis中使用最为广泛,具体如下

redis保存数据底层数据结构

string类型:

string类型底层是通过redis自身源码设计的数据结构SDS(Simple Dynamic String)存储,他是C语言中的一个结构体,代码结构类似如下

struct __attribute__ ((__packed__)) sdshdr {
    uint8_t len;         // 当前字符串长度
    uint8_t alloc;       // 已分配空间大小,不包括头部和终止符
    unsigned char flags; // 标志位,最低3位表示类型,剩余5位未使用
    char buf[];          // 实际的字符串数据
};

相较于普通字符串来说,SDS可以动态的管理char数组的长度(C语言保存字符串是通过char数组),其实动态管理字符数组长度并不难,普通字数组一样能做到,无非是根据字符数组长度来分配合适的内存节省空间,不过字符数组获取长度需要遍历整个字符串,并且还需要判断最后一个字符是否为null来判断是否是结尾(字符数组结构以null结尾),这一过程十分浪费性能,并且还要通过长度判断剩余空间防止缓存溢出,而通过结构体进行记录这些信息则可以提高性能,其中字符数组的扩容机制是未达到1M则翻倍,否则增加1M,在3.2版本以后则是通过sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64来进行数据的保存和扩容,其中除了sdshdr5以外,结构和之前的sdshdr一样,而sdshdr5则将alloc省略,并且将len融入flags中,结构如下

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; // 标志位,最低3位表示类型,剩余5位表示长度
    char buf[];          // 实际的字符串数据
};

这种结构下,当我们保存短字符串时,内存则被充分的使用了,而sdshdr后面的数字的意义则是保存长度信息所用的位数,比如sdshdr5用5位表示长度,最大长度位31,而sdshdr8用一个字节表示长度,也就是8位,以此类推。

hash类型:

hash数据保存的是键值对格式的数据,当保存的键值对数量较少时且每个键值对的长度较短时会使用压缩列表保存键值对字符数组,使用压缩列表保存数据可以节省内存,但当键值对数量较多(redis配置文件中的hash-max-ziplist-entries,默认值为512)或单个键值对的键和值较大(redis配置文件中的hash-max-ziplist-value,默认为 64 字节。),压缩列表的查询速度会变慢,此时redis使用hash表结构储存数据,这种转换操作是不可逆的,也就是说一旦转换为hash表结构,即使数量缩小,也不会转换会压缩列表。
hash表结构是和hashmap源码中维护的数据结构基本一致的数据结构。首先,hash表内部维护了一个数组结构,当一个键值对储存在其中时会计算key的hash值,并且将hash值与数组长度取余得到一个数组索引,并将保存key和value的指针的结构体存入其中,当当前位置已经有其他值时,会和已有元素组合成为链表,将自己的指针放入当前链表中最后一个元素的结构体的next属性中。和hashmap不同的时,首先对于负载因子,hashmap的默认为0.75,而redis实现的hash表的负载因子为1,也就是说,redis只有当数组全部占满时才会扩容,并且redis不允许自定义负载因子,而hashmap可以自定义(这也是可以理解的,redis使用的是内存资源,内存资源不像硬盘,内存资源较为珍贵,必须节省使用)。并且hashmap在数组中单个元素维护的链表过长时会将其转化为红黑树,而redis维护的hash表对于链表过长没有做处理,大致结构如下图。
请添加图片描述

List类型

在老版本的redis中储存list数据类型使用压缩列表和双向链表两个结构,其中压缩列表是在一块连续内存中保存了header(zlbytes列表字节数,zltail到最后一个元素的偏移量,allen列表长度),entry(数组内容)以及end(0xFF列表结尾表示符),结构如下

请添加图片描述
而双向链表则是讲元素变为记录前后元素指针的节点,与传统双向链表不同的是,redis实现双向链表时通过一个结构体保存了该双向链表的相关信息以及操作函数,结构体如下

typedef struct list {
    listNode *head;          // 指向链表的头节点
    listNode *tail;          // 指向链表的尾节点
    void *(*dup)(void *ptr); // 复制节点值的函数指针
    void (*free)(void *ptr); // 释放节点值的函数指针
    int (*match)(void *ptr, void *key); // 匹配节点值的函数指针
    unsigned long len;       // 链表的长度(节点数量)
} list;

元素默认以压缩列表的数据结构存储,当元素数量超过某个阀值(redis配置文件中的list-max-ziplist-entries,默认值为512),或者单个元素超过某个阀值时(redis配置文件中的list-max-ziplist-value,默认为 64 字节。),压缩链表会转换成双向链表。这种转换操作是不可逆的,也就是说一旦转换为双向链表,即使数量缩小,也不会转换会压缩链表

在新版本的redis中,redis通过快速链表保存数据,而快速链表实际上就是压缩列表和双向链表的结合体,他通过锁哥压缩列表,并且每个压缩列表记录前后压缩列表的指针实现快速链表。如下图
请添加图片描述

set类型

对于set类型数据,redis采用当数据全部是整形,并且数量不多时(redis配置项为set-max-intset-entries,默认是512),使用整形数组的结构保存数据,当数量过多或保存数据中包含非整形数据时则使用hash表存储,和hash类型使用的hash表基本一致,不过set类型没有key,所以在使用hash表结构存储数据时会直接使用元素本身转化为hash值进行取余运算。

zset类型

zset类型在维护数据时会同时维护两个数据结构,分别是hash表和跳表,当对元素进行增删改操作时会对两个数据结构都进行增删改操作,其中hash表不存在有序判断,所以zset的有序性是由跳表维护的。zset其中保存的键值对为key:score,当我们用key去查询分数时,redis会通过hash表查找,而当我们用score查找key时,则通过跳表查找,通过这样的方式,redis提高有序性的查找速度。
跳表以一种以空间换时间的形式提高查询速度,其查询方式类似于平衡二叉树。首先跳表分为多个层级,其中最底层有全部元素,第二层则有部分元素,第三层则有第二层的部分元素,以此类推,查找元素时会先从最上层开始比较,如果第一个元素比查找元素大,那么则从下一层的第一个元素找,如果第一个元素比查找元素小,则会继续向右查找,直到下一个元素比查找元素大时,则会进入下一层,从当前节点的右侧开始查找,查找过程依旧是向右查找,直到下一个元素比当前查找元素大,当找到相等的时,则查找成功。
跳表
除了最底层外,每层分布的元素是不规律的,一个元素是否出现在上一层取决于概率算法,比如说我们指定概率为二分之一,那么第一层元素出现在第二层的概率就是二分之一,而第二层元素出现在第三层的概率也是二分之一,以此类推,元素通过概率运算跃迁至上一层的过程叫升级。

对应的,层数在跳表中也是依据概率的,看元素通过概率算法升级到了多少层,就有多少层,也就是说,即使只有一个元素,也可能通过概率变为好多层(实际可能会避免这种情况),在这种概率算法下,每层元素都大概是下一层的2/1,层数也是元素越多层数越多,查找方式也更类似于平衡二叉树。
redis的跳表升级概率为四分之一。

redis相关指令及示例

string类型

SET:设置指定 key 的值

SET mykey "Hello"
# 设置 key "mykey" 的值为 "Hello"
# 返回值:OK

GET:获取指定 key 的值

GET mykey
# 获取 key "mykey" 的值
# 返回值:"Hello"

GETSET:将给定 key 的值设置为 value,并返回 key 的旧值

GETSET mykey "World"
# 将 key "mykey" 的值设置为 "World",并返回旧值 "Hello"
# 返回值:"Hello"

SETNX:只有在 key 不存在时,设置 key 的值

SETNX mykey "Hello"
# 只有在 key "mykey" 不存在时,设置其值为 "Hello"
# 返回值:1(成功设置)

SETEX:设置 key 的值,并指定该 key 的过期时间(以秒为单位)

SETEX mykey 10 "Hello"
# 设置 key "mykey" 的值为 "Hello",并在 10 秒后过期
# 返回值:OK

PSETEX:设置 key 的值,并指定该 key 的过期时间(以毫秒为单位)

PSETEX mykey 10000 "Hello"
# 设置 key "mykey" 的值为 "Hello",并在 10000 毫秒(10 秒)后过期
# 返回值:OK

MSET:同时设置一个或多个 key-value 对

MSET key1 "Hello" key2 "World"
# 同时设置 key1 的值为 "Hello" 和 key2 的值为 "World"
# 返回值:OK

MGET:获取所有给定 key 的值

MGET key1 key2
# 获取 key1 和 key2 的值
# 返回值:["Hello", "World"]

INCR:将 key 中存储的数字值增一

INCR mykey
# 将 key "mykey" 的值加 1
# 返回值:1(因为初始值为 0)

INCRBY:将 key 所储存的值加上给定的增量值

INCRBY mykey 5
# 将 key "mykey" 的值加 5
# 返回值:5

INCRBYFLOAT:将 key 所储存的值加上给定的浮点增量值

INCRBYFLOAT mykey 1.5
# 将 key "mykey" 的值加 1.5
# 返回值:1.5

DECR:将 key 中存储的数字值减一

DECR mykey
# 将 key "mykey" 的值减 1
# 返回值:-1(因为初始值为 0)

DECRBY:将 key 所储存的值减去给定的减量值

DECRBY mykey 5
# 将 key "mykey" 的值减 5
# 返回值:-5

APPEND:将值附加到 key 的现有值之后

APPEND mykey " World"
# 将 " World" 附加到 key "mykey" 的现有值之后
# 返回值:11(新值的长度)

STRLEN:获取 key 所储存的字符串值的长度

STRLEN mykey
# 获取 key "mykey" 的值的长度
# 返回值:11

GETRANGE:获取存储在 key 中的字符串的子字符串

GETRANGE mykey 0 4
# 获取 key "mykey" 的值的第 0 到 4 个字节
# 返回值:"Hello"

SETRANGE:用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始

SETRANGE mykey 6 "Redis"
# 从偏移量 6 开始,用 "Redis" 覆写 key "mykey" 的值
# 返回值:11(新值的长度)

MSETNX:同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在

MSETNX key1 "Hello" key2 "World"
# 同时设置 key1 的值为 "Hello" 和 key2 的值为 "World",当且仅当 key1 和 key2 都不存在
# 返回值:1(成功设置)

BITCOUNT:计算给定字符串中,被设置为 1 的比特位的数量

BITCOUNT mykey
# 计算 key "mykey" 的值中被设置为 1 的比特位的数量
# 返回值:3(假设 "mykey" 的值为 "Redis")

BITOP:对一个或多个存储在 key 中的字符串值,进行位元操作,并将结果存储在目标 key 中

BITOP AND result key1 key2
# 对 key1 和 key2 的值进行按位与操作,并将结果存储在 result 中
# 返回值:结果字符串的长度

BITPOS:返回字符串里面第一个被设置为1或者0的比特位的位置

BITPOS mykey 1
# 返回 key "mykey" 的值中第一个被设置为 1 的比特位的位置
# 返回值:0(假设 "mykey" 的值为 "Redis")

hash类型

HSET:将哈希表中的字段设置为指定值

HSET myhash field1 "value1"
# 设置哈希表 myhash 的字段 field1 的值为 "value1"
# 返回值:1(新字段被设置)

HGET:获取存储在哈希表中指定字段的值

HGET myhash field1
# 获取哈希表 myhash 中字段 field1 的值
# 返回值:"value1"

HSETNX:仅当字段不存在时,才设置哈希表字段的值

HSETNX myhash field2 "value2"
# 仅当 field2 不存在时,设置哈希表 myhash 中字段 field2 的值为 "value2"
# 返回值:1(字段被设置)

HMSET:同时将多个字段设置为指定值

HMSET myhash field1 "newvalue1" field3 "value3"
# 同时设置哈希表 myhash 中的多个字段
# 返回值:OK

HMGET:获取所有给定字段的值

HMGET myhash field1 field3
# 获取哈希表 myhash 中 field1 和 field3 的值
# 返回值:["newvalue1", "value3"]

HINCRBY:为哈希表中的字段值加上指定增量

HINCRBY myhash field4 5
# 将哈希表 myhash 中字段 field4 的值加上 5
# 返回值:5(新值)

HINCRBYFLOAT:为哈希表中的字段值加上指定浮点数增量

HINCRBYFLOAT myhash field5 1.5
# 将哈希表 myhash 中字段 field5 的值加上 1.5
# 返回值:1.5(新值)

HDEL:删除一个或多个哈希表字段

HDEL myhash field1
# 删除哈希表 myhash 中的字段 field1
# 返回值:1(字段被删除)

HEXISTS:检查哈希表中是否存在指定字段

HEXISTS myhash field1
# 检查哈希表 myhash 中是否存在字段 field1
# 返回值:0(字段不存在)

HKEYS:获取所有字段名

HKEYS myhash
# 获取哈希表 myhash 中所有字段名
# 返回值:["field2", "field3", "field4", "field5"]

HVALS:获取所有字段值

HVALS myhash
# 获取哈希表 myhash 中所有字段值
# 返回值:["value2", "value3", "5", "1.5"]

HLEN:获取哈希表中字段的数量

HLEN myhash
# 获取哈希表 myhash 中字段的数量
# 返回值:4

HGETALL:获取哈希表中所有字段和值

HGETALL myhash
# 获取哈希表 myhash 中所有字段和值
# 返回值:["field2", "value2", "field3", "value3", "field4", "5", "field5", "1.5"]

HSCAN:迭代哈希表中的键值对

HSCAN myhash 0 MATCH field*
# 迭代哈希表 myhash 中的键值对,匹配 field* 的字段
# 返回值:游标和匹配的键值对

list类型

LPUSH:将多个值插入到列表头部

LPUSH mylist "world" "hello"
# mylist 列表的内容为 ["hello", "world"]

RPUSH:将多个值插入到列表尾部

RPUSH mylist "!"
# mylist 列表的内容为 ["hello", "world", "!"]

LPOP:移出并获取列表的第一个元素

LPOP mylist
# 返回 "hello",mylist 列表的内容为 ["world", "!"]

RPOP:移出并获取列表的最后一个元素

RPOP mylist
# 返回 "!",mylist 列表的内容为 ["world"]

LREM:根据参数 count 的值,移除列表中与参数 value 相等的元素

LPUSH mylist "hello" "world" "hello"
# mylist 列表的内容为 ["hello", "world", "hello", "world"]
LREM mylist 1 "hello"
# 移除列表中第一个 "hello",mylist 列表的内容为 ["world", "hello", "world"]

LLEN:获取列表长度

LLEN mylist
# 返回列表长度 3

LRANGE:获取列表指定范围内的元素

LRANGE mylist 0 -1
# 返回列表中所有元素 ["world", "hello", "world"]

LINDEX:通过索引获取列表中的元素

LINDEX mylist 1
# 返回索引为 1 的元素 "hello"

LSET:通过索引设置列表元素的值

LSET mylist 1 "there"
# 将索引为 1 的元素设置为 "there",mylist 列表的内容为 ["world", "there", "world"]

LTRIM:对一个列表进行修剪,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除

LTRIM mylist 0 1
# 修剪列表,只保留索引 0 到 1 的元素,mylist 列表的内容为 ["world", "there"]

RPOPLPUSH:移除列表的最后一个元素,并将该元素添加到另一个列表并返回

RPOPLPUSH mylist myotherlist
# 从 mylist 列表中移除最后一个元素 "there",并将其插入到 myotherlist 列表的头部
# mylist 列表的内容为 ["world"]
# myotherlist 列表的内容为 ["there"]

BLPOP:移出并获取列表的第一个元素,如果列表没有元素则会阻塞列表直到等待超时或发现可弹出元素为止

BLPOP mylist 0
# 移出并获取列表的第一个元素 "world",mylist 列表为空

BRPOP:移出并获取列表的最后一个元素,如果列表没有元素则会阻塞列表直到等待超时或发现可弹出元素为止

RPUSH mylist "hello" "world"
# mylist 列表的内容为 ["hello", "world"]
BRPOP mylist 0
# 移出并获取列表的最后一个元素 "world",mylist 列表的内容为 ["hello"]

BRPOPLPUSH:从列表中弹出最后一个值,将弹出的元素插入到另一个列表的头部,并返回它;如果列表为空,阻塞直到发现可弹出元素为止

BRPOPLPUSH mylist myotherlist 0
# 从 mylist 列表中移除最后一个元素 "hello",并将其插入到 myotherlist 列表的头部
# mylist 列表为空
# myotherlist 列表的内容为 ["hello", "there"]

LINSERT:在列表的元素前或后插入元素

LINSERT mylist BEFORE "hello" "there"
# mylist 列表的内容为 ["there", "hello"]

LPUSHX:将一个值插入到已存在的列表头部,列表不存在时操作无效

LPUSHX mylist "start"
# 将 "start" 插入到 mylist 列表的头部,mylist 列表的内容为 ["start", "there", "hello"]

RPUSHX:将一个值插入到已存在的列表尾部,列表不存在时操作无效

RPUSHX mylist "end"
# 将 "end" 插入到 mylist 列表的尾部,mylist 列表的内容为 ["start", "there", "hello", "end"]

BLMOVE:阻塞弹出一个列表中的元素并将其推入到另一个列表中

BLMOVE mylist myotherlist LEFT RIGHT 0
# 从 mylist 列表左端弹出一个元素,并将其推入到 myotherlist 列表的右端

set类型

SADD:将一个或多个成员添加到集合中

SADD myset "value1" "value2"
# 将 "value1" 和 "value2" 添加到集合 myset
# 返回值:2(成功添加的成员数量)

SCARD:获取集合中的成员数量

SCARD myset
# 获取集合 myset 中的成员数量
# 返回值:2

SDIFF:返回一个集合与其他集合之间的差集

SADD otherset "value2" "value3"
SDIFF myset otherset
# 返回集合 myset 与 otherset 之间的差集
# 返回值:["value1"]

SDIFFSTORE:将一个集合与其他集合之间的差集存储到另一个集合

SDIFFSTORE diffset myset otherset
# 将 myset 与 otherset 之间的差集存储到 diffset
# 返回值:1(差集的成员数量)

SINTER:返回所有给定集合的交集

SINTER myset otherset
# 返回 myset 和 otherset 的交集
# 返回值:["value2"]

SINTERSTORE:将所有给定集合的交集存储到另一个集合

SINTERSTORE interset myset otherset
# 将 myset 和 otherset 的交集存储到 interset
# 返回值:1(交集的成员数量)

SISMEMBER:判断成员元素是否是集合的成员

SISMEMBER myset "value1"
# 判断 "value1" 是否是集合 myset 的成员
# 返回值:1(是成员)

SMEMBERS:返回集合中的所有成员

SMEMBERS myset
# 返回集合 myset 中的所有成员
# 返回值:["value1", "value2"]

SMOVE:将一个成员从一个集合移动到另一个集合

SMOVE myset otherset "value1"
# 将 "value1" 从 myset 移动到 otherset
# 返回值:1(成功移动)

SPOP:移除并返回集合中的一个随机元素

SPOP myset
# 移除并返回集合 myset 中的一个随机元素
# 返回值:"value2"

SRANDMEMBER:返回集合中的一个或多个随机成员

SRANDMEMBER myset 2
# 返回集合 myset 中的两个随机成员
# 返回值:["value1", "value2"]

SREM:移除集合中的一个或多个成员

SREM myset "value1"
# 移除集合 myset 中的 "value1"
# 返回值:1(成功移除的成员数量)

SUNION:返回所有给定集合的并集

SUNION myset otherset
# 返回 myset 和 otherset 的并集
# 返回值:["value1", "value2", "value3"]

SUNIONSTORE:将所有给定集合的并集存储到另一个集合

SUNIONSTORE unionset myset otherset
# 将 myset 和 otherset 的并集存储到 unionset
# 返回值:3(并集的成员数量)

SSCAN:迭代集合中的元素

SSCAN myset 0 MATCH value*
# 迭代集合 myset 中匹配 value* 的元素
# 返回值:游标和匹配的元素

zset类型

ZADD:向有序集合中添加一个或多个成员,或者更新已存在成员的分数

ZADD myzset 1 "one" 2 "two"
# 向有序集合 myzset 添加成员 "one" 和 "two",分数分别为 1 和 2
# 返回值:2(成功添加的新成员数量)

ZCARD:获取有序集合中的成员数量

ZCARD myzset
# 获取有序集合 myzset 中的成员数量
# 返回值:2

ZCOUNT:计算在有序集合中指定分数范围内的成员数量

ZCOUNT myzset 1 2
# 计算有序集合 myzset 中分数在 1 和 2 之间的成员数量
# 返回值:2

ZINCRBY:为有序集合中的成员的分数加上指定增量

ZINCRBY myzset 2 "one"
# 为有序集合 myzset 中的成员 "one" 的分数加上 2
# 返回值:3(新分数)

ZRANGE:通过索引区间返回有序集合成指定区间内的成员

ZRANGE myzset 0 -1 WITHSCORES
# 返回有序集合 myzset 中从索引 0 到最后一个成员的所有成员及其分数
# 返回值:["one", "3", "two", "2"]

ZRANGEBYSCORE:通过分数区间返回有序集合指定区间内的成员

ZRANGEBYSCORE myzset 1 3 WITHSCORES
# 返回有序集合 myzset 中分数在 1 和 3 之间的所有成员及其分数
# 返回值:["one", "3", "two", "2"]

ZRANK:返回有序集合中指定成员的索引

ZRANK myzset "one"
# 返回有序集合 myzset 中成员 "one" 的索引
# 返回值:1

ZREM:移除有序集合中的一个或多个成员

ZREM myzset "one"
# 移除有序集合 myzset 中的成员 "one"
# 返回值:1(成功移除的成员数量)

ZREMRANGEBYRANK:移除有序集合中指定索引区间内的所有成员

ZREMRANGEBYRANK myzset 0 1
# 移除有序集合 myzset 中从索引 0 到 1 的所有成员
# 返回值:2(成功移除的成员数量)

ZREMRANGEBYSCORE:移除有序集合中指定分数区间内的所有成员

ZREMRANGEBYSCORE myzset 1 3
# 移除有序集合 myzset 中分数在 1 和 3 之间的所有成员
# 返回值:2(成功移除的成员数量)

ZREVRANGE:返回有序集合中指定索引区间内的成员,按分数从高到低排序

ZREVRANGE myzset 0 -1 WITHSCORES
# 返回有序集合 myzset 中从索引 0 到最后一个成员的所有成员及其分数,按分数从高到低排序
# 返回值:["one", "3", "two", "2"]

ZREVRANGEBYSCORE:通过分数区间返回有序集合中指定区间内的成员,按分数从高到低排序

ZREVRANGEBYSCORE myzset 3 1 WITHSCORES
# 返回有序集合 myzset 中分数在 3 和 1 之间的所有成员及其分数,按分数从高到低排序
# 返回值:["one", "3", "two", "2"]

ZREVRANK:返回有序集合中指定成员的索引,按分数从高到低排序

ZREVRANK myzset "one"
# 返回有序集合 myzset 中成员 "one" 的索引,按分数从高到低排序
# 返回值:0

ZSCORE:返回有序集合中指定成员的分数

ZSCORE myzset "one"
# 返回有序集合 myzset 中成员 "one" 的分数
# 返回值:"3"

ZUNIONSTORE:计算给定的一个或多个有序集合的并集,并存储到目标有序集合中

ZUNIONSTORE myzset2 2 myzset otherset
# 计算有序集合 myzset 和 otherset 的并集,并存储到 myzset2 中
# 返回值:并集的成员数量

ZINTERSTORE:计算给定的一个或多个有序集合的交集,并存储到目标有序集合中

ZINTERSTORE myzset2 2 myzset otherset
# 计算有序集合 myzset 和 otherset 的交集,并存储到 myzset2 中
# 返回值:交集的成员数量

ZSCAN:迭代有序集合中的元素

ZSCAN myzset 0 MATCH "one*"
# 迭代有序集合 myzset 中匹配 "one*" 的元素
# 返回值:游标和匹配的元素
  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

不止会JS

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

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

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

打赏作者

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

抵扣说明:

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

余额充值