redis面试基础知识

redis的数据类型

Redis是一个key-value的数据库,key一般是String类型,不过value的类型多种多样:

五种基本数据类型

Redis 通用命令

通用指令是部分数据类型的,都可以使用的指令,常见的有:

  • KEYS:查看符合模板的所有key

  • DEL:删除一个指定的key

  • EXISTS:判断key是否存在

  • EXPIRE:给一个key设置有效期,有效期到期时该key会被自动删除

  • TTL:查看一个KEY的剩余有效期

通过help [command] 可以查看一个命令的具体用法,例如:

  • KEYS

127.0.0.1:6379> keys *
1) "name"
2) "age"
127.0.0.1:6379>
​
# 查询以a开头的key
127.0.0.1:6379> keys a*
1) "age"
127.0.0.1:6379>

贴心小提示:在生产环境下,不推荐使用keys 命令,因为这个命令在key过多的情况下,效率不高

  • DEL

127.0.0.1:6379> help del
​
  DEL key [key ...]
  summary: Delete a key
  since: 1.0.0
  group: generic
​
127.0.0.1:6379> del name #删除单个
(integer) 1  #成功删除1个
​
127.0.0.1:6379> keys *
1) "age"
​
127.0.0.1:6379> MSET k1 v1 k2 v2 k3 v3 #批量添加数据
OK
​
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k1"
4) "age"
​
127.0.0.1:6379> del k1 k2 k3 k4
(integer) 3   #此处返回的是成功删除的key,由于redis中只有k1,k2,k3 所以只成功删除3个,最终返回
127.0.0.1:6379>
​
127.0.0.1:6379> keys * #再查询全部的key
1) "age"    #只剩下一个了
127.0.0.1:6379>

贴心小提示:同学们在拷贝代码的时候,只需要拷贝对应的命令哦~

  • EXISTS

127.0.0.1:6379> help EXISTS
​
  EXISTS key [key ...]
  summary: Determine if a key exists
  since: 1.0.0
  group: generic
​
127.0.0.1:6379> exists age
(integer) 1
​
127.0.0.1:6379> exists name
(integer) 0
  • EXPIRE

贴心小提示:内存非常宝贵,对于一些数据,我们应当给他一些过期时间,当过期时间到了之后,他就会自动被删除~

127.0.0.1:6379> expire age 10
(integer) 1
​
127.0.0.1:6379> ttl age
(integer) 8
​
127.0.0.1:6379> ttl age
(integer) 6
​
127.0.0.1:6379> ttl age
(integer) -2
​
127.0.0.1:6379> ttl age
(integer) -2  #当这个key过期了,那么此时查询出来就是-2 
​
127.0.0.1:6379> keys *
(empty list or set)
​
127.0.0.1:6379> set age 10 #如果没有设置过期时间
OK
​
127.0.0.1:6379> ttl age
(integer) -1  # ttl的返回值就是-1

String字符串

String类型,也就是字符串类型,是Redis中最简单的存储类型。

其value是字符串,不过根据字符串的格式不同,又可以分为3类:

  • string:普通字符串

  • int:整数类型,可以做自增.自减操作

  • float:浮点类型,可以做自增.自减操作

String的常见命令有:

  • SET:添加或者修改已经存在的一个String类型的键值对

  • GET:根据key获取String类型的value

  • MSET:批量添加多个String类型的键值对

  • MGET:根据多个key获取多个String类型的value

  • INCR:让一个整型的key自增1

  • INCRBY:让一个整型的key自增并指定步长,例如:incrby num 2 让num值自增2

  • INCRBYFLOAT:让一个浮点类型的数字自增并指定步长

  • SETNX:添加一个String类型的键值对,前提是这个key不存在,否则不执行

  • SETEX:添加一个String类型的键值对,并且指定有效期

贴心小提示:以上命令除了INCRBYFLOAT 都是常用命令

  • SET 和GET: 如果key不存在则是新增,如果存在则是修改

127.0.0.1:6379> set name Rose  //原来不存在
OK

127.0.0.1:6379> get name 
"Rose"

127.0.0.1:6379> set name Jack //原来存在,就是修改
OK

127.0.0.1:6379> get name
"Jack"
  • MSET和MGET

127.0.0.1:6379> MSET k1 v1 k2 v2 k3 v3
OK
​
127.0.0.1:6379> MGET name age k1 k2 k3
1) "Jack" //之前存在的name
2) "10"   //之前存在的age
3) "v1"
4) "v2"
5) "v3"
  • INCR和INCRBY和DECY

127.0.0.1:6379> get age 
"10"
​
127.0.0.1:6379> incr age //增加1
(integer) 11
    
127.0.0.1:6379> get age //获得age
"11"
​
127.0.0.1:6379> incrby age 2 //一次增加2
(integer) 13 //返回目前的age的值
    
127.0.0.1:6379> incrby age 2
(integer) 15
    
127.0.0.1:6379> incrby age -1 //也可以增加负数,相当于减
(integer) 14
    
127.0.0.1:6379> incrby age -2 //一次减少2个
(integer) 12
    
127.0.0.1:6379> DECR age //相当于 incr 负数,减少正常用法
(integer) 11
    
127.0.0.1:6379> get age 
"11"
​
  • SETNX

127.0.0.1:6379> help setnx
​
  SETNX key value
  summary: Set the value of a key, only if the key does not exist
  since: 1.0.0
  group: string
