Redis——第1-7章:数据结构

数据结构

  • 简单动态字符串(SDS)
    • 优点:O(1)获取长度;空间预分配,惰性空间释放
  • 链表
  • 字典
    • 渐进式rehash
  • 跳跃表
  • 整数集合
    • 升级
  • 压缩列表
    • 连锁更新带来的问题

1. SDS(Simple Dynamic String)

3.2版本之前一种结构,3.2开始5种数据结构

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

//3.2版本
sdshdr5:32位 4B
sdshdr8:256位 32B
sdshdr16:64kB
sdshdr32:4G

以sdshdr8为例:
len:记录当前已使用的字节数(不包括'\0'),获取SDS长度的复杂度为O(1)
alloc:记录当前字节数组总共分配的字节数量(不包括'\0')
flags:标记当前字节数组的属性,是sdshdr8还是sdshdr16等,flags值的定义可以看下面代码
buf:字节数组,用于保存字符串,包括结尾空白字符'\0'

// flags值定义
#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4

(1)优点

  • 常数复杂度O(1)获取字符串长度:C语言需要遍历buf获取长度
  • 杜绝缓冲区溢出:SDS 在拼接字符串之前会检查 SDS 空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题
  • 减少了修改字符串时带来的内存重分配次数
    • 对于C语言,修改C字符串,需要对C字符串数组进行一次内存重分配操作。而Redis使用场景,需要频繁修改字符串,C的方式会造成性能影响
    • 为避免C的缺陷,在SDS中,buf长度不一定就是字符数量加一,数组里面可以包含未使用的字节,记录在free属性中
    • 通过free属性,SDS实现了空间预分配惰性空间释放两种优化空间的分配策略

空间欲分配

当SDS需要被修改增长时,程序会提前分配额外的空间,为之后扩展作预留

  • 当len<1MB时,程序预分配和len同样大小的空间
    • 例如:修改之后SDS的len变成13个字节,程序也会预分配13个字节的未使用空间,总共13+13+1=27(额外1字节用于保存空字符)
  • 当len>=1MB时,程序预分配1MB的未使用空间
    • 例如:修改之后的len=30MB,实际长度 30M + 1M = 31MB
惰性空间释放当SDS需要被修改缩短时,不会立刻释放内存空间。而是先用free属性记录起来,等待将来使用
  • SDS的API是二进制安全的
    • 由于C语言中空字符代表会被认为是字符串结尾,因此C字符串只能保存文本,不能保存图片、音频、视频这样的二进制数据
    • 而SDS的API都会以处理二进制的方式来处理存放在buf数组里的数据,不会对里面的数据做任何的限制。SDS使用len属性的值来判断字符串是否结束,而不是空字符。
  • 兼容部分C字符串函数

2.链表

  • 链表(List):包括 链表头指针head,链表尾指针tail,链表长度len,其他三个临时值用于实现函数(dup,free,match)
  • 节点(ListNode):listNode中包括(前节点指针,后节点指针,节点值)

//链表中的节点定义
typedef struct listNode {
    // 前置节点
    struct listNode *prev;
    // 后置节点
    struct listNode *next;
    // 节点值
    void *value;
} listNode;

//链表定义
typedef struct list {
    // 链表头节点
    listNode *head;
    // 链表尾节点
    listNode *tail;
    // 节点值复制函数
    void *(*dup)(void *ptr);
    // 节点值释放函数
    void (*free)(void *ptr);
    // 节点值对比函数
    int (*match)(void *ptr, void *key);
    // 链表所包含的节点数量
    unsigned long len;
} list;

3.字典

由于C语言并没有内置字典的数据结构,因此Redis构建了自己的字典实现

(1)数据结构

一共三种数据结构(字典,哈希表,哈希节点),结构类似Hashmap,采用数组+链表

  • dictEntry:哈希节点(类似JAVA中Entry)
    • key:节点键
    • value:节点值
    • 下个节点指针:指向下个节点
  • dictht:哈希表
    • size:哈希表大小,即table数组的大小
    • used:已有节点(key-value对)的数量
    • sizemask:总是等于size-1
  • dict:字典结构
    • type:指向dictType(存储与字典操作有关的函数)的指针,为创建多态字典而设置
    • privdata:用于传递dictType的参数args,可选参数
    • ht[2]:包含了两个dictht的数组,指向两个dictht用于扩容
      • 一般情况下,只使用ht[0],ht[1]在h[0]进行rehash时使用
    • rehashidx:记录rehash目前的进度
      • 如果目前没有进行rehash,rehashidx=-1;正在进行时rehashidx>=0

