Redis 简介

一.  简介

Redis是⼀种基于键值对(key-value)的 NoSQL 数据库(非关系型数据库)(而MySQL是基于表的数据库(关系型数据库))。

1. 特性

In-memory data structures

Redis 是将数据以键值对的方式存储在内存之中的。键值对中的value可以是由 string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)、Bitmaps(位图)、HyperLogLog、GEO(地理信息定位)等多种数据结构和算法组成。

Programmability

Redis 具有可编程性。在Redis 7及更高版本中,可以使用Redis函数来管理和运行脚本,而在Redis 6.2及更低版本中,可以使用Lua脚本和EVAL命令对Redis进行编程。

而当运行脚本或函数时,Redis保证其原子执行。脚本的执行在其整个时间内阻塞了所有服务器活动,类似于事务。因此,如果打算在应用程序中使用慢速脚本,所有其他客户端都被阻止,并且在运行时无法执行任何命令。

但脚本的执行时间最长(默认设置为5秒),当脚本达到超时阈值时,Redis不会自动终止脚本。而是再次开始接受来自其他客户端的命令,但将以BUSY错误回复发送正常命令的所有客户端。此状态下唯一允许的命令是SCRIPT KILL、FUNCTION KILL和SHUTDOWN NOSAVE。可以使用SCRIPT KILL和FUNCTION KILL命令终止仅执行只读命令的脚本。如果脚本执行了写入操作,那么我们就只能使用SHUTDOWN NOSAVE命令,它会在不将当前数据集保存在磁盘上的情况下停止服务器。

Extensibility

Redis 具有可扩展性。Redis的API模块本质上是一个动态链接库,当其被导出时,是一个名为redismodule.h的C头文件, 可以使用C++或其他具有C绑定功能的语言来进行编写(例如C++ / Rust)。在编写完毕之后,可以在启动时或使用MODULE LOAD命令加载到Redis中。

我们也可以通过使用redis.conf 的配置指令来测试正在开发的模块。

Persistence

Redis 具有可持久化性。虽然Redis是将数据存储在内存之中的,而内存中的数据容易丢失(进程退出或系统重启等)。但Redis也可以将数据存储在磁盘之中来作为备份避免数据的丢失。

Clustering

Redis 支持集群。Redis使用名为Redis Cluster的部署拓扑结构进行水平扩展。通过水平扩展,我们可以引入多个主机,部署多个Redis节点,使每个Redis节点存储部分的数据,来解决Redis存储空间有限的问题。

High availability

Redis 具有高可用性。Redis本身也支持主从结构,通过使用从节点作为主节点的副本,每次链接断开时,复制副本都会自动重新连接到主副本,并且无论主副本发生了什么,都会尝试成为它的精确副本。
该系统使用三种主要机制工作:
1.当主实例和副本实例连接良好时,主实例通过向副本发送命令流来复制由于客户端写入、密钥过期或逐出、任何其他操作更改主数据集而对数据集产生的影响,从而保持副本的更新。
2.当主服务器和副本之间的链路由于网络问题或由于在主服务器或副本中检测到超时而断开时,副本会重新连接并尝试进行部分重新同步:这意味着它将尝试只获取断开连接期间错过的命令流的一部分。
3.当无法进行部分重新同步时,复制副本将要求进行完全重新同步。这将涉及一个更复杂的过程,在这个过程中,主机需要创建其所有数据的快照,将其发送到副本,然后随着数据集的变化继续发送命令流。

2. 用例

缓存和会话存储

Redis的快速度、Redis所提供的键值过期时间、Redis所设置的灵活控制最⼤内存和内存溢出后的淘汰策略。使其非常适合缓存数据库查询、复杂计算、API调用和会话状态。

而Redis的快速度取决于多个方面

  1. Redis 将数据存储在内存中,相较于MySQL所存储的磁盘来说,读写速度要快很多。
  2. Redis 所提供的操作内存数据的数据结构比较简单(对键值对的增删查改等)。
  3. Redis 使用的是单线程模型(对数据的读写为单线程,而在进行网络IO的时候为多线程),减少了多线程之间的竞争开销。
  4. 处理网络IO时,使用了epoll之类的IO多路复用机制。

实时数据存储