​
127.0.0.1:6379> set name Jack  //设置名称
OK
127.0.0.1:6379> setnx name lisi //如果key不存在,则添加成功
(integer) 0
127.0.0.1:6379> get name //由于name已经存在,所以lisi的操作失败
"Jack"
127.0.0.1:6379> setnx name2 lisi //name2 不存在,所以操作成功
(integer) 1
127.0.0.1:6379> get name2 
"lisi"
  • SETEX

127.0.0.1:6379> setex name 10 jack
OK
​
127.0.0.1:6379> ttl name
(integer) 8
​
127.0.0.1:6379> ttl name
(integer) 7
​
127.0.0.1:6379> ttl name
(integer) 5

Hash散列表

Hash类型,也叫散列,其value是一个无序字典,类似于Java中的HashMap结构。 

String结构是将对象序列化为JSON字符串后存储,当需要修改对象某个字段时很不方便:

Hash结构可以将对象中的每个字段独立存储,可以针对单个字段做CRUD:

Hash类型的常见命令

  • HSET key field value:添加或者修改hash类型key的field的值

  • HGET key field:获取一个hash类型key的field的值

  • HMSET:批量添加多个hash类型key的field的值

  • HMGET:批量获取多个hash类型key的field的值

  • HGETALL:获取一个hash类型的key中的所有的field和value

  • HKEYS:获取一个hash类型的key中的所有的field

  • HINCRBY:让一个hash类型key的字段值自增并指定步长

  • HSETNX:添加一个hash类型的key的field值,前提是这个field不存在,否则不执行

贴心小提示:哈希结构也是我们以后实际开发中常用的命令哟

  • HSET和HGET

127.0.0.1:6379> HSET heima:user:3 name Lucy//大key是 heima:user:3 小key是name,小value是Lucy
(integer) 1
127.0.0.1:6379> HSET heima:user:3 age 21// 如果操作不存在的数据,则是新增
(integer) 1
127.0.0.1:6379> HSET heima:user:3 age 17 //如果操作存在的数据,则是修改
(integer) 0
127.0.0.1:6379> HGET heima:user:3 name 
"Lucy"
127.0.0.1:6379> HGET heima:user:3 age
"17"
  • HMSET和HMGET

127.0.0.1:6379> HMSET heima:user:4 name HanMeiMei
OK
127.0.0.1:6379> HMSET heima:user:4 name LiLei age 20 sex man
OK
127.0.0.1:6379> HMGET heima:user:4 name age sex
1) "LiLei"
2) "20"
3) "man"
  • HGETALL

127.0.0.1:6379> HGETALL heima:user:4
1) "name"
2) "LiLei"
3) "age"
4) "20"
5) "sex"
6) "man"
  • HKEYS和HVALS

127.0.0.1:6379> HKEYS heima:user:4
1) "name"
2) "age"
3) "sex"
127.0.0.1:6379> HVALS heima:user:4
1) "LiLei"
2) "20"
3) "man"
  • HINCRBY

127.0.0.1:6379> HINCRBY  heima:user:4 age 2
(integer) 22
127.0.0.1:6379> HVALS heima:user:4
1) "LiLei"
2) "22"
3) "man"
127.0.0.1:6379> HINCRBY  heima:user:4 age -2
(integer) 20
  • HSETNX

127.0.0.1:6379> HSETNX heima:user4 sex woman
(integer) 1
127.0.0.1:6379> HGETALL heima:user:3
1) "name"
2) "Lucy"
3) "age"
4) "17"
127.0.0.1:6379> HSETNX heima:user:3 sex woman
(integer) 1
127.0.0.1:6379> HGETALL heima:user:3
1) "name"
2) "Lucy"
3) "age"
4) "17"
5) "sex"
6) "woman"

List链表

Redis中的List类型与Java中的LinkedList类似,可以看做是一个双向链表结构。既可以支持正向检索和也可以支持反向检索。

特征也与LinkedList类似:

  • 有序

  • 元素可以重复

  • 插入和删除快

  • 查询速度一般

常用来存储一个有序数据,例如:朋友圈点赞列表,评论列表等。

List的常见命令有:

  • LPUSH key element ... :向列表左侧插入一个或多个元素

  • LPOP key:移除并返回列表左侧的第一个元素,没有则返回nil

  • RPUSH key element ... :向列表右侧插入一个或多个元素

  • RPOP key:移除并返回列表右侧的第一个元素

  • LRANGE key star end:返回一段角标范围内的所有元素

  • BLPOP和BRPOP:与LPOP和RPOP类似,只不过在没有元素时等待指定时间,而不是直接返回nil

  • LPUSH和RPUSH

127.0.0.1:6379> LPUSH users 1 2 3
(integer) 3
127.0.0.1:6379> RPUSH users 4 5 6
(integer) 6
  • LPOP和RPOP

127.0.0.1:6379> LPOP users
"3"
127.0.0.1:6379> RPOP users
"6"
  • LRANGE

127.0.0.1:6379> LRANGE users 1 2
1) "1"
2) "4"

Set集合

Redis的Set结构与Java中的HashSet类似,可以看做是一个value为null的HashMap。因为也是一个hash表,因此具备与HashSet类似的特征:

  • 无序

  • 元素不可重复

  • 查找快

  • 支持交集.并集.差集等功能

