4、redis基础系列—数据结构

Redis数据库里面每个键值对(key-value pair)都是由对象(object)组成的,其中:

  • 数据库键总是一个字符串对象(string object);
  • 数据库键的值则可以是字符串对象、列表对象(list object)、哈希对象(hash object)、集合对象(set object)、有序集合对象(sorted set object)五种对象中的一种;

当我们聊redis数据结构的时候,应该关注那些问题?

  • 数据库键的值的底层数据结构有哪些?sds、链表、字典、跳跃表、整数集合、压缩列表、对象、位图、geo
  • 底层数据结构的使用场景、功能和性能是什么样的?

常用数据结构

1、SDS简单动态字符串

数据结构&特性

每个sds.h/sdshdr结构表示一个SDS值:

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

4、redis基础系列—数据结构_第1张图片

  • free表示未使用空间,0未分配任何未使用空间;

  • len已经使用空间,标识一个7字节长的字符串;

  • buf是一个char类型数组,存放字符串对应字符,最后一位保存空字符'\0';

相比C字符串特性

  • SDS的len属性记录了SDS本身长度,所以获取SDS字符串长度的时间复杂度为O(1),而C字符串要遍历整个字符串找到'\0'结尾,时间复杂度为O(n);

  • C字符串不记录自身长度,还容易造成缓冲区溢出;SDS的空间分配策略杜绝了这种情况发生:SDS API需要对SDS修改时会检测空间是否满足要求,不满足会自动扩展,然后才执行修改操作;

  • SDS会进行空间预分配,空间扩展时,除了分配必要空间,还会分配一部分未使用空间free,避免频繁扩展;

  • SDS空间空闲不会立刻释放,使用free记录,等待将来使用,惰性释放,避免频繁释放;

  • SDS二进制安全、兼容部分C函数;

2、链表

数据结构&特性

redis链表提供了顺序的节点访问能力,增删改节点灵活;作为列表键、发布订阅、慢查询、监视器等功能的底层实现;

链表由链表节点和列表两个结构:

typedef struct listNode{
    //前置节点
    struct listNode * prev;
    //后置节点
    struct listNode * next;
    //节点值
    void * value;
}listNode;
  • 链表节点通过prev和next指针组成双端列表;

typedef struct list{
    // 表头节点
    listNode * head;
    // 表尾节点
    listNode * tail;
    // 链表包含节点数量
    unsigned long len;
    // 节点值复制函数
    void *(*dup)(void *ptr);
    // 节点值释放函数
    void (*free) (void *ptr);
    // 节点值对比函数
    void (*match) (void * prt,void *key); 
}list;
  • 双端、无环、带表头节点、表尾结点、带链表长度计数器、多态(使用void*指针保存节点值,并通过list结构的dup\free\match三个属性为节点值设置特定类型的函数);

3、字典

redis提供字典结构来保存键值对的数据;作为哈希键(哈希键的键值比较多或者键值对中的元素都是比较长的字符串)、Redis数据库的底层数据结构;

数据结构&特性

typedef struct dictht{
    //哈希表数组
    dictEntry **table;
    //哈希表大小
    unsigned long size;
    //哈希表大小掩码,用于计算索引值,总是等于size-1
    unsigned long sizemask;
    //该哈希表已有节点数量
    unsigned long used;
}dictht;
  • table是一个数组,每个元素指向dict.h/dictEntry结构的指针,每个dictEntry结构保存着一个键值对。

  • size记录哈希表的大小,数组的大小;sizemask总是等于size-1,这个属性和哈希值一起决定一个键应该被放到table数组的那个索引上面。

typedef struct dictEntry{
    //键
    void *key;
    //值
    union{
        void *val;
        uint64_tu64;
        int64_ts64;
    }v;
    //指向下个哈希表节点,形成链表
    struct dictEntry *next;
}dictEntry;
typedef struct dict{
    //类型特定函数
    dictType *type;
    //私有数据
    void *privatedata;
    //哈希表
    dictht ht[2];
    //rehash索引,rehash不进行时,值为-1;
    in trehashidx;   
}dict;
  • 根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组指定的索引上面。

  • 每个哈希表使用链地址法来解决哈希冲突;因为链表没有指向链表表尾的指针,所以为了速度考虑,程序总是将新节点添加到链表的表头位置(时间复杂度为O(1));

  • 扩展或收缩哈希表的操作可以由rehash(重新散列)完成;redis采用渐进式的rehash操作,是一种分而治之的思想,将rehash键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash带来的庞大计算量。rehash的过程中,字典会同时使用ht[0]和ht[1]两个哈希表,所以期间字典的增删改查会在两个哈希表上进行。例如查找是会现在ht[0]中查找,查找不到再去ht[1]中查;期间新添加的键值对一律会被保存到ht[1]里面,ht[0]不再进行任何添加操作,保证ht[0]包含键值对只减不增,并随着rehash进行最终变为空表。

4、跳跃表

是一种有序数据结构,每个节点维护多个指向其他节点的指针,从而达到快速访问节点的目的;平均O(logN)、最坏O(N)复杂度的节点查找;作为有序集合键(包含元素较多或元素成员为较长字符串)、集群节点 的底层实现;

数据结构&特性

跳跃表节点zskiplistNode和跳跃表zskiplist两个结构

