挖一挖 Redis 所有数据类型的底层数据结构

  1. 字符串(String)

    1. 特点:字符串是最简单的数据类型,在Redis中存储的是一个字符串值。可以存储文本、数字等数据,并支持对字符串的基本操作,如设置、获取、删除等,它是一个二进制安全的字符串,即字符串的内容不限于文本,可以包含任何数据,如文本、数字、二进制数据等。Redis 中的字符串可以存储最大512MB的数据。且非常简单高效,字符串的读写操作非常快速,是 Redis 中性能最高的数据类型之一
    2. 常见操作:
      • SET:设置指定key的值为value
      • GET:获取指定key的值。
      • GETSET:获取指定key的值,并且将该key的值设置为value
      • MGET:获取所有给定key的值
      • MSET:设置多个key的值
      • MSETNX:同时设置一个或多个key-value对,当且仅当所有给定key都不存在
      • SETNX:设置一个key的值为value,如果这个key不存在的话
      • SETRANGE:设置一个key对应的字符串值,从偏移量offset开始
      • STRLEN:返回一个key对应的字符串值的长度
      • GETRANGE:获取一个key对应的字符串值的一部分
      • SETEX:将值value关联到key,并将key的过期时间设为seconds(以秒为单位)
      • PSETEX:这个命令和SETEX命令相似,但它以毫秒为单位设置key的生存时间
      • TTL:返回一个key的剩余生存时间,以秒为单位
      • PTTL:返回一个key的剩余生存时间,以毫秒为单位
      • SETBIT:对key所储存的字符串值,设置或清除指定偏移量上的位(bit)
      • GETBIT:对key所储存的字符串值,获取指定偏移量上的位(bit)
      • INCR:将key中储存的数字值增一
      • INCRBY:将key所储存的值加上给定的增量值
      • DECR:将key中储存的数字值减一
      • DECRBY:将key所储存的值减去给定的减量值
      • APPEND:在指定的key后面追加字符串value
      • DEL:删除指定的key
      • GETRANGE:返回key中字符串值的子字符
      • SETRANGE:用value参数覆写给定key所储存的字符串值,从偏移量offset开始
      • STRLEN:返回key所储存的字符串值的长度
    3. 使用场景:
      • 缓存:作为缓存存储常见的使用场景之一。将经常访问的数据存储在 Redis 字符串中,可以提高数据的访问速度和性能。例如,存储数据库查询结果、API 调用结果等。
      • 计数器:用于记录计数信息,如网站访问量、文章阅读量、点赞数等。通过 INCR 和 DECR 等命令实现对计数的自增和自减操作,简单高效。
      • 会话管理:存储用户会话信息,如用户登录状态、用户访问信息等。可以将用户会话信息存储在 Redis 字符串中,并设置过期时间,实现会话的管理和过期清理。
      • 分布式锁:利用 Redis 字符串的 SETNX(SET if Not eXists)命令实现分布式锁。通过在 Redis 中设置一个唯一的键作为锁,可以确保在分布式环境下对共享资源的安全访问。
      • 配置信息存储:用于存储应用程序的配置信息,如数据库连接信息、缓存配置信息等。通过将配置信息存储在 Redis 字符串中,可以提高配置信息的读取速度和可管理性。
      • 验证码存储:用于存储验证码等临时性数据。通过设置过期时间,可以自动清理过期的验证码,保证数据的有效性和安全性。
      • 防止重复提交:Redis的字符串类型可以用来防止用户重复提交表单。例如,你可以在用户提交表单时,在Redis中设置一个标记,并在提交处理完成后删除这个标记。
      • 限制API在某一时段的访问次数:Redis的字符串类型可以用来限制API在某一时间段内的访问次数,例如,你可以使用INCR命令来增加API访问次数的计数器,并在达到限制次数后返回错误信息
    4. 底层的数据结构:
      • 如果Redis存储的字符串是一个字符串值,且长度大于44字节,那么底层数据结构是SDS(Simple Dynamic Strings),这是一种简单动态字符串数据结构。SDS的设计是为了解决C语言标准字符串的一些限制,例如无法直接获取字符串长度、容易发生缓冲区溢出等问题。SDS的结构主要包括三个部分:
        • len:记录buf数组中已使用字节的数量,等于SDS所保存的字符串的长度。

        • alloc:记录buf数组中未使用字节的数量。

        • buf[]:字节数组,用于保存字符串

      • 如果Redis存储的字符串是一个字符串值,且长度小于等于44字节,那么字符串对象将使用embstr编码方式来保存。

        • embstr编码是一种优化策略,它在内存分配时只需要一次,因为Redis Object和SDS(Simple Dynamic String)是连续的内存空间。相比之下,对于大于44字的字符串,Redis会使用raw编码,这种编码方式需要两次内存分配,因为Redis Object和SDS是分开的内存空间

        • 总的来说,embstr编码适用于存储较短的字符串,因为它可以减少内存分配的次数,从而提高效率。而对于较长的字符串,Redis则使用raw编码。这两种编码方式的切换点就是字符串的长度是否超过44字节。

      • 如果Redis存储的字符串是一个整数值,且可以用long类型来表示,那么它的底层存储会使用整数集合(IntSet)数据结构,存储 8 个字节的长整型。整数集合是Redis为了更有效地存储整数而专门设计的一种数据结构,它能够节省空间并提高处理速度。整数集合主要由以下部分组成:
        • encoding:表示整数集合中整数的类型和编码方式,可能值为INTSET_ENC_INT16INTSET_ENC_INT32INTSET_ENC_INT64,分别对应于16位、32位和64位的整数类型
        • length:表示整数集合中整数的数量
        • contents[]:一个数组,用于存储整数值
      • 整数集合的每个元素都是contents数组的一个数组项,这些项在数组中按值的顺序排列,且数组中不包含任何重复项。整数集合的每个元素都是contents数组的一个数组项,这些项在数组中按值的顺序排列,且数组中不包含任何重复项
      • 整数集合的优势在于,它能够以更紧凑的方式存储整数值,减少了内存的使用,并且在某些操作中比普通字符串更高效。例如,当你需要存储一系列整数值时,使用整数集合会比使用普通的字符串更节省空间,也更快
    5. 为什么选用这两种数据结构:
      • 快速获取字符串长度:SDS在结构体头部显式记录了字符串的长度,因此可以直接通过读取这个字段来获取字符串长度,而不需要像C语言传统字符串那样遍历整个字符串直到遇到结束符,从而将时间复杂度从O(n)降低到O(1)
      • 防止缓冲区溢出:SDS的空间分配策略可以有效避免缓冲区溢出的情况。在进行字符串修改操作时,SDS会首先检查是否有足够的空间,如果不足则自动进行扩展,从而避免了因空间不足而造成的缓冲区溢出
      • 减少内存重分配次数:C语言的字符串在长度变化时,往往需要进行内存的重分配操作,这不仅增加了操作的复杂度,也可能引发性能问题。相比之下,SDS在缩短字符串时,只会减少len字段的值,而不会立即回收内存,这样可以在未来的扩展操作中重复利用这部分空间,减少了内存重分配的次数
      • 二进制安全:C语言的字符串是以空字符结束,不支持包含内部空字符的二进制数据。而SDS支持任意二进制数据存储,因为它不是基于空字符来判断字符串的结束,而是根据长度字段来确定字符串内容
      • 兼容性:SDS 的设计考虑了与 C 语言标准字符串的兼容性,可以方便地与现有的 C 语言代码进行集成和交互。这使得 Redis 在与其他系统的集成和交互方面更加方便和灵活。
  2. 哈希表

    1. 特点:Redis 的哈希表(Hash)是一种键值对的数据结构,类似于字典(dictionary)或关联数组,在内部实现上使用了哈希表来存储键值对,并且提供了快速的读写操作,哈希表的字段名是唯一的,值可以重复,哈希表的字段数量可以动态增加或减少,没有固定大小限制。
      1. 开放寻址法: Redis的哈希表采用开放寻址法来实现,这意味着可以通过计算键的哈希值直接定位到存储位置,从而避免了链表遍历的开销,使得读写操作非常快
      2. 动态调整: 当哈希表的使用率达到一定的阈值时,Redis会启动rehash操作,创建一个新的更大的哈希表,并将原哈希表中的所有数据重新映射到新哈希表中。这个过程是渐进的,以避免一次性大量操作导致的性能问题
      3. 内存管理: Redis的哈希表还支持惰性删除和过期时间的机制,对于过期的键值对,会在后台进行清理,释放内存
      4. 丰富的数据类型: Redis的哈希表不仅可以存储简单的字符串键值对,还可以存储更复杂的结构,还支持整数、浮点数、列表、集合等多种数据类型
      5. 高效的键值对存储: 由于Redis的哈希表是基于内存实现的,因此相比于磁盘存储的数据库系统,Redis的哈希表可以提供更高的读写速度和更好的响应时间
    2. 常见操作:
      1. HSET: 用于创建一个新的哈希表或者向已存在的哈希表中添加新的键值对,HSET myhash field1 "value1" field2 "value2", 这将创建一个名为myhash的新哈希表,并添加两个字段field1field2,它们的值分别为"value1""value2"
      2. HGET: 用于获取哈希表中某个字段的值,HGET myhash field1, 这将返回myhash哈希表中field1字段的值
      3. HGETall: 用于获取哈希表中的所有字段和它们的值,HGETALL myhash,这将返回myhash哈希表中的所有字段和它们对应的值
      4. HDEL: 用于删除哈希表中的一个或多个字段,HDEL myhash field1 field2,这将删除myhash哈希表中的field1field2这两个字段
      5. HEXISTS: 用于检查哈希表中某个字段是否存在,HEXISTS myhash field1,这将检查myhash哈希表中是否存在field1这个字段
      6. HINCRBY: 用于给哈希表中的某个字段的整数值增加指定的增量,HINCRBY myhash counter 1,这将使myhash哈希表中的counter字段的值增加1
      7. HKEYS: 用于获取哈希表中的所有字段名。HKEYS myhash,这将返回myhash哈希表中的所有字段名
      8. HLEN: 用于获取哈希表中字段的数量,HLEN myhash,这将返回myhash哈希表中的字段数量。
      9. HMGET: 用于获取哈希表中所有给定字段的值,这将返回myhash哈希表中field1field2这两个字段的值
      10. HMSET: 用于同时设置多个字段和它们的值,HMSET myhash field1 "value1" field2 "value2",这将设置myhash哈希表中的field1field2字段的值为"value1""value2"
      11. HSCAN: 用于迭代哈希表中的键值对,HSCAN myhash 0 MATCH field* COUNT 1,这将迭代myhash哈希表中匹配模式field*的字段,并限制每次迭代返回的最大元素数量为1。
      12. HVALS: 用于获取哈希表中所有的值,HVALS myhash,这将返回myhash哈希表中的所有值。
    3. 使用场景:
      1. 缓存数据:哈希表适用于缓存一些结构化的数据,如用户信息、配置信息等。通过将数据存储在哈希表中,可以提高读写效率,并且减少内存占用。
      2. 存储配置信息:哈希表可以用来存储系统配置信息,每个配置项对应一个哈希表键,配置项的名称和值对应哈希表的字段和字段值
      3. 存储计数器:哈希表可以用来存储计数器,每个计数器对应一个哈希表键,计数器的名称和值对应哈希表的字段和字段值,可以实现对各种计数操作的支持
      4. 存储消息属性:哈希表可以用来存储消息的属性信息,每个消息对应一个哈希表键,消息的各种属性(如发送者、接收者、发送时间等)对应哈希表的字段和字段值
      5. 存储用户权限信息:哈希表可以用来存储用户权限信息,每个用户对应一个哈希表键,用户的权限和角色对应哈希表的字段和字段值,可以实现对用户权限的管理和控制
    4. 底层数据结构:Redis的哈希表(Hash)的底层数据结构有两种:
      1. 压缩列表(ZipList):压缩列表是一种特殊的内存分配器,它用于存储一系列的键值对。当哈希表中的键值对数量少于一定阈值且单个键值对的大小不超过一定字节时,Redis会使用压缩列表作为底层数据结构。具体来说,当以下两个条件满足时,Redis会使用压缩列表:
        • 哈希表中的键值对数量小于512个
        • 单个键值对的键和值的字符串长度都小于64字节
      2. 哈希表(Hashtable):如果哈希表中的键值对数量超过512个或者单个键值对的键或值的长度超过64字节,Redis则会使用哈希表作为底层数据结构。哈希表通过哈希函数将键映射到特定的槽位,然后在对应的槽位上维护一个链表来解决哈希冲突的问题。这样,即使哈希表的负载因子较高,也能保持较好的查找效率。
    5. 为什么选用这两种数据结构?
      1. 首先说下压缩列表:
        1. Redis的压缩列表(ZipList)是为了节省内存而设计的一种特殊的数据结构,它可以包含任意数量的元素,每个元素可以是字节数组或整数。压缩列表通常用于存储列表和哈希表等数据结构的底层实现。以下是压缩列表的一些关键特性
        2. 压缩列表结构:压缩列表由一系列特殊编码的连续内存块组成,这些内存块被称为节点(entry)。每个节点可以保存一个字节数组或整数值。压缩列表的整体布局如下:
          1. zlbytes: 表示整个压缩列表占用的内存字节数,占用4个字节
          2. zltail: 表示压缩列表尾节点相对压缩列表起始地址的偏移量,占用4个字节
          3. zllen: 表示压缩列表包含的节点数量,占用2个字节
          4. entries: 表示压缩列表的节点列表,每个节点紧跟着下一个节点
          5. zlend: 表示压缩列表的结束,占用1个字节,其值为0xFF
        3. 具体的每个节点(entry)的组成:每个压缩列表的节点由三部分组成:prevlenencoding 和 content
          1. prevlen: 表示该节点前一个节点的字节长度。如果前一个节点的长度小于254个字节,prevlen占用1个字节;否则占用5个字节,其中第一个字节被设置为0xFE,随后的四个字节保存前一个节点的字节长度
          2. encoding: 表示节点的数据类型和长度
          3. content: 表示节点的实际数据内容
        4. 压缩列表的使用场景:压缩列表适用于存储较少的键值对,特别是当键和值都是短字符串或整数时。这种结构可以有效地节省内存,因为它通过紧凑地存储节点来减少内存的开销。然而,压缩列表并不适合存储大量的数据,因为它的节点大小不是固定的,而且在插入和删除节点时可能需要进行复杂的内存调整
        5. 压缩列表的限制:不支持快速的节点插入和删除操作,因为这些操作可能需要对后续的节点进行移动,这会导致频繁的内存复制操作,从而影响性能。此外,压缩列表的最大节点数量和单个节点的最大大小也是有限的,因此在存储大量数据时可能需要考虑使用其他数据结构
      2. 其次是哈希表:Redis的哈希表通过哈希函数将键映射到特定的槽位,并在对应的槽位上维护一个链表来解决哈希冲突的问题。这样,即使哈希表的负载因子较高,也能保持较好的查找效率。Redis的哈希表并不是传统意义上的Java或C++中的Hashtable类,而是一种专门为Redis设计的内部数据结构,用于存储和管理键值对,它主要有以下几个部分组成:
        1. dictht: 表示一个哈希表,包含一个指向 dictEntry 的指针数组table,用于存储所有dictEntry节点,size属性表示数组的大小,sizemask属性用于计算哈希值。used属性表示已使用的空间数量。
        2. dictEntry: 这是一个结构体,用于存储键值对,表示哈希表的一项,可以看作就是一个键值对,包含一个指向键的指针key,一个联合体union,用于存储不同类型的值,如指针val、无符号64位整型u64、有符号64位整型s64。每个 dictEntry包含一个键和一个值。键和值可以是任何类型,包括整数、字符串、列表等。dictEntry还包含一个指向下一个dictEntry的指针,以形成链表,用于处理哈希冲突。
        3. dict: Redis给外层调用的哈希表结构,它包含了dictht类型的两个实例,分别用于存储旧的和新的哈希表,以便实现渐进式重新哈希(incremental rehashing)。dict还包含了dictType类型的实例,用于存储关于哈希函数和其他操作的回调函数的信息
      3. 哈希表是如何解决哈希冲突?
        1. Redis的哈希表(Hash)是通过链表和开放寻址法来解决哈希冲突的。具体来说,每个哈希表槽位都是一个指针数组,指向一个链表,链表中存储了哈希表中所有被映射到该槽位的键值对。当出现冲突时,新的键值对将被插入到链表的头部,这样可以保证链表中的键值对顺序与插入顺序一致
        2. 此外,Redis还采用了渐进式rehash策略。在进行rehash操作时,Redis会保留新旧两个哈希表结构,查询时会同时查询这两个哈希表。Redis会将旧哈希表中的内容一点一点地迁移到新的哈希表中,当迁移完成后,就会用新的哈希表取代之前的哈希表。当哈希表移除了最后一个元素后,这个数据结构将会被删除
      4. 总结:Redis的哈希表(Hash)之所以同时支持压缩列表(ZipList)和哈希表(Hashtable)两种数据结构,主要是出于对内存使用效率和操作性能的考虑,通过选择最适合当前数据量的底层数据结构,实现了在不同场景下的高效性能和灵活性。这种设计使得Redis能够处理各种大小和复杂度的哈希表数据,同时保持内存的低消耗和操作的快速性。
  3. 列表(List)

    1. 特点:Redis 的列表(List)是一种基本的数据结构,它是一个有序、可重复的集合,内部采用双向链表实现。列表中的每个元素都可以是字符串类型,也可以是二进制数据,列表中的元素顺序由插入顺序决定,允许在列表的两端进行插入、删除和查找操作。
    2. 常见操作:
      1. lpush:向列表的最左侧(头部)插入一个或多个元素
      2. rpush:向列表的最右侧(尾部)插入一个或多个元素
      3. lpop:移除列表最左侧(头部)的元素,并返回这个元素
      4. rpop:移除列表最右侧(尾部)的元素,并返回这个元素
      5. lrange:获取列表中指定范围的元素
      6. lrem:移除列表中与给定值匹配的所有元素
      7. lset:通过索引设置列表中某个位置的元素
      8. ltrim:修剪列表,只保留指定索引范围内的元素
      9. llen:获取列表的长度
      10. blpop:移除并获取列表的第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
      11. brpop:移除并获取列表的最后一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
      12. brpoplpush:从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它;如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
    3. 使用场景:
      1. 消息队列:Redis列表可以模拟队列的行为,通过lpushrpop命令实现生产者消费者模式,适用于构建简单的消息队列系统
      2. 限流队列:Redis列表可以用来实现流量控制的队列,例如限制用户每分钟只能发送一定数量的请求,超过限制的请求会被放入列表中,等待其他请求处理完毕后再进行处理
      3. 缓存队列:Redis列表可以用来缓存需要排队处理的队列,例如缓存需要批量处理的用户请求,然后通过lpop命令按顺序取出队列中的元素进行处理
      4. 分页系统:Redis列表可以用来实现分页系统,例如社交媒体上的动态列表,通过lrange命令可以获取特定页面的动态数据
      5. 历史记录:可以将用户的浏览记录、搜索记录等存储在列表中,方便用户查看历史操作
    4. 底层数据结构:
      1. 双向链表(linked list):
        1. Redis 的列表在数据量较大或者需要频繁插入、删除操作时会使用双向链表作为底层数据结构。
        2. 双向链表是一种线性数据结构,每个节点包含一个值字段和两个指针字段,分别指向前一个节点和后一个节点。
        3. 双向链表支持在任意位置进行插入、删除操作,对于列表的头部和尾部插入、删除操作时间复杂度为 O(1)。
        4. Redis 的双向链表实现了一系列的 API,用于在列表的头部和尾部进行插入、删除、获取等操作
      2. 压缩列表(ziplist)
        1. Redis 的列表在数据量较小且元素大小固定的情况下会使用压缩列表作为底层数据结构。
        2. 压缩列表是一种紧凑的、节省内存的连续内存结构,用于存储小型列表。
        3. 压缩列表将多个节点合并为一个 entry,以节省内存空间,相邻相同类型的元素会被合并为一个 entry。
        4. 压缩列表中的元素可以是字符串或整数,每个 entry 包含一个长度字段和一个数据字段,不同类型的元素长度和数据结构不同。
        5. 压缩列表支持在列表的两端进行插入和删除操作,对于头部和尾部插入、删除操作的时间复杂度为 O(1)。
        6. 当列表的数量超过512个或者单个元素的大小超过64字节时,就会自动切换到双向链表
      3. Quicklist:
        1. 从Redis 3.2版本开始,Redis引入了一种新的数据结构——Quicklist,它是ziplist和linkedlist的结合体。Quicklist内部使用ziplist来维护节点的顺序,但外部表现为一个linkedlist的结构。
        2. 这样做的好处是,在列表较小的情况下,可以使用内存高效的ziplist,而在列表较大时,可以通过切换到linkedlist来保持较好的性能。此外,Quicklist还支持动态地在ziplist和linkedlist之间切换,以适应不同大小的列表
    5. 为什么选这样的数据结构?
      1. Redis列表的底层数据结构设计得十分巧妙,它结合了压缩列表和双端列表的特点,并通过Quicklist实现了更好的性能和内存利用率。这种设计使得Redis列表既能处理小型的列表,又能有效地管理大型列表,从而在实际应用中得到了广泛的使用。
  4. 集合(Set)

    1. 特点:Redis 的集合(Set)是一种无序、不重复的数据结构,内部实现采用哈希表结构,集合中的元素是唯一的,不存在重复元素。Redis 集合支持添加、删除、查找等常见操作,同时也支持集合间的交集、并集、差集等操作。
    2. 常见操作:
      1. SADD key member1 member2: 向集合添加一个或多个成员
      2. SCARD key: 获取集合的成员数
      3. SDIFF key1 key2: 返回第一个集合与其他集合之间的差异
      4. SDIFFSTORE destination key1 key2: 返回给定所有集合的差集并存储在destination中
      5. SINTER key1 key2: 返回给定所有集合的交集
      6. SINTERSTORE destination key1 [key2]: 返回给定所有集合的交集并存储在destination中
      7. SISMEMBER key member: 判断member元素是否是集合key的成员
      8. SMEMBERS key: 返回集合中的所有成员
      9. SMOVE source destination member: 将member元素从source集合移动到destination集合
      10. SPOP key: 移除并返回集合中的一个随机元素
      11. SRANDMEMBER key count: 返回集合中一个或多个随机元素
      12. SREM key member1 member2: 移除集合中一个或多个成员
      13. SUNION key1 key2: 返回所有给定集合的并集
      14. SUNIONSTORE destination key1 key2: 返回给定所有集合的并集并存储在destination中
    3. 使用场景:
      1. 缓存: 集合可以用来缓存用户请求中的关键字,以便于后续的快速检索和分析。例如,一个搜索引擎可能会缓存用户搜索的关键词集合,以减少数据库的查询压力
      2. 社交网络: 在社交网络应用中,集合可以用来存储用户的好友关系,从而实现诸如共同好友推荐、好友关系统计等功能。例如,Facebook可能会使用集合来存储用户的好友列表,并通过集合的交集、并集和差集操作来找到共同的好友
      3. 实时分析: 集合可以用于实现实时数据分析,如实时统计网站的独立访客数(UV)。由于集合中的元素是唯一的,因此可以有效地去除重复的访客记录,从而准确地统计出独立的访客数
      4. 限流和熔断: 集合也可以用于实现流量控制和故障恢复机制,如漏桶算法和令牌桶算法,这些都是常见的限流和熔断策略
    4. 底层数据结构:Redis的集合(Set)在底层使用了两种数据结构:整数集合(IntSet)和哈希表(Hashtable)。这两种数据结构的选择取决于集合中存储的元素类型和数量
      1. 整数集合(IntSet):当集合中的所有元素都是整数,并且元素的数量不超过512个时,Redis会使用整数集合作为底层数据结构。整数数组的查找、插入和删除操作的时间复杂度通常为 O(n),在元素数量较少时具有较高的效率,整数集合是基于数组实现的,它比标准的Java或C语言数组更加高效,因为它可以自动处理溢出的情况,并且在添加和删除元素时不需要重新分配整个数组。此外,整数集合还支持自动扩展和缩减,可以在不影响已有元素的情况下,动态调整存储空间的容量
      2. 哈希表(Hashtable):如果集合中包含非整数元素,或者元素的数量超过512个,Redis则会使用哈希表作为底层数据结构。哈希表提供了快速的查找、插入和删除操作,这对于大多数集合操作来说是非常高效的。此外,哈希表还可以处理集合中可能出现的重复元素,因为哈希表的设计就是为了避免重复元素的出现,哈希表可以保证元素的唯一性,并且在处理集合操作(如交集、并集、差集)时具有较好的性能
    5. 为什么选择这两种数据结构?
      1. 总的来说,Redis集合选择使用整数集合和哈希表这两种数据结构,是为了根据集合中元素的类型和数量,选择最合适的数据结构以达到最佳的性能和内存使用效率。在实际应用中,这种设计允许Redis根据实际情况灵活选择底层数据结构,从而优化整体性能。
  5. 有序集合(Sorted Set):

    1. 特点:Redis 的有序集合(Sorted Set)是一种有序、不重复的数据结构,它结合了传统集合和列表的特点,并引入了一个额外的权重机制,它与集合(Set)类似,但每个成员都关联着一个双精度浮点数分数(score),根据这个分数来进行排序。有序集合内部使用了跳跃表(Skip List)和哈希表(Hash Table)两种数据结构来实现。
    2. 常见操作:
      1. ZADD key score member [score member ...]: 向有序集合添加一个或多个成员及其分数
      2. ZREM key member [member ...]: 从有序集合中移除一个或多个成员
      3. ZCARD key: 返回有序集合中的成员总数
      4. ZSCORE key member: 返回有序集合中某个成员的分数
      5. ZRANK key member: 返回有序集合中某个成员的排名
      6. ZREVRANK key member: 返回有序集合中某个成员的逆序排名
      7. ZRANGE key start end [WITHSCORES]: 返回有序集合中指定区间的成员
      8. ZREVRANGE key start end [WITHSCORES]: 返回有序集合中指定区间的逆序成员
      9. ZCOUNT key min max: 计算有序集合中分数在指定区间的成员数量
      10. ZINCRBY key increment member: 对有序集合中某个成员的分数加上指定的增量
      11. ZUNIONSTORE destination numkeys key [key ...]: 计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合destination中
      12. ZLEXCOUNT key min max: 计算有序集合中字典序在指定区间的成员数量
      13. ZRANGEBYLEX key min max [LIMIT offset count]: 根据字典序范围返回有序集合中的成员
      14. ZREVRANGEBYLEX key max min [LIMIT offset count]: 根据字典序范围返回有序集合中的成员,从高分到低分排序
      15. ZREMRANGEBYRANK key start stop: 移除有序集合中指定排名范围内的所有成员
      16. ZREMRANGEBYSCORE key min max: 移除有序集合中分数在指定范围内的所有成员
    3. 使用场景:
      1. 排行榜:有序集合非常适合实现各种排行榜系统,如游戏排行榜、网站访问量排行榜等。可以通过分数来决定元素的位置,从而轻松实现排名功能
      2. 优先级队列:在需要优先级排序的场景中,有序集合可以作为一个有效的解决方案,例如任务调度、邮件队列等。分数越高,元素在队列中的优先级就越高
      3. 计数器:有序集合也可以用于实现计数器,特别是当计数器的值需要按某种顺序排列时。例如,统计用户的活跃度、商品的热销程度等
      4. 实时分析:有序集合可以用来存储和检索具有特定分数的数据,这对于实时数据分析非常有用,比如实时监控系统的警报级别排序
      5. 地理空间数据:Redis从版本6.0开始支持了GEO空间数据类型,它可以利用有序集合来实现地理位置数据的存储和查询,如地图服务中的地点搜索、路径规划等
      6. 推荐系统:在推荐系统中,可以使用有序集合来存储物品的流行度和用户偏好等信息,然后根据这些信息来进行个性化推荐
      7. 搜索建议:用于存储搜索关键词信息,并根据搜索频率进行排名,实现搜索建议功能。
      8. 投票系统:用于存储投票信息,例如投票参与者、已投票用户等,并根据分数进行排名
    4. 底层数据结构:Redis有序集合(Sorted Set)是一种特殊的数据结构,它结合了列表和哈希表的特点,并在此基础上增加了排序功能。每个元素不仅存储一个值,还附带一个分数(score),根据分数的高低进行排序,因此得名“有序集合”,在内部实现上,Redis有序集合使用了两种数据结构:压缩列表(ziplist)和跳跃表(skiplist)。压缩列表主要用于存储元素和它们的分数,而跳跃表则用于实现高效的排序和查找操作
      1. 压缩列表(ziplist):压缩列表是一种特殊的数据结构,它类似于数组,但更加紧凑和高效。每个元素都是一个节点,包含了前一个节点的指针、自身的值和分数,以及后一个节点的指针。这种结构使得压缩列表可以在O(1)的时间复杂度内完成添加和删除操作
      2. 跳跃表(skiplist):跳跃表是一种层次化的数据结构,它由一系列节点组成,每个节点都有一个指向同一层的下一个节点的指针。这种结构使得跳跃表可以在O(log n)的时间复杂度内完成查找、插入和删除操作。跳跃表的每一层都是一个有序列表,并且更高层的列表有更多的节点。这样,当进行查找操作时,可以从最高层开始,逐层向下查找,直到找到目标节点,从而提高了查找效率,在Redis中,每个跳跃表节点的层数都是1至32之间的随机数。在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序Redis的跳跃表由以下两部分组成:
        1. zskiplist用于保存跳跃表的信息,如表头节点、表尾节点、长度等
          1. header:指向跳跃表的表头节点。
          2. tail:指向跳跃表的表尾节点。
          3. level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)。
          4. length:记录跳跃表的长度,也就是跳跃表目前包含节点的数量(表头节点不计算在内)
          5. header:指向跳跃表的表头节点。
        2. zskiplistNode:用于表示跳跃表节点
          1. lazy_update:表示是否需要延迟更新。
          2. score:节点的分数
          3. obj:节点的对象。
          4. level:节点的层数,每个节点的层数都是1至32之间的随机数1
          5. lazy_update:表示是否需要延迟更新。
    5. 为什么选择这两种数据结构?
      1. 当有序集合的元素较少或者元素的值较小(如小于或等于44字节)时,Redis会使用压缩列表来存储这些元素。压缩列表是一种特殊的数据结构,它可以减少内存的使用,并且由于元素少,可以直接在内存中进行快速操作。
      2. 当有序集合的元素较多或者元素的值较大(如大于44字节)时,Redis会使用跳跃表来存储这些元素。跳跃表是一种概率化的数据结构,它可以在O(logn)的时间内完成查找、插入和删除操作,这使得它在处理大量数据时具有较高的效率。
      3. 在实际应用中,Redis通常会将压缩列表和跳跃表结合起来使用。这是因为压缩列表适合存储小量的数据,而跳跃表适合存储大量的数据。通过这种方式,Redis可以在保持内存占用较低的同时,也能保证在大数据量情况下的操作效率。
      4. Redis的设计目标是提供高性能的内存数据结构,因此它会根据数据的实际情况选择最合适的数据结构来进行存储。压缩列表和跳跃表的结合使用,正是为了满足不同数据量级下的性能需求,同时也保证了内存使用的有效性和经济性。
  6. Bitmaps(位图)

    1. 特点:Redis的Bitmaps(位图)并不是一个独立的数据类型,而是基于String(字符串)类型之上的一种特殊操作,可以将字符串类型的数据看作是一个位向量。这种操作方式使得Redis可以对字符串中的每一个位进行精确的控制和操作。位图是一种非常紧凑的数据存储方式,它通过使用位(bit)为单位来存储数据。在计算机科学中,一个字节等于8位,所以一个字节可以存储8个布尔值。如果一个数据只需要存储0或1的状态,那么就可以使用位图来存储。位图的一个显著优势是节省空间,因为它只存储必要的位信息,而不需要额外的空间来存储其他不需要的信息
    2. 常见操作:
      1. SETBIT: 设置或清除位图中指定偏移位置的位的值。此命令接受三个参数:要操作的键名、偏移位置和要设置的值。例如,SETBIT key offset value,其中key是要操作的键名,offset是偏移位置,value是要设置的值,只能是0或1
      2. GETBIT: 获取位图中指定偏移位置的位的值。此命令接受两个参数:要操作的键名和偏移位置。例如,GETBIT key offset,其中key是要操作的键名,offset是偏移位置
      3. BITCOUNT: 统计位图中值为1的位的数量。此命令可以接受三个参数:要操作的键名、开始偏移位置和结束偏移位置。例如,BITCOUNT key [start] [end],其中key是要操作的键名,startend是可选的偏移位置
      4. BITOP: 对两个不同字串进行位运算。可进行的运算有AND(与)、OR(或)、XOR(异或)和NOT(非)。例如,BITOP AND destkey key[key…],其中destkey是目标键名,key是要操作的其他键名
      5. BITPOS: 查找位图中第一个值为0或1的比特位的位置。此命令接受三个参数:要操作的键名、目标位值(0或1)和可选的开始偏移位置和结束偏移位置。例如,BITPOS key targetBit [start][end],其中key是要操作的键名,targetBit是目标位值,startend是可选的偏移位置
      6. 在使用位图时,需要注意的一点是,如果设置的偏移位置超出了当前字符串的长度,Redis会自动增加字符串的长度。此外,位图的删除操作并不支持原子性,因此在设计系统时需要注意这个问题
    3. 使用场景:
      1. 用户签到: 位图可以用来跟踪用户的签到行为。例如,可以为每个用户创建一个位图,其中每一位代表一天是否签到。这样,你可以快速地检查用户是否在某一天签到,或者计算出用户连续签到的天数
      2. 活跃用户统计: 位图可以用来统计活跃用户。例如,对于每个用户ID,你可以设置一个位图,其中每一位代表一天用户是否活跃。这样,你可以快速地找出在特定日期活跃的用户
      3. 权限控制: 位图可以用来表示用户的权限。例如,你可以为每个用户创建一个位图,其中每一位代表一个特定的权限。这样,你可以快速地检查用户是否有某个权限
      4. 统计分析: 位图可以用来统计某些事件的发生次数。例如,你可以使用位图来统计用户在特定时间段内的活动情况
      5. 缓存加速: 位图可以作为缓存的一部分,用于加速某些查询操作。例如,如果你经常需要查询某个用户是否具有某个权限,你可以使用位图来存储这个信息,从而提高查询效率
      6. IP黑名单/白名单:使用位图来记录IP地址的黑名单或白名单,每个IP地址对应位图中的一个位,黑名单中的IP地址对应位为1,白名单中的IP地址对应位为0。这样可以高效地过滤非法的访问请求
      7. 布隆过滤器:使用位图来实现布隆过滤器,用于快速判断一个元素是否存在于一个集合中。通过多个哈希函数将元素映射到位图中的多个位,查询时检查这些位是否都为1,如果是则认为元素存在,否则认为元素不存在
    4. 底层数据结构:
      1. Redis的位图功能并不是一个独立的数据结构,而是基于Redis的字符串类型进行操作的一种机制。在Redis中,字符串类型的最大长度可以达到512MB,因此位图的偏移量(offset)值也有相应的上限,即2^32-1。位图的实际占用存储空间取决于位图偏移量offset的最大值,占用字节数可以用(max_offset / 8) + 1公式来计算或者直接借助底层字符串函数strlen来计算
      2. 位图的操作是通过一系列针对字符串的位操作命令来完成的,这些命令包括SETBIT、GETBIT、BITCOUNT等。SETBIT命令用于设置位图中指定偏移位置的位的值,GETBIT命令用于获取位图中指定偏移位置的位的值,BITCOUNT命令则用于统计位图中值为1的位的数量
      3. 总的来说,Redis的位图功能是基于字符串类型实现的,通过一系列位操作命令来完成各种位图操作。这种设计不仅充分利用了字符串类型的特性,而且提供了高效的位操作能力,使得位图成为Redis中的一个重要且灵活的数据结构
    5. 为什么选用这种数据结构?
      1. 灵活性:字符串数据结构在 Redis 中是非常通用和灵活的,可以用来存储各种类型的数据,包括文本、数字、二进制等。选择字符串作为位图的底层数据结构可以使位图与其他数据类型无缝集成,方便统一管理和操作。
      2. 高效性:字符串在内存中是连续存储的,这样可以实现高效的内存访问和位操作。位图中的每个比特位都可以直接对应字符串中的一个字符,从而可以实现高效的位操作,如设置、获取、位运算等。
      3. 空间效率:字符串在 Redis 中采用简单动态字符串(SDS)实现,具有动态调整内存大小的能力。这意味着位图可以根据实际需要动态调整大小,从而节约内存空间
      4. 便于持久化:字符串数据结构可以被很方便地序列化和持久化到磁盘上,Redis 通过持久化机制可以将位图持久化到磁盘上,实现数据的持久化存储和恢复
      5. 丰富的命令支持:Redis 提供了丰富的字符串操作命令和位图专用的命令,可以方便地对位图进行各种操作,如设置特定位置的比特位、获取特定位置的比特位、对比特位进行位运算等。
  7. HyperLogLog(基数估计)

    1. 特点:Redis的HyperLogLog是一种特殊的概率型数据结构,主要用于解决大数据量的去重问题,即统计一个集合中不同元素的数量。不同于传统的Bitmap和BitmapMap数据结构,HyperLogLog具有空间效率高、内存占用固定以及处理速度快等优点,可以用来估计一个集合中不重复元素的数量,而不需要占用大量的内存。它通过使用非常少的内存空间来存储大量的元素,使得在牺牲一定精确度的情况下,能够快速地计算出近似的基数
      1. 空间效率高:相比于其他数据结构,HyperLogLog在存储相同数量的数据时所需的空间更少。例如,它可以近似统计一个包含2^64个元素的集合,而只占用约12KB的内存空间
      2. 内存占用固定:无论输入数据的规模大小,HyperLogLog的内存占用始终保持不变,这对于处理大规模数据集非常有用
      3. 处理速度快:HyperLogLog的读写操作都可以在O(1)的时间复杂度内完成,这对于需要频繁更新和查询的数据集来说非常重要
      4. 基于概率算法:HyperLogLog 使用了一种基于概率的算法来估计集合的基数,其核心思想是利用哈希函数将元素映射到一个固定大小的比特位数组中,然后统计数组中的非零位的数量来估计基数
      5. 哈希函数:HyperLogLog 使用哈希函数将元素映射到比特位数组中的位置。Redis 使用了 MurmurHash 等高效的哈希函数来实现这一过程。
      6. 近似基数计算:HyperLogLog 的内存占用量是固定的,与集合中元素数量无关,只取决于指定的精度参数 b。因此,可以存储非常大的集合而不会占用大量的内存空间
      7. 误差控制:HyperLogLog 的基数估计值具有一定的误差,通常在千分之一左右,但误差随着集合大小增加而减小,Redis官方给出的HyperLogLog的标准误差率是0.81%。这意味着当你使用HyperLogLog进行统计时,得到的结果可能会有最多0.81%的偏差。换句话说,如果你统计得到的结果是10000,那么实际的元素数量可能在9189到10811之间。
      8. Union 和 Merge 操作:HyperLogLog 支持将多个 HyperLogLog 结构合并为一个 HyperLogLog,以便对多个集合的基数进行估计
    2. 常见操作:
      1. PFADD key element [element ...]  这个命令用于将指定的元素添加到指定的HyperLogLog中。如果添加成功,则返回1;否则,返回0。例如,你可以创建一个新的HyperLogLog,并向其中添加一些元素:PFADD ip 192.168.100.1 192.168.100.1 192.168.100.1 192.168.100.2 192.168.100.2 192.168.100.3 192.168.100.3 这将返回1,表明元素已成功添加到HyperLogLog
      2. PFCOUNT key [key ...]  这个命令用于返回给定HyperLogLog的基数估算值。例如,你可以使用这个命令来获取之前添加到HyperLogLog中的元素的总数, 比如:PFCOUNT ip ,这将返回3,这是HyperLogLog中不同元素的数量
      3. PFMERGE destkey sourcekey [sourcekey ...] 这个命令用于将多个HyperLogLog合并为一个HyperLogLog。例如,你可以将两个不同的HyperLogLog合并为一个
    3. 使用场景:
      1. UV统计:在网站分析领域,HyperLogLog可以用于统计独立访客(Unique Visitor, UV)的数量。由于HyperLogLog具有较高的空间效率,可以在存储空间有限的情况下,处理大量的用户访问数据
      2. 去重计数:在数据清洗和预处理阶段,可以使用HyperLogLog来进行重复数据的统计和去除。例如,在一个大型数据集中,你可能想要知道有多少唯一的不重复数据项。HyperLogLog可以有效地完成这项任务,同时保持较低的内存消耗
      3. 大数据量处理:当处理的数据量非常大时,传统的集合数据结构可能无法胜任,因为它们需要更多的存储空间。在这种情况下,HyperLogLog可以作为一种有效的解决方案,因为它能够在保持较低内存消耗的同时,处理大量的数据
      4. 分布式系统:在分布式系统中,你可能需要跟踪不同服务器的请求来源。HyperLogLog可以帮助你在内存中维护这些请求的分布,并且以较低的存储成本进行统计分析
      5. 实时数据分析:在一些需要实时数据分析的应用中,HyperLogLog可以快速响应并提供近似的统计结果,这对于那些不需要精确到每一个数据点的应用来说是非常有用的
    4. 底层数据结构:
      1. 它使用了16384个桶(bucket)来记录各自桶的元素数量。每个桶对应一个位,通过位操作来记录元素是否出现过。
      2. 当一个新的元素加入时,会通过一定的哈希函数将其映射到一个特定的桶中。如果该桶对应的位已经被设置为1,则说明这个元素已经出现过;如果没有,则将该位的值设为1。通过这种方式,HyperLogLog能够以极小的空间代价来近似统计集合中不同元素的数量。
      3. HyperLogLog的一个关键特性是其空间效率非常高,对于非常大的数据集,它只需要很少的内存空间就可以进行有效的统计。此外,虽然HyperLogLog提供的统计结果是近似的,但它设计得足够智能,使得即使在大量数据的情况下,也能保持较低的误差率。
    5. 为什么选择这样的数据结构?
      1. 存储需求与准确度的权衡:HyperLogLog 旨在以较小的内存消耗提供对大型数据集合基数的近似估计。通过使用 16384 个桶,可以在相对较小的内存空间内存储大量的哈希值统计信息,并提供足够的准确度。HyperLogLog被设计为占用固定且较小的内存空间,大约为12KB。这是通过使用16384个桶(每个桶占用6位)来实现的,从而达到空间上的高效利用
      2. 桶数量的理论基础:HyperLogLog 使用基数为 2 的指数增长的桶数量。16384(即 2^14)正好是一个较为合适的值,它在提供良好准确度的同时,也可以有效地控制内存消耗。
      3. 位操作的效率:HyperLogLog 使用位操作来记录每个桶的计数值,而位操作在计算机硬件中通常具有较高的效率。通过使用 16384 个桶,可以更好地利用位操作的高效性,从而提高计算性能。
      4. 概率算法的稳定性:HyperLogLog 基于概率算法实现近似计数,而概率算法的稳定性通常受到样本数量的影响。通过使用 16384 个桶,可以确保对大多数数据集合都能提供稳定且准确的估计结果。

      5. 扩展性:16384个桶的设计允许HyperLogLog在处理大规模数据集时具有较好的扩展性。随着数据量的增加,HyperLogLog可以通过调整桶的数量来适应更大的数据集

      6. 内存管理:在Redis中,当HyperLogLog的计数较小的时候,会采用稀疏存储的方式来节省内存。只有当计数逐渐增大,超过一定阈值时,才会转变为占用12KB空间的密集存储

  8. Geospatial(地理空间索引)

    1. 特点:Redis Geospatial是Redis 3.2版本引入的一种数据类型,主要用于存储地理位置信息,并对存储的信息进行操作。这种数据类型特别适合于需要地理位置服务的应用场景,如地图服务、导航系统、社交媒体中的地理标签等。
    2. 常见操作:
      1. GEOADD 命令用于向指定的 key 添加一个或多个地理位置。你需要提供 key 的名字,然后是每个地点的经纬度和它的名称,如:GEOADD map 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"  这条命令会将 "Palermo" 和 "Catania" 这两个地点的经纬度添加到名为 "map" 的 key 中
      2. GEOPOS 命令用于获取一个或多个地理位置的经纬度。你可以指定一个或多个地点的名称,然后 Redis 会返回它们的经度和纬度。例如:GEOPOS map "Palermo" "Catania", 这条命令会返回 "Palermo" 和 "Catania" 这两个地点的经纬度
      3. GEODIST 命令用于计算两个地理位置之间的距离。你可以指定两个地点的名称,然后 Redis 会返回它们之间的距离。例如:GEODIST map "Palermo" "Catania" KM  这条命令会返回 "Palermo" 和 "Catania" 这两个地点之间的距离,单位是千米
      4. GEORADIUS 命令用于查找给定中心坐标周围特定半径内的地理位置。你可以指定中心坐标的经纬度,然后是半径和单位(如米或千米)。例如:GEORADIUS map 13.361389 38.115556 100 KM ,这条命令会返回在 "Palermo"(中心坐标)周围 100 千米半径内的所有地理位置
      5. GEORADIUS BY MEMBER 命令用于查找给定成员周围特定半径内的其他成员。你可以指定一个成员的名称,然后是半径和单位。例如:GEORADIUS BY MEMBER map "Palermo" 100 KM,这条命令会返回在 "Palermo" 周围 100 千米半径内的所有其他地理位置
    3. 使用场景:
      1. 位置服务:在基于位置的服务(LBS)中,可以使用 Redis Geospatial 来存储用户的经纬度信息,并根据这些信息进行各种查询和分析。例如,滴滴打车等服务就需要处理大量的地理位置信息,Redis Geospatial 可以帮助快速找到附近的车辆或司机
      2. 推荐系统:在电商或社交媒体平台中,可以根据用户的地理位置推荐附近的商家或朋友。例如,美团外卖可以推荐用户附近的餐厅,微信朋友圈也可以推荐附近的人
      3. 导航和地图服务:在地图服务和导航应用中,Redis Geospatial 可以用来存储地图上的标记点,并快速查询用户附近的标记点,以便于提供导航和地图浏览功能
      4. 实时数据分析:在物联网(IoT)领域,设备通常会有自己的地理位置信息。Redis Geospatial 可以用来存储这些信息,并通过地理位置信息来进行数据的实时分析和展示
      5. 游戏开发:在多人在线游戏中,玩家的角色通常会在地图上有自己的位置。Redis Geospatial 可以用来存储这些位置信息,并快速查询玩家附近的敌人或其他玩家
      6. 紧急响应和服务调度:在紧急响应系统中,如救护车调度,需要快速查询最近的救护站或医院。Redis Geospatial 可以用来存储这些位置信息,并快速查询最近的服务点
    4. 底层数据结构:
      1. Redis Geospatial 的底层数据结构是基于 Sorted Set(有序集合)实现的。在 Redis 中,每个 Sorted Set 是由一个唯一的标识符(key)和一个对应的分数(score)组成的。对于 Geospatial 功能,这个分数实际上是地理位置的经纬度信息,而标识符则是地点的名称。
      2. 当使用 GEOADD 命令添加地理位置时,Redis 会首先将经纬度转换为 GeoHash 编码,然后将这个编码作为 Sorted Set 中的分数,地点的名称作为成员member
      3. GeoHash 是一种将二维空间坐标(经度和纬度)转换为一维字符串的方法。这种编码方式能够有效地将地理位置信息压缩为一个字符串,从而方便在 Sorted Set 中进行管理和查询。
      4. 总的来说,Redis Geospatial 的底层数据结构设计得十分巧妙,它充分利用了 Sorted Set 的特性,并结合 GeoHash 编码,使得地理位置信息的存储和查询变得更加高效和便捷。
    5. 为什么选这种数据结构?
      1. 有序性: Sorted Set 可以保持元素的有序性,这对于地理位置数据的查询非常有用,因为通常我们需要根据地理位置的距离或者其他属性进行排序。
      2. 效率: Sorted Set 使用了跳表(Skip List)这一数据结构,它在查找、插入和删除操作上都有较高的效率,这对于地理位置数据的频繁读写操作是非常适合的。
      3. 空间利用率: Sorted Set 在存储时会自动压缩数据,减少内存占用,这对于存储大量地理位置数据来说非常重要。
      4. 扩展性: Sorted Set 可以动态调整大小,这对于地理位置数据集可能会不断变化的情况非常有利。
      5. 地理哈希编码: Redis Geospatial 使用了一种叫做 GeoHash 的编码方法,将地理位置坐标转换为一个分数,这个分数可以被用作 Sorted Set 中的分数。这种方法可以有效地将地理位置数据映射到一个数值上,从而利用 Sorted Set 的特性进行操作

  • 16
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值