Set类型的常见命令

  • SADD key member ... :向set中添加一个或多个元素

  • SREM key member ... : 移除set中的指定元素

  • SCARD key: 返回set中元素的个数

  • SISMEMBER key member:判断一个元素是否存在于set中

  • SMEMBERS:获取set中的所有元素

  • SINTER key1 key2 ... :求key1与key2的交集

  • SDIFF key1 key2 ... :求key1与key2的差集

  • SUNION key1 key2 ..:求key1和key2的并集

例如两个集合:s1和s2:

求交集:SINTER s1 s2

求s1与s2的不同:SDIFF s1 s2

具体命令

127.0.0.1:6379> sadd s1 a b c
(integer) 3
127.0.0.1:6379> smembers s1
1) "c"
2) "b"
3) "a"
127.0.0.1:6379> srem s1 a
(integer) 1
    
127.0.0.1:6379> SISMEMBER s1 a
(integer) 0
    
127.0.0.1:6379> SISMEMBER s1 b
(integer) 1
    
127.0.0.1:6379> SCARD s1
(integer) 2

案例

  • 将下列数据用Redis的Set集合来存储:

  • 张三的好友有:李四.王五.赵六

  • 李四的好友有:王五.麻子.二狗

  • 利用Set的命令实现下列功能:

  • 计算张三的好友有几人

  • 计算张三和李四有哪些共同好友

  • 查询哪些人是张三的好友却不是李四的好友

  • 查询张三和李四的好友总共有哪些人

  • 判断李四是否是张三的好友

  • 判断张三是否是李四的好友

  • 将李四从张三的好友列表中移除

127.0.0.1:6379> SADD zs lisi wangwu zhaoliu
(integer) 3
    
127.0.0.1:6379> SADD ls wangwu mazi ergou
(integer) 3
    
127.0.0.1:6379> SCARD zs
(integer) 3
    
127.0.0.1:6379> SINTER zs ls
1) "wangwu"
    
127.0.0.1:6379> SDIFF zs ls
1) "zhaoliu"
2) "lisi"
    
127.0.0.1:6379> SUNION zs ls
1) "wangwu"
2) "zhaoliu"
3) "lisi"
4) "mazi"
5) "ergou"
    
127.0.0.1:6379> SISMEMBER zs lisi
(integer) 1
    
127.0.0.1:6379> SISMEMBER ls zhangsan
(integer) 0
    
127.0.0.1:6379> SREM zs lisi
(integer) 1
    
127.0.0.1:6379> SMEMBERS zs
1) "zhaoliu"
2) "wangwu"

SortedSet(有序集合)

Redis的SortedSet是一个可排序的set集合,与Java中的TreeSet有些类似,但底层数据结构却差别很大。SortedSet中的每一个元素都带有一个score属性,可以基于score属性对元素排序,底层的实现是一个跳表(SkipList)加 hash表

SortedSet具备下列特性:

  • 可排序

  • 元素不重复

  • 查询速度快

因为SortedSet的可排序特性,经常被用来实现排行榜这样的功能。

SortedSet的常见命令有:

  • ZADD key score member:添加一个或多个元素到sorted set ,如果已经存在则更新其score值

  • ZREM key member:删除sorted set中的一个指定元素

  • ZSCORE key member : 获取sorted set中的指定元素的score值

  • ZRANK key member:获取sorted set 中的指定元素的排名

  • ZCARD key:获取sorted set中的元素个数

  • ZCOUNT key min max:统计score值在给定范围内的所有元素的个数

  • ZINCRBY key increment member:让sorted set中的指定元素自增,步长为指定的increment值

  • ZRANGE key min max:按照score排序后,获取指定排名范围内的元素

  • ZRANGEBYSCORE key min max:按照score排序后,获取指定score范围内的元素

  • ZDIFF.ZINTER.ZUNION:求差集.交集.并集

注意:所有的排名默认都是升序,如果要降序则在命令的Z后面添加REV即可,例如:

  • 升序获取sorted set 中的指定元素的排名:ZRANK key member

  • 降序获取sorted set 中的指定元素的排名:ZREVRANK key memeber

五种基本数据类型的底层原理

String

String 类型的底层的数据结构实现主要是 SDS(简单动态字符串)。SDS 和我们认识的 C 字符串不太一样,之所以没有使用 C 语言的字符串表示,因为 SDS 相比于 C 的原生字符串:

  • SDS 不仅可以保存文本数据,还可以保存二进制数据。因为 SDS 使用 len 属性的值而不是空字符来判断字符串是否结束,并且 SDS 的所有 API 都会以处理二进制的方式来处理 SDS 存放在 buf[] 数组里的数据。所以 SDS 不光能存放文本数据,而且能保存图片、音频、视频、压缩文件这样的二进制数据。
  • SDS 获取字符串长度的时间复杂度是 O(1)。因为 C 语言的字符串并不记录自身长度,所以获取长度的复杂度为 O(n);而 SDS 结构里用 len 属性记录了字符串长度,所以复杂度为 O(1)。
  • Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出。因为 SDS 在拼接字符串之前会检查SDS 空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。

List

List 类型的底层数据结构是由双向链表或压缩列表实现的:

  • 如果列表的元素个数小于 512 个(默认值,可由 list-max-ziplist-entries 配置),列表每个元素的值都小于 64 字节(默认值,可由 list-max-ziplist-value 配置),Redis 会使用压缩列表作为 List 类型的底层数据结构;
  • 如果列表的元素不满足上面的条件,Redis 会使用双向链表作为 List 类型的底层数据结构;

