Redis对象,Redis从放弃到入门【Redis对象】

117 阅读14分钟

String

1.String是什么?

String就是字符串,最大为512MB。

2.String怎么用?

适用存储字节数据、文本数据、序列化后的对象数据等。

缓存场景,Value存Json字符串等信息。

计数场景,因为Redis处理命令是单线程,所以执行命令的过程时原子的。因此String数据类型适合计数场景,比如计算访问次数、点赞、转发、库存数量等。

3.常用操作

创建、查询、更新、删除。

  1. SET 写操作(创建、更新)
SET key value [EX seconds] [PX milliseconds] [NX|XX]

参数:

  • EX second:设置键的过期时间为多少秒。
  • PX millisecond:设置键的过期时间为多少毫秒。
  • NX:只在键不存在时,才对键进行设置操作。SET key value NX等同于SETNX key value。
  • XX:只在键存在时,才对键操作。

示例:

SET user:1 "Alice" EX 60  # 设置并在 60 秒后过期
SET counter 100 NX  # 仅在 counter 不存在时设置值

  1. GET 读操作
GET key

示例:

GET user:1  # 获取 user:1 的值

3. MGET 读操作

MGET key1 key2 ... keyN

示例:

MGET user:1 user:2 user:3  # 获取多个键的值

4. SETNX 写操作

SETNX key value

示例:

SETNX user:1 "Alice"  # 只有在 user:1 不存在时设置其值

5. SETEX 写操作

SETEX key seconds value
  • 设置 key 的值并设置过期时间(秒)。

示例:

SETEX session:12345 3600 "user_token"  # 设置并在 3600 秒后过期

4.底层实现?

String有三种编码方式:

  • INT编码:就是存一个整型,可以用long表示的整数就以这种编码存储。
  • EMBSTR编码:如果字符串小于等于阈值字节,使用EMBSTR编码。
  • RAW编码:如果字符串大于阈值字节,则用RAW编码。

redis3.0-4.0阈值是39字节,redis5.0是44字节。

EMBSTR和RAW都是由redisObject和SDS两个结构组成,它们的差异在于,EMBSTR下redisObject和SDS是连续的内存,RAW编码下redisObject和SDS内存是分开的。

EMBSTR优点是redisObject和SDS两个结构可以一次性分配空间,缺点在于如果重新分配空间,整体都需要再分配,所以EMBSTR设计为只读,任何写操作之后EMBSTR都会变成RAW,理念是发生过修改的字符串通常会认为是易变的。

我们注意到,EMBSTR和RAW里都有一个叫SDS的结构,那么它是什么呢?

1.增加长度字段len,快速返回长度;

2.增加空余空间alloc-len,为后续追加数据留余地;

3.不再以'\0'作为判断标准,二进制安全。

List

1.简介

  Redis List是一组连接起来的字符串集合。

  List最大的元素个数是2的32次方-1,新版本4.0之后是64次方-1。

2.使用场景

  List作为一个列表存储,属于比较底层的数据结构,比如存储一批任务数据、存储一批消息等。