​​​​​​

typedef struct dictht {
    // 哈希表数组
    dictEntry **table;
    // 哈希表大小
    unsigned long size;
    // 哈希表大小掩码,用于计算索引值,等于size-1
    unsigned long sizemask;
    // 哈希表已有节点的数量
    unsigned long used;
} dictht;

typedef struct dictEntry {
    // 键
    void *key;
    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    // 指向下一个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;

typedef struct dict {
    // 和类型相关的处理函数
    dictType *type;
    // 私有数据
    void *privdata;
    // 哈希表
    dictht ht[2];
    // rehash 索引,当rehash不再进行时,值为-1
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    // 迭代器数量
    unsigned long iterators; /* number of iterators currently running */
} dict;

(2)​​哈希算法

  • 索引计算:先通过dict->type->hashFunction()计算哈希值,再与dictht中的sizemask作&操作得到数组下标
  • 插入方法:拉链头插法
  • 渐进式rehash
    • rehash条件
      • 服务器目前没有执行bgsave或bgrewriteof:负载因子>=1时触发
      • 服务器目前正在执行bgsave或bgrewriteof:负载因子>=5时触发
      • 当负载因子<0.1时,自动开始收缩
      • 负载因子:比如数组table长度为32,但是一共使用了48个Entry,则负载因子是:48/32 = 1.5
    • rehash扩容大小
      • 自动扩展到>size的2的n次方。eg.used=17时候,扩展到32。
    • 渐进式rehash
      • 步骤:rehash并不是一次性完成的,而是多次完成。
        • 为ht[1]分配空间
        • 将rehashidx设置为0,表示rehash开始
        • 每次对字典的增删改查都在ht[1]上进行,不在更改原来的ht[0]
        • 增删改查的同时,顺带将ht[0]在rehashidx索引上的所有键值对rehash到ht[1]。每次结束后rehashidx++
        • 全部完成后,rehashidx再置为-1
        • 如果这期间,有查询操作,先从ht[0]查找,找不到再去ht[1]中查找
      • 优缺点
        • 优点:如果键值对有无限个,一次性rehash()会导致数据库停止服务,避免了redis阻塞
        • 缺点:由于需要新分配ht[1],如果原本就很大,会导致内存量暴增,导致大量key被驱逐

4.整数集合

整数集合是redis用于保存整数值的数据结构

(1)数据结构

intset结构体:

  • length:元素数量
  • encoding:表示数组里面的类型int16/32/64
  • contents:从小到大排好序的数组,且数组中不包含任何重复项

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

升级:如果此时encoding=int16,插入了一个int32类型的数,就会对所有数进行升级,都变成int32。

具体方法:计算出需要增加的空间大小,从后往前移到新位置

不支持降级


5.压缩列表

(1)数据结构:ziplist和entry组成

  • ziplist:一个压缩列表包含多个节点entry,每个节点可以保存一个字节数组或者一个整数值
    • zlbytes:记录整个压缩列表占用的内存字节数。
    • zltail:第一个entry1到最后一个entry的偏移量。eg.如果知道第一个entry的指针,可以计算出末节点的地址
    • zllen:包含的entry节点数量,但该属性值小于UINT16_MAX(65535)时,该值就是压缩列表的节点数量,否则需要遍历整个压缩列表才能计算出真实的节点数量
    • zlend:特殊值0xFF(十进制255),用于标记压缩列表的末端

  • entry
    • previous_entry_length:记录压缩列表前一个字节的长度(1字节或者5字节),可以根据这个计算出前一个entry的地址
      • 如果前一个entry<254字节,则用1个字节保存前一个entry长度
      • 如果前一个entry>=254字节,则用5个字节保存前一个entry长度,最高位是0xFE
    • encoding:节点的encoding保存的是节点的content的内容类型
      • 最高位是00,01,10表示数组
      • 最高位是11表示整数
    • content:content区域用于保存节点的内容,节点内容类型和长度由encoding决定

(2)连锁更新问题

  • 大致意思是:previous_entry_length可能是1字节,也可能是5字节,取决于大小是否<254字节
  • 如果存在多个连续的,长度介于250字节到254字节的节点。就会引起连锁更新
  • 最坏情况下要对压缩链表执行N次空间重分配操作

6.跳跃表

最初由William Pugh的论文提出,是一种可以媲美平衡树的链表结构

与红黑树的区别在于:高并发时红黑树的rebalance可能涉及整棵树操作;而跳跃表只涉及局部

在复杂度相同的情况下,跳跃表实现更简单

(1)原始的跳跃表

正常来说链表的查找复杂度是O(n)。跳跃表的复杂度是O(log2n)

对于链表的每层节点,每两个节点都往上加一层索引。假设第一层n个索引,第二层n/2,第三层n/4,以此类推。

下图展示了寻找数字8的搜索过程

(2)redis改进的跳跃表

原始的存在的问题:维护麻烦,每插入一个新节点,需要维护后面节点这种上下相邻两层链表上节点个数严格的 2:1 的对应关系。复杂度变回log(n)

redis的改进:对于每个节点,根据幂次定律(类似反比例函数的图像)随机生成一个介于1到32之间的值作为层level的大小

eg. 50%概览level=1, 25%概览level=2,12.5%概览level=3.....

(3)redis结构

zSkipListNode:节点

double score:记录值

sds:记录value

*backward:指向前一个node的指针

level[n]:span表示跨度记录节点之间的距离,*forward记录下一个

zSkipList:跳表

*header, *tail记录头尾指针

length记录节点数量

注:sds必须唯一,但是score可以相同。

eg. 按照score进行排序,相同score的按照sds字典序排序

zadd myzset 15 banana
zadd myzset 10 apple
zadd myzset 20 apple    //(apple,10)更新为(apple,20)
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    // 成员对象 (robj *obj;)
    sds ele;
    // 分值
    double score;
    // 后退指针
    struct zskiplistNode *backward;
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 跨度
        // 跨度实际上是用来计算元素排名(rank)的,在查找某个节点的过程中,将沿途访过的所有层的跨度累积起来,得到的结果就是目标节点在跳跃表中的排位
        unsigned long span;
    } level[];
} zskiplistNode;