但是在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了(结合了双向链表和压缩链表的优点),替代了双向链表和压缩列表

压缩列表
压缩列表的最⼤特点,就是它被设计成⼀种内存紧凑型的数据结构,占⽤⼀块连续的内存空间,不仅可以利⽤ CPU 缓存,⽽且会针对不同⻓度的数据,进⾏相应编码,这种⽅法可以有效地节省内存开销。
但是,压缩列表的缺陷也是有的:
  • 不能保存过多的元素,否则查询效率就会降低;
  • 新增或修改某个元素时,压缩列表占⽤的内存空间需要重新分配,甚⾄可能引发连锁更新的问题。
因此, Redis 对象( List 对象、 Hash 对象、 Zset 对象)包含的元素数量较少,或者元素值不大的情况才会使⽤压缩列表作为底层数据结构。
接下来,就跟⼤家详细聊下压缩列表。

压缩列表结构设计

压缩列表是 Redis 为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组。

压缩列表在表头有三个字段:
  • zlbytes,记录整个压缩列表占⽤对内存字节数;
  • zltail,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量;
  • zllen,记录压缩列表包含的节点数量;
  • zlend,标记压缩列表的结束点,固定值 0xFF(⼗进制255)。
在压缩列表中,如果我们要查找定位第⼀个元素和最后⼀个元素,可以通过表头三个字段的⻓度直接定位,复杂度是 O(1) 。⽽ 查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了,因此压缩列表不适合保存过多的元素
另外,压缩列表节点( entry )的构成如下:
压缩列表节点包含三部分内容:
  • prevlen,记录了「前⼀个节点」的⻓度;
  • encoding,记录了当前节点实际数据的类型以及长度;
  • data,记录了当前节点的实际数据;
当我们往压缩列表中插⼊数据时,压缩列表就会根据数据是字符串还是整数,以及数据的⼤⼩,会使⽤不同空间⼤⼩的 prevlen encoding 这两个元素⾥保存的信息, 这种根据数据大小和类型进行不同的空间大小 分配的设计思想,正是 Redis 为了节省内存而采用的
分别说下, prevlen encoding 是如何根据数据的⼤⼩和类型来进⾏不同的空间⼤⼩分配。
压缩列表⾥的每个节点中的 prevlen 属性都记录了「前⼀个节点的⻓度」,⽽且 prevlen 属性的空间⼤⼩跟前⼀个节点⻓度值有关,⽐如:
  • 如果前⼀个节点的长度小于 254 字节,那么 prevlen 属性需要⽤ 1 字节的空间来保存这个⻓度值;
  • 如果前⼀个节点的⻓度⼤于等于 254 字节,那么 prevlen 属性需要⽤ 5 字节的空间来保存这个⻓度值;
encoding 属性的空间大小跟数据是字符串还是整数,以及字符串的⻓度有关:
  • 如果当前节点的数据是整数,则 encoding 会使⽤ 1 字节的空间进⾏编码。
  • 如果当前节点的数据是字符串,根据字符串的⻓度⼤⼩encoding 会使⽤ 1 字节/2字节/5字节的空间进⾏编码。

连锁更新

压缩列表除了查找复杂度⾼的问题,还有⼀个问题。

压缩列表新增某个元素或修改某个元素时,如果空间不不够,压缩列表占用的内存空间就需要重新分配。而 当新插入的元素较大时,可能会导致后续元素的 prevlen 占用空间都发生变化,从而引起「连锁更新」 问题,导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降
举个例子
现在假设⼀个压缩列表中有多个连续的、⻓度在 250 253 之间的节点,如下图:
因为这些节点⻓度值小于 254 字节,所以 prevlen 属性需要⽤ 1 字节的空间来保存这个⻓度值。
这时,如果将⼀个⻓度⼤于等于 254 字节的新节点加⼊到压缩列表的表头节点,即新节点将成为 e1 的前置节点,如下图:
因为 e1 节点的 prevlen 属性只有 1 个字节⼤⼩,⽆法保存新节点的长度,此时就需要对压缩列表的空间重分配操作,并将 e1 节点的 prevlen 属性从原来的 1 字节大小扩展为 5 字节大小。
多⽶诺牌的效应就此开始。后面的节点都需要重新分配空间,修改节点里面的数据。

压缩列表的缺陷

空间扩展操作也就是重新分配内存,因此连锁更新⼀旦发⽣,就会导致压缩列表占用的内存空间要多次重新分配,这就会直接影响到压缩列表的访问性能

所以说, 虽然压缩列表紧凑型的内存布局能节省内存开销,但是如果保存的元素数量增加了,或是元素变大 了,会导致内存重新分配,最糟糕的是会有「连锁更新」的问题
因此, 压缩列表只会用于保存的节点数量不多的场景 ,只要节点数量⾜够⼩,即使发⽣连锁更新,也是能接受的。
虽说如此, Redis 针对压缩列表在设计上的不⾜,在后来的版本中,新增设计了两种数据结构: quicklist (Redis 3.2 引⼊) 和 listpack Redis 5.0 引⼊)。这两种数据结构的设计⽬标,就是尽可能地保持压缩列表节省内存的优势,同时解决压缩列表的「连锁更新」的问题。
quicklist
quicklist 的节点是quicklistNode。 quicklistNode 结构体⾥包含了前⼀个节点和下⼀个节点指针,这样每个 quicklistNode 形成了 ⼀个双向链表。但是链表节点的元素不再是单纯保存元素值,⽽是保存了⼀个压缩列表,所以 quicklistNode 结构体⾥有个指向压缩列表的指针 *zl
我画了⼀张图,⽅便你理解 quicklist 数据结构。
在向 quicklist 添加⼀个元素的时候,不会像普通的链表那样,直接新建⼀个链表节点。⽽是会检查插⼊位置的压缩列表是否能容纳该元素,如果能容纳就直接保存到 quicklistNode 结构⾥的压缩列表,如果不能容纳,才会新建⼀个新的 quicklistNode 结构。
quicklist 会控制 quicklistNode 结构⾥的压缩列表的⼤⼩或者元素个数,来规避潜在的连锁更新的⻛险,但是这并没有完全解决连锁更新的问题。