Redis的多功能内存数据结构能够为需要低延迟和高吞吐量的实时应用程序构建数据基础设施。例如计数器应⽤、排⾏榜系统、社交⽹络等。

流媒体和消息

流数据类型支持高速率数据接收、消息传递、事件源和通知。

3. 通用命令

遍历

KEYS pattern

通过 key 值在 redis 中查询,其中 pattern可以进行模糊匹配。

? 表示匹配单个任意字符,* 表示匹配0个或多个任意字符,[ ] 表示匹配中括号内的任意单个字符,[^ ] 表示匹配除开中括号内的任意单个字符,[a-b]表示匹配单个从a到b之间的字符。

由于 Redis 为单线程模型,执行一条命令时无法执行其他命令,因此当 Redis 中键值对数目过多的时候,执行 keys 命令会造成阻塞问题。

SCAN cursor [match pattern] [count number]

渐进式遍历,与 keys 命令不同的是,该命令每执行一次,只会遍历部分键值对,当再次执行时,会继续执行剩下的键值对。

cursor 为一个整形游标,表示本次从哪个位置开始遍历(该游标是由 Redis 本身决定的,我们无法知晓每个游标位置的 key 是什么),初始为0。

match pattern 就类似于 keys 命令,可以进行模糊匹配,不带该选项则表示匹配所有 key。

count number 表示一次遍历 number 个 key ,但 number 只是提供一个建议,而事实上一次遍历多少个 key 还是要由 Redis 本身决定。

该命令返回值为两部分,第一部分为当前遍历的 key 之后的下一个游标位置,可以根据该返回值来作为下次 scan 命令的 cursor 来进行之后的遍历。

而第二部分则为遍历的 key。

 键管理

EXISTS key [key ...]

查询是否存在 key 的键值对,并返回存在的个数。

RANDOMKEY

随机返回一个 key。 

DBSIZE

返回键值对的个数,由于Redis中有内置的键值对个数,因此该命令的时间复杂度为O(1) 。

RENAME key newkey
RENAMENX key newkey

重命名,若是 newkey  已经存在,则会 / 不会对原本的 newkey 键值对覆盖。

DEL key [key ...]

删除键值对,并返回删除成功的个数。

TYPE key

获取键值对中 value 的数据类型。

键过期

EXPIRE key seconds
PEXPIRE key milliseconds

EXPIREAT key timestamp
PEXPIREAT key milliseconds-timestamp

为键值对设置过期时间(s / ms) / 时间戳(秒级 / 毫秒级),当该键值对过期时间归零时 / 当前时间到达时间戳对应时间是时,自动将其删除。过期时间可以为负值且时间戳可以小于当前时间戳,这种情况下,会之间删除该键值对。

若是维护每一个键值对的精准的过期时间,那么会消耗大量的资源,因此 Redis 采用惰性删除和定期任务删除机制来回收过期的键值对。

惰性删除机制指的是当键值对过期时先不做处理,知道该过期键被读取。但若是过期键长时间不被读取,会出现内存泄漏的问题,因此需要定期任务删除机制的配合。

定期任务删除机制指的是设置一个时间间隔,每个时间段进行一个对过期键的批量回收。而当键值对的数量过大时,也可以每个时间段对一部分的键值对进行检索和回收。

TTL key
PTTL key

获取键值对剩余的过期时间(s / ms)。当 key 值所对应的键值对不存在时(包括超出过期时间后被删除),返回-2,而当该键值对没有被设置过期时间时,返回-1,否则返回该键值对所剩的过期时间。

PERSIST key

清除 key 键值对的过期时间

二.  数据结构

redisObject对象

Redis 将所有类型的 value 都存储在 redisObject 结构体中。

type 表示当前对象锁使用的数据类型,由数字表示,从1到5依次为string、hash、list、set、zset

encoding 表示 Redis 内部编码类型

lru 表示对象最后一次的访问时间

refcount 表示当前对象被引用的次数,当 refcount 为0时可以安全回收当前对象空间,可以通过object refcount key 命令来查看键值对的 refcount 。

*ptr 指向实际存储的数据类型

1. string

在Redis的键值对中,key的类型固定为string,而value的类型包含很多种。