typedef struct zskiplist {
    // 表头节点和表尾节点
    struct zskiplistNode *header, *tail;
    // 表中节点的数量
    unsigned long length;
    // 表中层数最大的节点的层数
    int level;
} zskiplist;

(4)实现

插入删除操作:类似链表,先找到该节点,难点在于维护好各个指针

更新:如果value存在,调整score的值。如果value不存在,变成插入

rank操作:沿着搜索路径,把所有经过节点的跨度span 值进行累加就可以算出当前元素的最终 rank 值。


Reference

java - Redis(2)——跳跃表 - 我没有三颗心脏 - SegmentFault 思否

跳表这种高效的数据结构,值得每一个程序员掌握 - 知乎

深入了解Redis底层数据结构 - 掘金

《Redis设计与实现》读书笔记 - 简书

《Redis设计与实现》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Redis的Bitmap是一种特殊的数据结构,用于存储位图信息,每个位的值只能是0或1。它采用底层的字符串类型(string)来存储位图信息,每个字符都可以表示8个位。 Bitmap的常见使用场景包括数据统计、用户在线状态、布隆过滤器等。 Redisbitmap数据结构的常用命令包括: 1. SETBIT key offset value:将指定偏移量offset的位设置为value(0或1)。 2. GETBIT key offset:获取指定偏移量offset的位的值(0或1)。 3. BITCOUNT key [start end]:计算指定范围内(start和end为可选参数)的所有位的值为1的个数。 4. BITOP operation destkey key [key ...]:对指定的多个key进行位运算,并将结果保存在destkey。operation可以为AND、OR、XOR、NOT。 5. BITPOS key bit [start] [end]:查找指定范围内(start和end为可选参数)第一个值为bit的位的偏移量。 6. BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment]:对指定的位进行位域操作,可进行GET、SET、INCRBY等操作。 示例: 1. SETBIT user:1 0 1:将user:1的第0位设置为1。 2. GETBIT user:1 0:获取user:1的第0位的值。 3. BITCOUNT user:1:计算user:1所有位的值为1的个数。 4. BITOP AND result user:1 user:2:对user:1和user:2进行AND运算,并将结果保存在result。 5. BITPOS user:1 1:查找user:1第一个值为1的位的偏移量。 6. BITFIELD user:1 GET u4 0:获取user:1的第0~3位的值。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值