Hash

Hash 类型的底层数据结构是由压缩列表或哈希表实现的:

  • 如果哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值,可由 hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;
  • 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的底层数据结构。

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了

listpack
quicklist 虽然通过控制 quicklistNode 结构⾥的压缩列表的大小或者元素个数,来减少连锁更新带来的性能影响,但是并没有完全解决连锁更新的问题。
因为 quicklistNode 还是⽤了压缩列表来保存元素,压缩列表连锁更新的问题,来源于它的结构设计,所以要想彻底解决这个问题,需要设计⼀个新的数据结构。
于是, Redis 5.0 新设计⼀个数据结构叫 listpack ,⽬的是替代压缩列表,它最⼤特点是 listpack 中每个节点不再包含前⼀个节点的长度了,压缩列表每个节点正因为需要保存前⼀个节点的⻓度字段,就会有连锁更新的隐患。
我看了 Redis Github ,在最新 6.2 发行版本中, Redis Hash 对象、 Set 对象的底层数据结构的压缩列 表还未被替换成 listpack ,而 Redis 的最新代码(还未发布版本)已经将所有用到压缩列表底层数据结构 Redis 对象替换成 listpack 数据结构来实现,估计不久将来, Redis 就会发布⼀个将压缩列表为 listpack 的发行版本

listpack结构设计

listpack 采⽤了压缩列表的很多优秀的设计,比如还是⽤⼀块连续的内存空间来紧凑地保存数据,并且为了节省内存的开销,listpack 节点会采⽤不同的编码⽅式保存不同大小的数据。

我们先看看 listpack 结构:

listpack 头包含两个属性,分别记录了 listpack 总字节数和元素数量,然后 listpack 末尾也有个结尾标识。图中的 listpack entry 就是 listpack 的节点了。
每个 listpack 节点结构如下:
主要包含三个⽅⾯内容:
  • encoding,定义该元素的编码类型,会对不同长度的整数和字符串进行编码
  • data,实际存放的数据;
  • lenencoding+data的总⻓度;
可以看到, listpack 没有压缩列表中记录前⼀个节点长度的字段了, listpack 只记录当前节点的长度,当 我们向 listpack 加⼊⼀个新元素的时候,不会影响其他节点的长度字段的变化,从而避免了压缩列表的连 锁更新问题
 哈希表
哈希表是⼀种保存键值对( key-value )的数据结构。
哈希表中的每⼀个 key 都是独⼀⽆⼆的,程序可以根据 key 查找到与之关联的 value ,或者通过 key 来更新 value ,⼜或者根据 key 来删除整个 key-value 等等。
在讲压缩列表的时候,提到过 Redis Hash 对象的底层实现之⼀是压缩列表(最新 Redis 代码已将压缩列表替换成 listpack )。 Hash 对象的另外⼀个底层实现就是哈希表。
哈希表优点在于,它 能以 O(1) 的复杂度快速查询数据 。怎么做到的呢?将 key 通过 Hash 函数的计算,就能定位数据在表中的位置,因为哈希表实际上是数组,所以可以通过索引值快速查询到数据。
但是存在的⻛险也是有,在哈希表⼤⼩固定的情况下,随着数据不断增多,那么 哈希冲突 的可能性也会越⾼。
解决哈希冲突的⽅式,有很多种。
Redis 采用了「链式哈希」来解决哈希冲突 ,在不扩容哈希表的前提下,将具有相同哈希值的数串起来,形成链接起,以便这些数据在表中仍然可以被查询到。
接下来,详细说说哈希表。

哈希表结构设计

Redis 的哈希表结构如下:

可以看到,哈希表是⼀个数组( dictEntry  table ),数组的每个元素是⼀个指向「哈希表节点
dictEntry )」的指针。

dictEntry 结构⾥不仅包含指向键和值的指针,还包含了指向下⼀个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对链接起来,以此来解决哈希冲突的问题,这就是链式哈希。
另外,这⾥还跟你提⼀下, dictEntry 结构⾥键值对中的值是⼀个「联合体 v 」定义的,因此,键值对中的值可以是⼀个指向实际值的指针,或者是⼀个⽆符号的 64 位整数或有符号的 64 位整数或 double 类的值。这么做的好处是可以节省内存空间,因为当「值」是整数或浮点数时,就可以将值的数据内嵌在 dictEntry结构⾥,⽆需再⽤⼀个指针指向实际的值,从⽽节省了内存空间。

哈希冲突

哈希表实际上是⼀个数组,数组⾥每⼀个元素就是⼀个哈希桶。
当⼀个键值对的键经过 Hash 函数计算后得到哈希值,再将 ( 哈希值 % 哈希表⼤⼩ ) 取模计算,得到的结果值就是该 key-value 对应的数组元素位置,也就是第⼏个哈希桶。