而Redis的string数据结构与我们在语言学习中所学到的字符串有所不同,Redis中的string是以二进制的方式来存储数据,不会做任何的编码转换。因此该数据结构可以存储各种数据。

string类型大小限制为512M。

命令

读写命令
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]

set 命令用于设置 key 值所对应的 value ,当 key 值所对应的键值对不存在时,创建新的键值对来存储 key 值和 value 值的对应关系,若是存在,则将其 value 值进行更新。要注意的是,在更新好value 值之后,先前设置的过期时间作废。

后面的 expiration 选项,表示在 set 的同时为该键值对设置过期时间(若是原本键值对就存在过期时间,若是没有加 expiration 选项,则去除原本的过期时间),EX 表示单位为秒,PX 表示单位为毫秒。相当于在 set key value 之后使用 expire 命令设置过期时间,但与之不同的是,set 后面接 expiration 选项的操作是原子性的,且只需要进行一次网络通信。

而最后的 NX|XX,NX 表示只有 key 值所对应的键值对不存在时进行创建新的键值对,而当其存在时不做处理。而 XX 相反。

SETNX key value

相当于 set key value NX

SETEX key seconds value

相当于 set key value EX seconds

SETPX key seconds value

相当于 set key value PX seconds

MSET key value [key value ...]

进行批量的 set 操作,同样具有原子性以及只进行一次网络通信。

GET key

通过 key 值获取 value 值。

MGET key [key ...]

批量进行 get 操作。

计数命令
INCR key

对 key 值对应的 value 进行+1操作,并返回+1后的值。

INCRBY key increment

 对key值对应的 value 进行 +increment 操作,并返回 +increment 后的值。increment 可以为负数。

DECR key

 对 key 值对应的 value 进行-1操作,并返回-1后的值。

DECRBY key decrement

 对 key 值对应的 value 进行 +decrement 操作,并返回 +decrement 后的值。decrement 可以为负数。 

上面的四个命令,都要求 key 所对应的 value 和 increment、decrement 为整数,且原本的 value值、increment、decrement、以及加减后的值需要处于八字节有符号整数的范围之内。

INCRBYFLOAT key increment

对 key 值对应的 value 进行 +increment 操作,并返回 +increment 后的值。increment 可以为负数。与上面命令不同的是,value 和 increment 可以为浮点数。

字符串命令
APPEND key value

在 key 所对应的 value 值后面插入 value ,并返回插入 value 后的字符串的长度(字节数)。当 key 值所对应的键值对不存在时,直接将所要插入的 value 当做键值对的 value 进行创建,相当于set 命令。

GETRANGE key start end

获取 key 所对应的 value 字符串中从下标 start 到 end(包括 end )的数据。字符串的下标从0开始。同时,下标可以为负数,表示倒数第n个字节。而当 start 和 end 超出 key 所对应的 value 长度时也不会出现报错,而是忽略超出的部分。

还要注意的是,当我们在 value 中存储中文的情况下,由于redis默认的编码方式将中文表示为三个字节,而 getrange 命令也是通过字节来获取字符串,因此若是我们将中文字符的三个字节拆开,就会出现问题。

SETRANGE key offset value

在 key 所对应的 value 值的 offset 开始覆盖新的 value,并返回被覆盖后的 value 的长度。

当 offset 超出原本 value 的长度或是不存在 key 所对应的键值对,那么在 value 后面直至 offset 的位置插入 \x00 字节。

同样,当 value 中存在中文时,也需要注意中文所占三个字节的问题。

STRLEN key

返回 key 所对应 value 的字节长度。

编码方式

  • int:8个字节的⻓整型。
  •  embstr:⼩于等于44个字节的字符串(字节数取决于版本,redis 5 为44字节)。
  •  raw:⼤于44个字节的字符串。

浮点数也是作为字符串的编码方式,因此当我们在使用浮点数的计数命令时,需要先将字符串转换为浮点数,在运算之后若是依然为浮点数,那么就再转换为字符串。

object encoding key

我们可以通过该命令来查看 key 值对应的 value 的编码方式

优化

预分配

Redis 采用SDS结构体来存储字符串

len 表示已用字节数,free 表示未用字节数,buf 数组存储数据

