Redis 包含五种数据类型,分别为String、List、Hash、Set、ZSet
底层实现的数据结构包SDS、双向链表、压缩列表、哈希表、整数集合、跳表
- redis结构图
- 数据类型和数据结构的关系
Redis六种数据结构
一、动态字符串(SDS)
Redis 是用 C 语言实现的,但是它没有直接使用 C 语言的 char* 字符数组来实现字符串,而是自己封装了一个名为简单动态字符串(simple dynamic string,SDS) 的数据结构来表示字符串,也就是 Redis 的 String 数据类型的底层数据结构是 SDS
总的来说,Redis 的 SDS 结构在原本字符数组之上,增加了三个元数据:len、alloc、flags,用来解决 C 语言字符串的缺陷,之所以 SDS 设计不同类型的结构体,是为了能灵活保存不同大小的字符串,从而有效节省内存空间。比如,在保存小字符串时,结构头占用空间也比较少
优点:
- 获取字符串长度复杂度:C 语言的字符串长度获取 strlen 函数,复杂度是O(n),而 Redis 的 SDS 结构因为加入了 len 成员变量,所以是O(1)
- 二进制安全:因为 SDS 不需要用 “\0” 字符来标识字符串结尾了
- 不会发生缓冲区溢出:C 语言的字符串标准库提供的字符串操作函数,大多数(比如 strcat 追加字符串函数)都是不安全的,Redis 的 SDS 结构里引入了 alloc 和 leb 成员变量,这样 SDS API 通过 alloc - len 计算,可以算出剩余可用的空间大小,这样在对字符串做修改操作的时候,就可以由程序内部判断缓冲区大小是否足够用
- 节省内存空间:SDS 结构中有个 flags 成员变量,表示的是 SDS 类型,之所以 SDS 设计不同类型的结构体,是为了能灵活保存不同大小的字符串,从而有效节省内存空间。比如,在保存小字符串时,结构头占用空间也比较少
二、链表(linkedlist)
list 结构为链表提供了链表头指针 head、链表尾节点 tail、链表节点数量 len、以及可以自定义实现的 dup、free、match 函数
三、压缩列表(ziplist)
压缩列表是 Redis 为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组
当我们往压缩列表中插入数据时,压缩列表 就会根据数据是字符串还是整数,以及它们的大小会在 prevlen 和 encoding 这两个元素里保存不同的信息,这种根据数据大小进行对应信息保存的设计思想,正是 Redis 为了节省内存而采用的
压缩列表除了查找复杂度高的问题,压缩列表在插入元素时,如果内存空间不够了,压缩列表还需要重新分配一块连续的内存空间,而这可能会引发连锁更新的问题
压缩列表里的每个节点中的 prevlen 属性都记录了「前一个节点的长度」,而且 prevlen 属性的空间大小跟前一个节点长度值有关,比如:
- 如果前一个
节点的长度小于 254 字节,那么 prevlen 属性需要用 1 字节的空间来保存这个长度值;
- 如果前一个
节点的长度大于等于 254 字节,那么 prevlen 属性需要用 5 字节的空间来保存这个长度值
这种在特殊情况下产生的连续多次空间扩展操作就叫做「连锁更新」,连锁更新一旦发生,就会导致压缩列表 占用的内存空间要多次重新分配,这就会直接影响到压缩列表的访问性能
四、哈希表(hash)
Hash 表优点在于,它能以 O(1) 的复杂度快速查询数据。主要是通过 Hash 函数的计算,就能定位数据在表中的位置,紧接着可以对数据进行操作,这就使得数据操作非常快
但是存在的风险也是有,在哈希表大小固定的情况下,随着数据不断增多,那么哈希冲突的可能性也会越高,Redis 采用了链式哈希来解决哈希冲突,以及rehash
为了避免 rehash 在数据迁移过程中,因拷贝数据的耗时,影响 Redis 性能的情况,所以 Redis 采用了渐进式 rehash,也就是将数据的迁移的工作不再是一次性迁移完成,而是分多次迁移
触发 rehash 操作的条件,主要有两个:
- 当负载因子大于等于 1 ,并且 Redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令,也就是没有执行 RDB 快照或没有进行 AOF 重写的时候,就会进行 rehash 操作。
- 当负载因子大于等于 5 时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF 重写,都会强制进行 rehash 操作
五、跳表(skiplist)
有序列表 zset 的数据结构,它类似于 Java 中的 SortedSet 和 HashMap 的结合体,一方面它是一个 set 保证了内部 value 的唯一性,另一方面又可以给每个 value 赋予一个排序的权重值 score,来达到 排序 的目的
因为 zset 要支持随机的插入和删除,所以它 不宜使用数组来实现,关于排序问题,我们也很容易就想到 红黑树/ 平衡树 这样的树形结构,为什么 Redis 不使用这样一些结构呢
- 性能考虑:在高并发的情况下,树形结构需要执行一些类似于 rebalance 这样的可能涉及整棵树的操作,相对来说跳跃表的变化只涉及局部
- 实现考虑:在复杂度与红黑树相同的情况下,跳跃表实现起来更简单,看起来也更加直观;
跳跃表 skiplist 就是受到这种多层链表结构的启发而设计出来的。按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到 O(logn)
这种方法在插入数据的时候有很大的问题。新插入一个节点之后,就会打乱上下相邻两层链表上节点个数严格的 2:1 的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点 (也包括新插入的节点) 重新进行调整,这会让时间复杂度重新蜕化成 O(n)。删除数据也有同样的问题
skiplist 为了避免这一问题,它不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是 为每个节点随机出一个层数(level)。从上面的创建和插入的过程中可以看出,每一个节点的层数(level)是随机出来的,而且新插入一个节点并不会影响到其他节点的层数,因此,插入操作只需要修改节点前后的指针,而不需要对多个节点都进行调整,这就降低了插入操作的复杂度。
六、整数集合(intset)
intset实质就是一个有序数组,存储元素紧密,空间利用率高,并通过二分法降低查找元素的时间复杂度,而且不容易因频繁地插入删除而产生内存碎片
支持整型编码,intset中所有数据元素的存储类型是一致的。新插入数据时,如果数据的类型大于当前intset的数据类型,为了防止溢出,会对其进行升级操作,然后才能将新元素添加到整数集合里
Intset 只支持升级,不支持降级
升级会引起整个 intset 进行内存重分配,并移动集合中的所有元素,这个操作的复杂度为O(n)
升级整数集合:
1)根据新元素的类型,拓展整数集合底层数组的空间大小,并且为新元素分配空间。
2)将底层数组现有的元素都转成新原属相同的类型,并且将转换后的元素放置到正确的位上,而且放置元素的过程中,需要继续位置数组的有序性质不变。
3)将新元素加入到底层数组里面
Redis五种基本数据类型
一、String
字符串对象的编码可以是int(整数 可以用long类型)、raw(超过39字节)、embstr(小于等于39字节)
raw编码会调用两次内存分配函数来创建redisObject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的内存,空间依次包含redisObject和sdshdr结构
- 应用场景
- 计数器:incr操作用来计数
- 缓存:缓存普通信息
- 常用API
set [key] [value] 给指定key设置值
get [key] 获取指定key 的值
setex [key] [time] [value] 等价于 set + expire 命令组合
expire [key] [time] 给指定key 设置过期时间 单位秒
exists [key] 判断是否存在指定key
mset [key1] [value1] [key2] [value2] ...... 批量存键值对
mget [key1] [key2] ...... 批量取key
incr [key] 如果value为整数 可用 incr命令每次自增1
incrby [key] [number] 使用incrby命令对整数值 进行增加 number
二、Hash
Redis 散列可以存储多个键值对之间的映射。和字符串一样,散列存储的值既可以是字符串又可以是数值,并且用户同样可以对散列存储的数字值执行自增或自减操作。这个和 Java 的 HashMap 很像,每个 HashMap 有自己的名字,同时可以存储多个 k/v 对
- 使用场景
- Hash更适合存储结构化的数据:存储对象的每个属性
- 购物车场景:hset [key] [field] [value] 存储购物车的三个要素
- 用户已读:key:uid field:mid value:时间戳
- 常用API
hset [key] [field] [value] 新建字段信息
hget [key] [field] 获取字段信息
hgetall [key] 获取指定key 字典里的所有字段和值
hmset [key] [field1] [value1] [field2] [value2] ...... 批量创建
三、List
有序可重复列表,编码可以是ziplist、linkedlist,列表对象保存的所有字符串元素的长度都小于 64 字节并且保存的元素数量小于 512 个,使用 ziplist 编码;否则使用 linkedlist
- 使用场景
- 消息队列:rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试
- 可以用来实现粉丝/点赞列表,通过rpush插入数据,然后使用lrange命令读取最新的元素列表
- 常用API
rpush [key] [value1] [value2] ...... 链表右侧插入
rpop [key] 移除右侧列表头元素,并返回该元素
lpop [key] 移除左侧列表头元素,并返回该元素
llen [key] 返回该列表的元素个数
lrange [key] [start_index] [end_index] 获取list 区间内的所有元素 (时间复杂度为 O(n))
四、Set
Redis 的set和list都可以存储多个字符串,他们之间的不同之处在于,list是有序可重复,而set是无序不可重复
- 使用场景
- 业务场景用户白名单:点赞、投稿
- 业务失败兜底留存:用户打赏失败后记录信息
- 常用API
sadd [key] [value] 向指定key的set中添加元素
smembers [key] 获取指定key 集合中的所有元素
scard [key] 获取集合的长度
srem [key] [value] 删除指定元素
五、SortSet
zset也叫SortedSet一方面它是个 set ,保证了内部 value 的唯一性,另方面它可以给每个 value 赋予一个score,代表这个value的排序权重。它的内部实现用的是一种叫作“跳跃列表”的数据结构
- 使用场景
- 排行榜:score为热度值或者点赞数等 飙升榜
- 带权重的消息队列:重要的消息 score 大一些,普通消息 score 小一些,可以实现优先级高的任务先执行
- 投稿审核优先级队列:score为mediaId uuid 发号器,越大说明最新
- 常用API
zadd [key] [score] [value] 向指定key的集合中增加元素
zrem [key] [value] 删除元素
zrange [key] [start_index] [end_index] 获取下标范围内的元素列表,按score 排序输出
zrevrange [key] [start_index] [end_index] 获取范围内的元素列表 ,按score排序 逆序输出
zrangebyscore [key] [score1] [score2] 输出score范围内的元素列表
zcard [key] 获取集合列表的元素个数
zscore [key] [value] 获取元素的score