什么是哈希冲突呢?

举个例⼦,有⼀个可以存放 8 个哈希桶的哈希表。 key1 经过哈希函数计算后,再将「哈希值 % 8 」进⾏取模计算,结果值为 1 ,那么就对应哈希桶 1 ,类似的, key9 key10 分别对应哈希桶 1 和桶 6
此时, key1 key9 对应到了相同的哈希桶中,这就发⽣了哈希冲突。
因此, 当有两个以上数量的 key 被分配到了哈希表中同⼀个哈希桶上时,此时称这些 key 发生了冲突。

链式哈希

Redis 采⽤了「链式哈希」的⽅法来解决哈希冲突。

链式哈希是怎么实现的?
实现的⽅式就是每个哈希表节点都有⼀个 next 指针,⽤于指向下⼀个哈希表节点,因此多个哈希表节点可以⽤ next 指针构成⼀个单项链表, 被分配到同⼀个哈希桶上的多个节点可以⽤这个单项链表连接起来 ,这样就解决了哈希冲突。
还是⽤前⾯的哈希冲突例⼦, key1 key9 经过哈希计算后,都落在同⼀个哈希桶,链式哈希的话, key1就会通过 next 指针指向 key9 ,形成⼀个单向链表。
不过,链式哈希局限性也很明显,随着链表⻓度的增加,在查询这⼀位置上的数据的耗时就会增加,毕竟链表的查询的时间复杂度是 O(n)
要想解决这⼀问题,就需要进⾏ rehash ,也就是对哈希表的大小进⾏扩展。
接下来,看看 Redis 是如何实现的 rehash 的。

rehash