为了减少内存再分配的次数,Redis 采用预分配机制。

当首次创建 string 类型时(包含set命令处理已存在键值对时替换 value ),将 free 设为0。

而当 string 类型已经存在并作修改时(例如 append、setrange 等命令),若是 free+len 的长度不足以存储修改后的字符串,若是数据小于1MB,则预分配一倍的容量(即 len 与 free 都为修改后的字符串的字节长度);否则预分配1MB。

而若是修改后比原本的字符串长度短,会触发惰性删除机制,将富余的空间作为预分配空间保留。

预分配机制虽然会减少内存分配次数,但是或多或少都会造成内存浪费,尤其是在超出预分配空间后再次预分配的大量空间,因此,我们应该尽量避免去修改 string 。

共享内存池

除开利用SDS结构体的机制来进行优化,Redis 还通过共享对象池来进行字符串的内存优化。

Redis 在内部维护了一个[0, 9999]的整数对象池,当字符串存储该范围内的整数时,可以直接引用整数对象池中的数据,并使该对象的引用次数(refcount)自增1(初始为1)。

而只存在整数对象池是因为整数的使用次数较高,且整数比较的时间复杂度为O(1),而例如字符串(Redis中浮点数也是通过字符串来存储)的数据类型比较的时间复杂度为O(N),而 hash、list 等比较的时间复杂度为O(N*N)。

在开启 maxmemory 和 LRU淘汰策略后该整数对象池无效。

2. hash

hash和redis类似,都是使用键值对来存储数据,因此,为了区分它们的键值对,将hash的键值对命名为field-value。而 field-value 中的 value 类型为 string。

命令

HSET key field value [field value ...]

当 key 所对应的键值对不存在时,hset 通过 key 新建键值对并设置对应的 hash 中的键值对,并返回 hash 中的键值对数目。而当 key 所对应的键值对存在时,则将新的 field-value 键值对增添到 key 所对应的 hash 中,而同名的 field 会进行替换。

HSETNX key field value

虽然hset命令没有[NX|XX]选项,但可以用 hsetnx 命令来表示当 key 所对应的 hash 存在时不做处理 。但hsetnx命令只能设置存在一个键值对的hash。

HGET key field

获取 key 值所对应的 hash 中 field 所对应的 value。

HMGET key field [field ...]

批量获取。

HEXISTS key field

返回1表示存在,返回0表示不存在。

HDEL key field [field ...]

返回所删除的键值对的个数

HKEYS key
HVALS key
HGETALL key

分别表示获取 key 所对应的 hash 中的 field、value、field 和 value。而 hgetall 命令所获取的filed-value交替打印。

HLEN key

获取 key 所对应的 hash 中的键值对的个数。

HINCRBY key field increment
HINCRBYFLOAT key field increment

对key 值所对应的 hash 中 field 所对应的 value(整数或浮点数进行加减操作)。

编码方式

  • ziplist :当哈希类型元素个数⼩于 hash-max-ziplist-entries 配置(默认512字节)、同时所有 value 都⼩于 hash-max-ziplist-value 配置(默认64字节)时,Redis会使⽤ziplist作为哈希的内部实现。

zlbytes 表示该结构体的字节长度,4字节

zlrail  表示距离尾节点(zlend)的偏移量,4字节

rllen 表示压缩链表的节点数量(entry),2字节

zlend 表示尾节点,1字节

prev_entry_bytes_length 表示前一个节点所占用的空间,用于逆向遍历

encoding 表示前两位表示该节点数据(字符串/整数)的编码类型,其他位表示数据长度

contents 实际数据

ziplist 可以模拟双向链表的操作,但与之不同的是,ziplist 是连续的,可以提高遍历速度,减少内存碎片。但也正因如此,当我们进行新增删除操作的时候,需要对整块 ziplist 空间进行重新分配

  •  hashtable :除去ziplist,则使用 hashtable 作为哈希的内部实现。

3. list

类似于C++中的deque,可以进行时间复杂度为O(1)的头插、尾插、头删、尾删。同样,redis的list也支持下标为负数,表示倒数第n个元素。同时,list 中的元素是有序的(顺序不同, list 不同)。

命令

LPUSH key element [element ...]
LPUSHX key element [element ...]