3.常用操作

  • 创建:LPUSH、RPUSH
  • 查询:LLEN、LRANGE
  • 更新:LPUSH、RPUSH、LPOP、RPOP、LREN
  • 删除:DEL、UNLINK

  1.写操作

  1、LPUSH从左侧插入,RPSH从右侧插入。

  2、LPOP移出并获取列表的第一个元素;RPOP移出并获取列表最后一个元素。

  3、LREN key count value,移出值等于value的元素,当count=0,则移出素有等于value的元素,当count>0,则从左到右移出count个,当count<0,则从右到左移出count个。返回被移除元素的数量。

  4、DEL和UNLINK,DEL是同步删除命令,UNLINK是异步删除命令不会阻塞客户端。

  2.读操作

  1、LLEN,查看list的长度。

  2、LRANGE,查看从start到stop的元素。

  4.底层实现

  3.2版本之前,List对象有两种编码方式,ZIPLIST和LINKEDLIST。

  ZIPLIST使用条件:

  1. 列表对象保存的所有字符串对象长度都小于64字节。
  2. 列表对象元素个数少于512个。

  LinkedList:

  ZIPLIST和LInkedList相比,ZIPLIST内存更加紧凑,所以只有在列表个数或节点数据比较大的时候,才会使用到LINKEDLIST编码。

  所以,ZIPLIST是为了在数据较少时节约内存,LInkedList是为了数据多时提高更新效率,而ZIPLIST数据稍多时会导致很多内存复制。

  后来,引入了QUICKLIST,其实就是ZIPLIST和LInkedLIST的结合体。

  原来LInkedList是单个节点,只能存一个数据,现在单个节点存的是一个ZIPLIST,即多个数据。

  5.压缩列表的优化

  平常说的压缩列表一般是指ZIPLIST,一种是LISTPACK 5.0引入的,直到7.0完全替代了ZIPLIST。

  1.ZIPLIST结构

  • zlbytes:占用4个字节,记录了整个ziplist占用的总字节数。
  • zltail:占用4个字节,指向最后一个entry偏移量,用于快速定位最后一个entry。
  • zllen:占用2字节,记录entry总数。
  • entry:列表元素。
  • zlend:ziplist结束标志,占用1字节,值等于255。

  ziplist节点结构

  <prevlen> <encoding> <entry-data>

  prevlen:表示上一个节点的数据长度。如果前一节点的长度,也就是entry的大小 小于254字节,那么prevlen需要用1字节长的空间来保存这个长度值,255是特殊字符,被zlend占用了。

  encoding:编码类型,包含了一个entry的长度信息,可用于正向遍历。

  entry-data:实际的数据。

  2.ziplist更新数据

  更新操作可能带来连锁更新。连锁更新是指这个后移,发生了不止一次,而是多次。

  什么是连锁更新?

  比如增加一个头部新节点,后面依赖它的节点,需要prevlen记录它的大小,原本只用1字节记录,因为更新可能膨胀为5字节,然后这个entry的大小就也膨胀了。所以,当这个新数据插入导致的后移完成之后,还需要逐步迭代更新。

  3.LISTPACK优化

  ziplist需要支持LIST,LIST是双端访问的结构,所以需要能从后向前遍历。

  <prevlen> <encoding> <entry-data>

  其中,prevlen就表示上一个节点的数据长度,通过这个字段可以定位上一个节点的数据。

  那么,我们可不可以改为不记录这个prevlen,但是又能找到上一个节点的起始位置的办法?

  <encoding-type> <element-data> <element-tot-len>

  encoding-type:是编码类型;element-datt:是数据内容;element-tot-len:存储整个节点除它自身的长度。

  要找到上一个节点的秘密就需要element-tot-len

  element-tot-len所占用的每个字节的第一个bit用于标识是否结束。0是结束,1是继续,剩下7个bit来存储数据大小。

  当我们需要找到当前元素的上一个元素时,我们可以从后向前依次查找每个字节,找到上一个Entry的element-tot-len字段的结束标识。

Set

1.SET是什么?

Redis的Set是一个不重复、无序的字符串集合。

2.适用场景

适用于无序集合场景,比如某个用户关注了哪些公众号,这些信息就可以放进一个集合,Set提供了查交集、并集的功能,可以很方便地实现共同关注的功能。

3.常用操作

  • 创建:SADD
  • 查询:SISMEMBR、SCARD、SMEMBERS、SSCAM、SINTER、SUNION、SDIFF
  • 更新:SADD、SREM
  • 删除:DEL

1.写操作

1、SADD,添加元素,返回成功添加了几个元素。

2、SREM,删除元素,返回值为成功删除了几个元素。

2.读操作

1、SISMEMBER,查询元素是否存在。

2、SCARD,查询集合元素个数。可以查看成员个数:SCARD key

3、SMEMBERS,查看集合的所有元素。可以查看所有成员:SMEMBERS set1

4、SSCAN

5、SINTER,返回在第一个集合里,同时在后面所有集合都存在的元素。A交B,交集。

6、SUNION,返回第一个集合里有,且在后续集合中不存在的元素。并集。

4.底层实现

Set对象编码方式:INTSET、HASHTABLE。

INSET编码

如果集合元素都是整数,且元素数量不超过512个,就可以用INTSET编码。INTSET编码排列比较紧凑,内存占用少,但是查询时需要二分查找(INSET下是有序的)。

HASHTABLE

如果不满足INTSET的条件,就需要用HASHTABLE。

Hash

1.hash是什么?

