透视Redis的常用数据类型

536 阅读10分钟

Redis的数据结构

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication)LUA脚本(Lua scripting), LRU驱动事件(LRU eviction)事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。 其中主要的的有五大数据类型:String、List、Set、Hash 和 Zset,外加常用的 bitmaps、hyperloglogs 和 geospatial等三种类型。

1. String 字符串

  • String是Redis最基本的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value。
  • String类型是二进制安全的。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象。
  • String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M

1.1 SDS 数据结构

String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。

struct sdshdr {  
    //记录buf数组中已使用字节的数量  
    //等于SDS所保存字符串的长度  
    unsigned int len;  
  
    //记录buf数组中未使用字节的数量  
    unsigned int free;  
  
    //char数组,用于保存字符串  
    char buf[];  
};

image.png 如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。

2. List 列表

Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。

2.1 quickList 数据结构

  • List的底层实际是个双向链表即快速链表quickList,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。

  • 首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成quicklist。

  • 因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。

image.png Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

3. Set 集合

Set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。

3.1 dict字典 数据结构

  • Set的数据结构是dict字典,字典是用哈希表实现的。它是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)
  • Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值。

4. Hash 哈希

  • Hash 是一个键值对集合。
  • Hash特别适合用于存储对象。类似Java里面的Map<String,Object>用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储。

image.png

对象的存储方式

  1. 每次修改用户的某个属性需要,先反序列化改好后再序列化回去。开销较大。
  2. 用户ID数据冗余,key存用户id+字段,value存字段数据

通过 key(用户 ID) + field( 属性标签 ) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题

4.1 ziplist/hashtable 数据结构

Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。

5. Zset (sorted set) 有序集合

zset与set非常相似,是一个没有重复元素的字符串集合。不同之处是有序集合的每个成员都关联了一个评分(score),score被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。因为元素是有序的, 所以你也可以很快的根据score或者次序(position)来获取一个范围的元素。访问zset的中间元素也是非常快的,因此你能够使用zset作为一个没有重复成员的智能列表。

5.1 hash+跳表 数据结构

SortedSet(zset)是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map<String, Double>,可以给每一个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。

zset底层使用了两个数据结构

(1)hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。

(2)跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。

5.1.1 跳表的插入和查询

/* ZSETs use a specialized version of Skiplists */  
typedefstruct zskiplistNode {  
// value  
sds ele;  
// 分值  
double score;  
// 后退指针  
struct zskiplistNode *backward;  
// 层  
struct zskiplistLevel {  
// 前进指针  
struct zskiplistNode *forward;  
// 跨度  
unsignedlong span;  
} level[];  
} zskiplistNode;  
  
typedefstruct zskiplist {  
// 跳跃表头指针  
struct zskiplistNode *header, *tail;  
// 表中节点的数量  
unsignedlong length;  
// 表中层数最大的节点的层数  
int level;  
} zskiplist;

跳跃表 skiplist 就是受到这种多层链表结构的启发而设计出来的。查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到 O(logn) 。但是为了避免插入和删除时维持2:1对应关系导致时间复杂度重新蜕化成 O(n) 这一问题,它不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是 为每个节点随机出一个层数(level) 。比如,一个节点随机出的层数是 3,那么就把它链入到第 1 层到第 3 层这三层链表中。

  • 每一个节点的层数(level)是随机出来的,而且新插入一个节点并不会影响到其他节点的层数,因此,插入操作只需要修改节点前后的指针,而不需要对多个节点都进行调整,这就降低了插入操作的复杂度。 image.png

具体有关跳表的查询路径可以参考下图

image.png

6.1 BitMap 位图

记录某一用户在一年中每天是否有登录系统这一需求该如何完成呢?如果使用KV存储,每个用户需要记录365个,当用户量上亿时,这所需要的存储空间是惊人的。

BitMap 这一数据结构,可为每个用户每天的登录记录只占据一位,365天就是365位,仅仅需要46字节就可存储,极大地节约了存储空间。

  • 位图数据结构其实并不是一个全新的玩意,我们可以简单的认为就是个数组,只是里面的内容只能为0或1而已(二进制位数组)。

6.1.1 SDS 数据结构

String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态,你可以把 Bitmap 看作是一个 bit 数组。

image.png

6.1.2 常用指令

  • SETBIT:为位数组指定偏移量上的二进制位设置值,偏移量从0开始计数,二进制位的值只能为0或1。返回原位置值。
  • GETBIT:获取指定偏移量上二进制位的值。
  • BITCOUNT:统计位数组中值为1的二进制位数量。
  • BITOP:对多个位数组进行按位与、或、异或运算。

7.1 hyperloglogs 基数统计

hyperloglogs 可以非常省内存的去统计各种计数,比如注册 IP 数、每日访问 IP 数、页面实时UV、在线用户数,共同好友数等。

7.1.1 稀疏存储 数据结构

image.png

7.1.2 常用指令

  • PFADD:增加
  • PFCOUNT:计数

8.1 geospatial 地理位置

Geospatial 是 Redis 3.2 版本新增的数据类型,主要用于存储地理位置信息,并对存储的信息进行操作。

GEO 本身并没有设计新的底层数据结构,而是直接使用了 Sorted Set 集合类型。 GEO 类型使用 GeoHash 编码方法实现了经纬度到 Sorted Set 中元素权重分数的转换,这其中的两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为 Sorted Set 元素的权重分数。 这样一来,我们就可以把经纬度保存到 Sorted Set 中,利用 Sorted Set 提供的“按权重进行有序范围查找”的特性,实现 LBS 服务中频繁使用的“搜索附近”的需求。

8.1.1 常用指令

  • GEOADD:将指定的地理空间位置(纬度、经度、名称)添加到指定的key中。
  • GEODIST:返回两个给定位置之间的距离,如果两个位置之间的其中一个不存在, 那么命令返回空值。
  • GEOPOS:从key里返回所有给定位置元素的位置(经度和纬度)。
  • GEOHASH:返回一个或多个位置元素的 Geohash 表示。
  • GEORADIUS:以给定的经纬度为中心, 返回键包含的位置元素当中,与中心的距离不超过给定最大距离的所有位置元素。
  • GEORADIUSBYMEMBER:这个命令和 GEORADIUS 命令一样, 都可以找出位于指定范围内的元素, 但是 GEORADIUSBYMEMBER 的中心点是由给定的位置元素决定的, 而不是像 GEORADIUS 那样, 使用输入的经度和纬度来决定中心点指定成员的位置被用作查询的中心。

拓展文章

图解 Redis 数据结构 (qq.com)

面试杀手锏:Redis源码之SDS (qq.com)

面试杀手锏:Redis源码之BitMap (qq.com)

Reids—神奇的HyperLoglog解决统计问题 (qq.com)

Redis—跳跃表 (qq.com)