4、redis基础系列—数据结构_第2张图片

  • header:指向跳跃表的表头节点;

  • tail:指向跳跃表的表位节点;

  • level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内);L1\L2\L3的字样代表各个层,每个层有两个属性,前进指针和跨度;前进指针用于访问位于表尾方向的其他节点,跨度用于记录前进指针指向节点和当前节点的距离。箭头连线上的数自即为跨度。当程序遍历时,访问会沿着层的前进指针进行;

  • length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内);

typedef struct zskiplistNode{
    //层
    struct zskiplistLevel{
        //前进指针
        struct zskiplistNode *forward;
        //跨度
        unsigned int span;
    }level[];
    //后退指针
    struct zskiplistNode *backward;
    //分值
    double score;
    //成员对象
    robj *obj;
}zskiplistNode;

zskiplistNode包含以下属性:

  • 层:跨度和前进指针组成,通过层来加快访问其他节点的速度,层数越多,访问其他节点的速度越快。每次创建一个新节点,程序根据幂次定律随机生成一个1到32之间的值作为level数组的大小,这个大小就是层的高度。

  • 后退指针:节点中BW字样的标记节点,指向前一个节点,用于表尾向表头遍历;

  • 分值:各节点中1.0、2.0、3.0是节点保存的分值。在跳跃表中,节点按照各节点所保存的分值从小到大排列;分值相同则按成员对象在字典序中的大小进行排序;

  • 成员对象:各个节点中的o1\o2和o3是节点锁保存的成员对象;

typedef struct zskiplist{
    //表头节点和表尾节点
    structz skiplistNode *header,*tail;
    //表中节点的数量
    unsigned long length;
    //表中层数最大的节点的层数
    int level;
}zskiplist;
  • header和tail指针分别指向跳跃表的表头节点和表尾节点,通过这两个指针,程序定位表头节点和表尾节点的时间复杂度为O(1)

  • 通过length属性记录节点数量,O(1)复杂度返回跳跃表长度;

  • level属性用于O(1)复杂度内获取跳跃表中层数高数最大的节点的层数量,表头节点的层高并不计算在内;

5、整数集合

intset整数集合用于保存整数值的集合,并且整数值不多,可以保存int16_t、int32_t或者int64_t的整数值。并且保存整数集合不会出现重复。

数据结构&特性

typedef struct intset{
    // 编码方式
    uint32_t encoding;
    //集合包含元素数量
    uint32_t length;
    //保存元素的数组
    int8_t contents[];
}intset;

6、压缩列表

当列表键和哈希键包含少量元素,并且每项要么是小整数值,要么是长度较短的字符串,redis就使用压缩列表作为底层数据结构实现。由一系列特特殊编码的连续内存块组成顺序型数据结构。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值;

数据结构&特性

压缩列表和压缩列表节点组成

  • zlbytes:表示压缩列表的总长度

  • zltail:标识如果我们有一个指向压缩列表起始地址的指针p,那么只要用指针p加上偏移量x,就可以计算出表尾几点entryN的地址;

  • zllen:表示压缩列表节点数

  • previoids_entry_length:表示压缩列表中前一个节点的长度。previous_entry_length字节或5字节;用于计算前一个节点的起止地址;连锁更新时,需要重新分配压缩列表节点的字节数为5;但性能可接受

7、Redis位图Bitmap

通过一个bit位来表示某个元素对应的值或者状态,其中的key就是对应元素本身,value对应0或1,我们知道8个bit可以组成一个Byte,所以bitmap本身会极大的节省储存空间。

Redis从2.2.0版本开始新增了setbit、getbit、bitcount等几个bitmap相关命令。虽然是新命令,但是并没有新增新的数据类型,因为setbit等命令只不过是在set上的扩展。

应用场景一:位图计数统计

位图计数统计的是bitmap中值为1的位的个数。位图计数的效率很高,例如,一个bitmap包含10亿个位,90%的位都置为1,在一台MacBook Pro上对其做位图计数需要21.1ms。

4、redis基础系列—数据结构_第3张图片

例子:日活跃用户

为了统计今日登录的用户数,我们建立了一个bitmap,每一位标识一个用户ID。当某个用户访问我们的网页或执行了某个操作,就在bitmap中把标识此用户的位置为1。

4、redis基础系列—数据结构_第4张图片

每次用户登录时会执行一次redis.setbit(daily_active_users, user_id, 1)。将bitmap中对应位置的位置为1,时间复杂度是O(1)。统计bitmap结果显示有今天有9个用户登录。Bitmap的key是daily_active_users,它的值是1011110100100101。

因为日活跃用户每天都变化,所以需要每天创建一个新的bitmap。我们简单地把日期添加到key后面,实现了这个功能。例如要统计某一天有多少个用户访问,可以把这个bitmap的key设计为daily_active_users:2019-03-27。当用户访问进来,我们只是简单地在bitmap中把标识这个用户的位置为1,时间复杂度是O(1)。

8、Geo地理位置

redis目前已经到了3.2版本,3.2版本里面新增的一个功能就是对GEO(地理位置)的支持。

使用案例:

  • 命令:GEOADD key longitude latitude member [longitude latitude member ...]
  • 命令描述:将指定的地理空间位置(纬度、经度、名称)添加到指定的key中。
  • 返回值:添加到sorted set元素的数目,但不包括已更新score的元素。

4、redis基础系列—数据结构_第5张图片

具体参考:https://www.cnblogs.com/simibaba/p/7090350.html

你可能感兴趣的:(redis相关那些事儿)