Redis Hash是一个field、value都为string的hash表,存储在Redis的内存中。

2.适用场景

适用于O(1)时间字典查找某个field对应数据的场景,比如任务信息的配置,就可以任务类型为field,任务配置参数为value。

3.常用操作

  • 创建:HSET、HSETNX
  • 查询:HGETALL、HGET、HLEN、HSCAN
  • 更新:HSET、HSETNX、HDEL
  • 删除:DEL

1.写操作

1、HSET,为集合对应field设置value数据。字段+值。

HSET key field value [field value ...]

2、HSETNX,如果field不存在,则为集合对应设置value数据。如果存在则不设置。

3、HDEL,删除指定字段field,可以一次删除多个。

4、DEL,删除Hash对象。

5、HMSET,可以设置多个键值对。在Redis4.0之前,HSET只能设置单个键值对,4.0之后,弃用HMSET,改用HSET。

2.读操作

1、HGETALL,查找全部数据。

2、HGET,查询field对应的value。

3、HLEN,查找Hash中元素总数。

4、HSCAN,从指定位置查询一定数量的数据。

4.原理

Hash底层有两种编码结构,压缩列表和HASHTABLE。同时满足以下两个条件,用压缩列表:

  1. Hash对象保存的所有值和键的长度都小于64字节;
  2. Hash对象元素个数少于512个。

两个条件任何一条不满足,编码结构就用HASHTABLE。

ZIPLIST其实就是在数据量小的时候将数据紧凑排列,对应到Hash,就是将field-value当做entry放入ZIPLIST。查找key的时间复杂度O(N)。

HASHTABLE在之前无序集合SET中也有应用,区别就是,在SET中value始终为null,但是Hash中是有对应的值。查找key的时间复杂度O(1)。

5.总结

1、Hash的编码方式是什么?

一个是ZIPLIST,一个是HASHTABLE。ZIPLIST适用于元素较少且单个元素长度较小的情况,其他情况使用HASHTABLE。

2、HASH为什么要用两种编码方式?

采用两种编码方式的原因是ZIPLIST更节约内存,所以在小数据量使用,而数据多时,需要使用HASHTABLE提高更高的查找、更新性能。

HASTABLE

别,这个模块还没结束呢。学了SET和HASH之后,我们都见到了底层有一个叫HASHTABLE的结构,接下来就去探究一下这是个啥。

1.HASHTABLE简述

简单点说,就是哈希表。那么有什么用呢?

就好比一本书,如果让你一页一页去找是不是很麻烦,要是有一个目录可以直接根据关键字就能定位,是不是效率就更高了。

2.HASHTABLE结构

// redis 5.0.5
typedef struct dictht {
    dictEntry **table;    /* 哈希桶数组,指向实际的hash存储 */
    unsigned long size;   /* 哈希表大小(桶数) */
    unsigned long sizemask; /* 哈希表大小掩码 */
    unsigned long used;   /* 哈希表已使用的桶数量 */
} dictht;

3.渐进式扩容,缩容

// redis 5.0.5
typedef struct dict {
    dictht ht[2];         /* 目前使用的两个哈希表(用于rehash) */
    dictType *type;       /* 数据类型 */
    void *privdata;       /* 私有数据(通常为 NULL),保存需要传给那些类型特定函数的可选参数*/
    long rehashidx;       /* 正在进行的 rehash 操作的桶索引 */
    unsigned long iterators; /* 迭代器数量 */
} dict;

为了实现渐进式扩容,redis没有直接把dictht暴露给上层,而是再封装一层,如上。

可以看到dict结构里面,包含了两个dictht结构,也就是两个HASHTABLE结构。dictEntry是链表结构,也就是用拉链法解决哈希冲突,用的头插法。