RPUSH key element [element ...]
RPUSHX key element [element ...]

将 element 按照顺序头插 / 尾插到 key 对应的 list 中(当键值对不存在时,新建list后头插 / 不做处理),并返回插入之后 list 的长度,时间复杂度O(1)。

LPOP key
BLPOP key [key ...] timeout

RPOP key
BRPOP key [key ...] timeout

头、尾删,并返回删除掉的值。B 代表阻塞删除,当 list 中不存在数据的时候,进行阻塞等待(存在最大阻塞时间 timeout ),直到 list 中插入数据,并返回所删除的数据所处于的 list 所对应的key、被删除的数据以及阻塞等待的时间,时间复杂度O(1)。

由于 Redis 是单线程模型,命令具有原子性,因此,当在 list 中同时插入多个数据时,阻塞删除的是头部 / 尾部的数据,即(最后一个被头插 / 第一个被尾插 )/ (最后一个被尾插 / 第一个被头插)的数据。

若是多个客户端同时阻塞删除一个空的 list ,当插入数据时,首先处理第一个进行阻塞删除的命令。

若是同时阻塞删除多个 list ,只要有一个 list 中存在数据,便进行删除、返回删除的数据并结束阻塞删除。

LRANGE key start stop

获取 start 到 stop 之间(闭区间)的元素。同样会忽略非法下标的数据。时间复杂度O(N) 。

LINDEX key index

获取 index 位置的元素。时间复杂度O(N) 。

LINSERT key <BEFORE | AFTER> pivot element

在 pivot 位置前 / 后插入元素 element 并返回插入后的元素个数。与字符串命令不同的是,如果 pivot 为非法下标,会直接返回 -1 。

LREM key count value

删除 list 中的数据为 value 的元素,并返回删除的个数。

当 count>0 ,从左向右删除 count 个 value ,count<0,从右向左,count=0,全部删除。

LTRIM key start end

 保留 list 中从 start 到 end 的数据。

LSET key index newvalue

将 list 中下标为 index 的数据替换为 newvalue。同样,当 index 为非法的下标时报错。

编码方式

在 redis 3.2 之前,list 的编码方式分为 ziplist 和 linkedlist(链表),而在 redis 3.2 ,redis 将 list 的编码方式更改为 quicklist, quicklist 结合了 ziplist 和 linkedlist ,类似于deque,最外层为linkedlist, 而 linkedlist 中饭存储的数据为 ziplist ,ziplist 中存储的数据才是我们所真实存储的数据。通过这种方式,能够很好的结合两种编码方式的优点。

4. set

无序的元素集合,因此不能存在重复的元素,也不能通过下标来访问内部元素

命令

SADD key member [member ...]

添加 member 入 key 所对应的 set 中,并返回添加成功的个数。

SMEMBERS key

获取 set 中的所有数据。

SISMEMBER key member

查询 key 所对应的 set 中是否存在 member,存在返回1,不存在 member 或是 key 返回0。

SRANDMEMBER key member [member ...]

随机从 key 所对应的 set 中获取 count 个(当不带count选项时为1个)数据。

SCARD key

返回 key 所对应 set 中的元素个数

SPOP key [count]

随机删除 count 个 set 中的数据,并返回所删除的数据。

SREM key member [member ...]

删除 member ,并返回删除成功的个数。

SMOVE source destination member

将 member 元素从 source 对应的 set 中移动到 destination 对应的 set(当destination所对应的键值对不存在时,创建一个空的set),成功(即使 destination 对应的 set 中存在member也会视为成功,只是不做处理)返回1,失败(当 source 对应的 set 中不存在 member)返回0。

SINTER key [key ...]
SINTERSTORE destination key [key ...]

SUNION key [key ...]
SUNIONSTORE destination key [key ...]

SDIFF key [key ...]
SDIFFSTORE destination key [key ...]

分别从多个 key 中取交集 / 并集 / 差集 ,将结果返回 / 存储到第一个 key 中(第一个 key 不做集合运算)并返回结果中的元素个数。

编码方式

  • intset(整数集合):当集合中的元素都是整数并且元素的个数⼩于 set-max-intset-entries 配置(默认512个)时,Redis 会选⽤ intset 来作为集合的内部实现。