哈希表结构设计的这一小节,我给⼤家介绍了 Redis 使⽤ dictht 结构体表示哈希表。不过,在实际使用哈希表时,Redis 定义⼀个 dict 结构体,这个结构体⾥定义了 两个哈希表( ht[2]

之所以定义了 2 个哈希表,是因为进行  rehash 的时候,需要用上 2 个哈希表了。
在正常服务请求阶段,插⼊的数据,都会写⼊到「哈希表 1 」,此时的「哈希表 2 」 并没有被分配空间。
随着数据逐步增多,触发了 rehash 操作,这个过程分为三步:
  • 给「哈希表 2」 分配空间,⼀般会比「哈希表 1」 大 2 倍;
  • 将「哈希表 1 」的数据迁移到「哈希表 2」 中;
  • 迁移完成后,「哈希表 1 」的空间会被释放,并把「哈希表 2」 设置为「哈希表 1」,然后在「哈希表 2」 新创建⼀个空⽩的哈希表,为下次 rehash 做准备。
为了方便你理解,我把 rehash 这三个过程画在了下⾯这张图:

这个过程看起来简单,但是其实第⼆步很有问题, 如果「哈希表 1 」的数据量非常大,那么在迁移至「哈 希表 2 」的时候,因为会涉及大量的数据拷贝,此时可能会对 Redis 造成阻塞,无法服务其他请求

渐进式哈希rehash

为了避免 rehash 在数据迁移过程中,因拷贝数据的耗时,影响 Redis 性能的情况,所以 Redis 采⽤了 进式 rehash ,也就是将数据的迁移的⼯作不再是⼀次性迁移完成,⽽是分多次迁移
渐进式 rehash 步骤如下:
  • 给「哈希表 2」 分配空间;
  • rehash 进行期间,每次哈希表元素进行新增、删除、查找或者更新操作时,Redis 除了会执行对应的操作之外,还会顺序将「哈希表 1 」中索引位置上的所有 key-value 迁移到「哈希表 2」 上
  • 随着处理客户端发起的哈希表操作请求数量越多,最终在某个时间点,会把「哈希表 1 」的所有key-value 迁移到「哈希表 2」,从⽽完成 rehash 操作。
这样就巧妙地把⼀次性⼤量数据迁移⼯作的开销,分摊到了多次处理请求的过程中,避免了⼀次性 rehash的耗时操作。
在进⾏渐进式 rehash 的过程中,会有两个哈希表,所以在渐进式 rehash 进⾏期间,哈希表元素的删除、查找、更新等操作都会在这两个哈希表进⾏。
比如,查找⼀个 key 的值的话,先会在「哈希表 1 」 里面进⾏查找,如果没找到,就会继续到哈希表 2 里面 进⾏找到。另外,在渐进式 rehash 进行 期间,新增⼀个 key-value 时,会被保存到「哈希表 2 」⾥⾯,⽽「哈希表 1」 则不再进⾏任何添加操作,这样保证了「哈希表 1 」的 key-value 数量只会减少,随着 rehash 操作的完成,最终「哈希表 1 」就会变成空表。

rehash触发条件

介绍了 rehash 那么多,还没说什么时情况下会触发 rehash 操作呢?
rehash 的触发条件跟 负载因⼦( load factor 有关系。
负载因⼦可以通过下⾯这个公式计算:

触发 rehash 操作的条件,主要有两个:
  • 当负载因子大于等于 1 ,并且 Redis 没有在执行bgsave 命令或者 bgrewiteaof 命令,也就是没有执行 RDB 快照或没有进行 AOF 重写的时候,就会进行 rehash 操作。
  • 当负载因子大于等于 5 时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF重写,都会强制进⾏ rehash 操作。

Set

Set 类型的底层数据结构是由哈希表或整数集合实现的:

  • 如果集合中的元素都是整数且元素个数小于 512 (默认值,set-maxintset-entries配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;
  • 如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。
整数集合
整数集合是 Set 对象的底层实现之⼀。当⼀个 Set 对象只包含整数值元素,并且元素数量不时,就会使⽤整数集这个数据结构作为底层实现。

整数集合结构设计
整数集合本质上是⼀块连续内存空间,它的结构定义如下:
可以看到,保存元素的容器是⼀个 contents 数组,虽然 contents 被声明为 int8_t 类型的数组,但是实际上 contents 数组并不保存任何 int8_t 类型的元素, contents 数组的真正类型取决于 intset 结构体⾥的encoding 属性的值。⽐如:
  • 如果 encoding 属性值为 INTSET_E0.N0C_INT16,那么 contents 就是⼀个 int16_t 类型的数组,数组中每⼀个元素的类型都是 int16_t
  • 如果 encoding 属性值为 INTSET_ENC_INT32,那么 contents 就是⼀个 int32_t 类型的数组,数组中每⼀个元素的类型都是 int32_t
  • 如果 encoding 属性值为 INTSET_ENC_INT64,那么 contents 就是⼀个 int64_t 类型的数组,数组中每⼀个元素的类型都是 int64_t
不同类型的 contents 数组,意味着数组的⼤⼩也会不同。
整数集合的升级操作
整数集合会有⼀个升级规则,就是当我们将⼀个新元素加⼊到整数集合里面,如果新元素的类型
int32_t )⽐整数集合现有所有元素的类型( int16_t )都要长时,整数集合需要先进行升级,也就是按新元素的类型(int32_t )扩展 contents 数组的空间⼤⼩,然后才能将新元素加入到整数集合⾥,当然升级的过程中,也要维持整数集合的有序性。
整数集合升级的过程不会重新分配⼀个新类型的数组,而是在原本的数组上扩展空间,然后再将每个元素按间隔类型大小分割,如果 encoding 属性值为 INTSET_ENC_INT16 ,则每个元素的间隔就是 16 位。
举个例⼦,假设有⼀个整数集合⾥有 3 个类型为 int16_t 的元素。
现在,往这个整数集合中加⼊⼀个新元素 65535 ,这个新元素需要⽤ int32_t 类型来保存,所以整数集合要进⾏升级操作,⾸先需要为 contents 数组扩容, 在原本空间的大小之上再扩容多 80 位( 4x32- 3x16=80 ),这样就能保存下 4 个类型为 int32_t 的元素
扩容完 contents 数组空间⼤⼩后,需要将之前的三个元素转换为 int32_t 类型,并将转换后的元素放置到正确的位上⾯,并且需要维持底层数组的有序性不变,整个转换过程如下:
如果要让⼀个数组同时保存 int16_t int32_t int64_t 类型的元素,最简单做法就是直接使⽤ int64_t 类型的数组。不过这样的话,当如果元素都是 int16_t 类型的,就会造成内存浪费的情况。
整数集合升级就能避免这种情况,如果⼀直向整数集合添加 int16_t 类型的元素,那么整数集合的底层实现就⼀直是⽤ int16_t 类型的数组,只有在我们要将 int32_t 类型或 int64_t 类型的元素添加到集合时,才会对数组进⾏升级操作。0.
因此,整数集合升级的好处是 节省内存资源
整数集合⽀持降级操作吗?
不支持降级操作 ,⼀旦对数组进⾏了升级,就会⼀直保持升级后的状态。⽐如前⾯的升级操作的例⼦,如果删除了 65535 元素,整数集合的数组还是 int32_t 类型的,并不会因此降级为 int16_t 类型。

Zset

Zset 类型的底层数据结构是由压缩列表或跳表实现的:

  • 如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为Zset 类型的底层数据结构;
  • 如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。

跳表

跳表结构设计

链表在查找元素的时候,因为需要逐⼀查找,所以查询效率⾮常低,时间复杂度是 O(N) ,于是就出现了跳表。 跳表是在链表基础上改进过来的,实现了⼀种「多层」的有序链表 ,这样的好处是能快读定位数据。
那跳表长什么样呢?我这⾥举个例⼦,下图展示了⼀个层级为 3 的跳表。
图中头节点有 L0~L2 三个头指针,分别指向了不同层级的节点,然后每个层级的节点都通过指针连接起来:
  • L0 层级共有 5 个节点,分别是节点12345
  • L1 层级共有 3 个节点,分别是节点 235
  • L2 层级只有 1 个节点,也就是节点 3
如果我们要在链表中查找节点 4 这个元素,只能从头开始遍历链表,需要查找 4 次,⽽使⽤了跳表后,只需要查找 2 次就能定位到节点 4 ,因为可以在头节点直接从 L2 层级跳到节点 3 ,然后再往前遍历找到节点4。
可以看到,这个查找过程就是在多个层级上跳来跳去,最后定位到元素。当数据量很⼤时,跳表的查找复杂度就是 O(logN)
跳表节点层数设置
跳表的相邻两层的节点数量最理想的比例是 2:1 ,查找复杂度可以降低到 O(logN)
跳表的查询过程
举个例⼦,下图有个 3 层级的跳表。
如果要查找「元素: abcd ,权重: 4 」的节点,查找的过程是这样的:
  • 先从头节点的最⾼层开始,L2 指向了「元素:abc,权重:3」节点,这个节点的权重⽐要查找节点的小,所以要访问该层上的下⼀个节点;
  • 但是该层上的下⼀个节点是空节点,于是就会跳到「元素:abc,权重:3」节点的下⼀层去找,也就是 leve[1];
  • 「元素:abc,权重:3」节点的 leve[1] 的下⼀个指针指向了「元素:abcde,权重:4」的节点,然后将其和要查找的节点⽐较。虽然「元素:abcde,权重:4」的节点的权重和要查找的权重相同,但是当前节点的 SDS 类型数据「⼤于」要查找的数据,所以会继续跳到「元素:abc,权重:3」节点的下⼀层去找,也就是 leve[0]
  • 「元素:abc,权重:3」节点的 leve[0] 的下⼀个指针指向了「元素:abcd,权重:4」的节点,该节点正是要查找的节点,查询结束。

三种特殊的数据类型

Geo 地理位置

命令介绍
(1)geoadd命令:将指定的地理空间位置(纬度、经度、名称)添加到指定的key中。
格式:geoadd key 经度 纬度 地理位置
例如:geoadd china:city 116.40 39.90 beijing
有效的经度从-180度到180度。
有效的纬度从-85.05112878度到85.05112878度。
当坐标位置超出上述指定范围时,该命令将会返回下述错误。
(error) ERR invalid longitude,latitude pair 39.900000,116.400000
同时,这个命令还可以添加多个元素,例如:
geoadd china:city 116.40 39.90 beijing 121.47 32.23 shanghai

规则:两级(南极北极)是无法直接添加的,一般会下载城市数据,直接通过Java程序一次性导入

(2)geopos命令:从key里返回所有给定位置元素的位置(经度和纬度)。
格式:geopos key member
例如:geopos china:city beijing
返回值:GEOPOS 命令返回一个数组, 数组中的每个项都由两个元素组成: 第一个元素为给定位置元素的经度, 而第二个元素则为给定位置元素的纬度。
当给定的元素不存在时,对应的数组项为空值。

(3)geodist命令:返回两个给定位置之间的直线距离,如果两个位置之间的其中一个不存在,那么命令就返回空值。
格式:geodist key member1 member2 [unit]
指定单位的参数 unit 必须是以下单位的其中一个:
m 表示单位为米。
km 表示单位为千米。
mi 表示单位为英里。
ft 表示单位为英尺。
如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位。

(4)georadius命令:以给定的经纬度为中心,找出某一半径内的元素。
以给定的经纬度为中心,返回键包含的位置元素当中,与中心的距离不超过给定最大距离的所有位置元素。
格式:georadius key longitude latitude radius m|km|ft|mi
例如: georadius china:city 110 23 1000 km
key为键,longitude、latitude表示当前位置的经纬度,radius表示搜索半径,m|km|ft|mi表示单位。

上图的代码解释:
以110 23为经纬度查询方圆1000km内的城市。前提是所有的数据录入了china:city中。

Hyperloglog 基数统计

  • 什么是基数?

一个可重复集合内不重复元素的个数就是基数。

  • 简介

Redis 2.8.9版本就更新了Hyperloglog数据结构。
Redis Hyperloglog基数统计的算法。

  • 优点

占用的内存是固定的,2^64不同元素的技术,只需要费12KB内存。如果要从内存角度来比较的话,Hyperloglog是首选。

举例:网页的UV(一个人访问一个网站多次,但是还是算作一个人)

传统的方式:使用set保护用户的ID,然后就可以统计set中的元素数量作为标准判断,这个方式如果保存大量的用户id,就会比较麻烦。

我们的目的是为了计数,而不是存储用户id。

(1)pfadd key member1 member2 ....:添加一个或者多个元素
(2)pfcount key:统计key集合中基数的个数

(3)pfmerge newKey key1 key2 ...:合并key1和key2中的元素,并且剔除其中重复的元素,产生新的集合newKey。

key1中的元素有:a b c d e f g
key2中的元素有:h i j a e q n m c
重复的元素有:a e c
产生的newKey中的元素有:a b c d e f g h i j q n m


应用场景:适合做页面统计。
如果允许容错,那么一定要使用Hyperloglog;
如果不允许容错,就使用set或者自己的数据类型即可。
(容错就是当由于种种原因在系统中出现了数据、文件损坏或丢失时,系统能够自动将这些损坏或丢失的文件和数据恢复到发生事故以前的状态,使系统能够连续正常运行的一种技术,很简单的意思)

Bitmap位图

  • 概括

Bitmap是位图,数据结构,都是操作二进制位来进行记录,就只有0和1两个状态。
365天 = 365bit 1字节 = 8bit 大约45个字节左右
基本命令
(1)setbit key offset value:value只能是0或者1,如下例子所示:

使用bitmap来记录周一到周日的打卡。
上图中的offeset表示周几,value的0表示未打卡,1表示打开成功。
周一0:1,周二1:0,周三2:1,周四3:1,…

2)getbit key offset:获取key中的offset值,查看某一天是否打卡

以上表示的是查看周四是否打卡,周日是否打卡。

(3)bitcount key:统计操作,统计打开的天数,相当于统计sign中value为1的元素个数

应用场景
统计用户信息、活跃、不活跃、登录、未登录、打卡等等… 两个状态的都可以使用Bitmap。

在生活或者开发中都有十分多的应用场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

编程小猹

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

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

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

打赏作者

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

抵扣说明:

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

余额充值