实际上平时用的都是一个HASHTABLE,在触发扩容之后,就会两个HASHTABLE同时使用,以下是详细流程:

  1. 首先,为新Hash表ht[1]分配空间。新表大小为第一个大于等于原表2倍used(已使用的桶数量)的2次方幂。然后迁移ht[0]数据到ht[1]。在ReHash(是指重新计算键的哈希值和索引值)进行期间,每次对字典执行增删改查操作,程序会顺带迁移当前rehashidx在ht[0]上对应的数据,并更新偏移索引。同时,部分情况周期函数也会进行迁移。(这里解释一下这个rehashidx是什么意思:字典同时是拥有ht[0]和ht[1],将rehashidx设置为0,表示rehash开始;在rehash期间,每次对字典crud,会顺带将ht[0]哈希表在rehashidx索引上的所有kv rehash到ht[1],当rehash完成后rehashidx+1;随着字典不断操作,最终ht[0]所有键值都会被rehash到ht[1],这时将rehashidx设置为-1,表示操作结束。注意:在渐进式rehash的过程,如果有crud,如果index大于rehashidx,访问ht[0],否则访问ht[1]。)
  2. 然后,随着字典不断执行,最终在某个时间点上,ht[0]的所有键值都会被Rehash至ht[1],此时再将ht[1]和ht[0]指针对象互换,同时把偏移索引rehashidx的值设为-1,表示Rehash已完成。

既然知道了扩容的流程,那么扩容时机是什么时候呢?

redis会根据负载因子的情况来扩容:

  1. 负载因子大于等于1,说明此时空间已经非常紧张。
  2. 负载因子大于5,此时即使有复制命令,也要进行Rehash扩容。

负载因子:k=ht[0].used / ht[0].size

如果扩容太大,但是数据已经减少了,就需要进行缩容,缩容也是渐进式的。那么什么时机缩容呢?

当负载因子小于0.1,即负载率小于10%,此时进行缩容,新表大小为第一个等于原表used的2次方幂。

总之,ZIPLIST、HASHTABLE面试超级热点,不仅学习这些大致思路,还要掌握一些细节。

Sorted SET

跳表

1.跳表是什么?

跳表是Redis有序集合ZSet底层的数据结构,跳表在ZSET中尤其重要。

跳表的本质还是链表,只是在普通链表的基础上,增加了多级的索引,通过索引可以一次实现多个节点的跳跃,提高性能。

跳表的结构

标准的跳表(Redis不是使用标准的跳表)有以下限制:

  1. score值不能重复;
  2. 只有向前指针,没有回退指针。

2.Redis的跳表实现

Redis跳表单个节点有几层?

层次的决定,需要比较随机,Redis是使用概率均衡的思路来确定新插入节点的层数。

Redis跳表决定每一个节点,是否能增加一层的概率为25%,而最大层数限制在Redis5.0是64层,Redis7.0是32层。

Redis跳表优化了多少?

O(N)降低到log(N)。

ZSET

1.ZSET是什么?

ZSET就是有序集合,也叫SORTED SET,是一组按关联积分有序的字符串集合,这里的分数是个抽象概念,任何指标都可以抽象为分数,以满足不同场景。积分相同的情况下,按字典序排序。

2.适用场景

用于需要排序集合的场景,最为典型的就是游戏排行榜。

3.常用操作

  • 创建:ZADD
  • 查询:ZRANGE、ZCOUNT、ZRANK、ZCARD、ZSCORE
  • 更新:ZADD、ZREN
  • 删除:DEL、UNLINK
1.写操作

1、ZADD key scoremember [score member ...]

向ZSET增加数据,如果key已经存在,则更新对应数据。

扩展参数:

  • XX:仅更新存在的成员,不添加新成员。
  • NX:不更新存在的成员,只添加新成员。
  • LT:更新新的分值比当前分值小的成员,不存在则新增。
  • GT:更新新的分值比当前分值大的成员,不存在则新增。

2、ZREM key member[member ...] ,删除ZSET中的元素。

2.读操作

1、ZCARD key,查看成员总数。

2、ZRANGE key start stop,查看从start到stop范围的ZSET数据。

3、ZREVRANGE key start stop,从大到小遍历。

4、ZCOUNT key min max,计算min-max积分范围的成员个数。

5、ZRANK key member,查看ZSET中的member的排名索引。

6、ZSCORE key member,查询ZSET中成员的分数。

4.底层实现

ZSET编码有两种方式,一种是ZIPLIST,另一种是SKIPLIST+HASHTABLE。

ZIPLIST编码的使用条件:

  1. 列表对象保存的所有字符串对象长度都小于64字节。
  2. 列表对象元素个数少于128个。

若有一条不满足,编码就使用SKIPLIST+HASHTABLE。

SKIPLIST是一种可以快速查找的多级链表结构。并且还使用HASHTABLE来配合查询O(1)。

完结撒花