encoding 表示整数的类型,根据集合内字节数最长的整数来确定,分为int-16、int-32、int-64(当set 中整数发生增添时,如果最长的整数超出该整数类型,则进行“升级”,并不予回退)

length 表示集合中的元素个数

contents 表示升序整数数组

由于 intset 内部数据以升序数组存储,因此例如查找(O(logn) )等命令的时间复杂度会大大降低

  • hashtable(哈希表):当集合类型⽆法满⾜ intset 的条件时,Redis 会使⽤ hashtable 作为集合的内部实现
     

5. zset

不能存在重复数据(member)的有序集合,而其中是顺序是由 member 对应的另一个数据 sorce(整型或浮点型) 决定。虽然 member 不能重复,但 sorce 可以重复。

命令

ZADD key [NX | XX] [GT | LT] [CH] [INCR] score member [score member ...]

ZINCRBY key increment member

在 key 所对应的 zset 中(批量)插入 score、member 数据对,并返回新插入成功的数量。若是 member 存在,则更新对应的 score。

NX | XX 选项依旧代表存在时不做处理 / 不存在时不做处理。

GT | LT 选项表示若是 member 存在,仅在新 score 比原 score 小 / 大的时候进行修改。

CH 选项表示返回插入成功或修改成功的数量。

INCR 选项表示将 member 对应的 score 加上新的 score 值(无法进行批量操作),并返回修改后的 score 值。当 member 不存在时,创建并将 score 直接设置为新的 score 值。

ZCARD key

获取元素个数

ZCOUNT key min max

获取 score 在 min 和 max 之间的个数(闭区间,在 min 或 max 前加  '(' ,表示排除 min 或 max)

ZRANGE key start stop [WITHSCORES]
ZREVRANGE key start stop [WITHSCORES]

ZRANGEBYSCORE key min max [WITHSCORES]
ZREVRANGEBYSCORE key max min [WITHSCORES]

按升序 / 降序返回下标在 start 和 stop 之间的 member /

按升序 / 降序返回 score 在 min 到 max 之间的 member。

WITHSCORES 选项表示一并 score。

ZPOPMAX key [count]
BZPOPMAX key [key ...] timeout

ZPOPMIN key [count]
BZPOPMIN key [key ...] timeout

(阻塞)删除(前 count 个)最大 / 小的数据并返回。

ZRANK key member
ZREVRANK key member

获取 member 的升序 / 降序排名(从 0 开始)。

ZSCORE key member

获取 member 对应的 score。

ZREM key member [member ...]

批量删除并返回删除成功的个数。

ZREMRANGEBYRANK key start stop

将升序排名在 start 到 stop 之间的数据删除。

ZREMRANGEBYSCORE key min max

将 score 在 min 和 max 之间的数据删除。

ZINTERSTORE destination numkeys key [key ...] 
[WEIGHTS weight [weight ...] ] [AGGREGATE <SUM | MIN | MAX>]

ZUNIONSTORE destination numkeys key [key ...] 
[WEIGHTS weight [weight ...] ] [AGGREGATE <SUM | MIN | MAX>]

将 numkeys 个 key 对应的 zset 中的 member 求交集 / 并集,并将结果存储在 destination 对应的 zset 中(将原本 destination 中的数据删除),并返回结果的元素个数。

AGGREGATE 选项表示交集 / 并集中对 score 的处理方式,分别为求和、最小值、最大值,默认为 SUM。

WEIGHTS 选项代表每个 zset 的权重,在进行 AGGREGATE 选项的处理之前将 zset 的每个 score 与权重相乘,默认为1。

编码方式

  • ziplist(压缩列表):当有序集合的元素个数⼩于 zset-max-ziplist-entries 配置(默认 128 个),同时每个元素的值都⼩于 zset-max-ziplist-value 配置(默认 64 字节)时,Redis 会⽤ ziplist 来作为有序集合的内部实现,ziplist 可以有效减少内存的使⽤。
  • • skiplist(跳表):当 ziplist 条件不满⾜时,有序集合会使⽤ skiplist 作为内部实现,因为此时 ziplist的操作效率会下降。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

finish_speech

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

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

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

打赏作者

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

抵扣说明:

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

余额充值