《redis设计与实现》 第一部分:数据结构与对象 || 读书笔记

不得不夸赞redis的轻量级,以及代码透明度。可以通过这本书《Redis设计与实现》看原理,对照着官网上的src看code,理解更深刻!!!
可能还需要看几遍,一遍看过去也不能全部记住,重复是硬道理!!!

0. 前言

  • 特性:
    • redis内置集合数据类型,支持对集合执行交集、并集、差集等
    • 一部分命令只能对特定数据类型执行(append只能对字符串,hset只能对哈希表),还有一部分命令可以对全部的数据类型执行(del、type以及expire)

1. 引言

  • 单机功能:第一部分、第二部分、第四部分
  • 多机功能:第三部分
  • 源代码:C语言
  • 相关订正信息:http://redisbook.com/

2. 第2章 简单动态字符串

  • SDS:simple dynamic string
  • 键值对:底层实现是保存着对应字符串的SDS
    • 键(msg)值(hello world):2个SDS
    • 键(numbers)值(1 2 3 4):5个SDS
  • 还可以被用作缓冲区buffer

2.1 SDS的定义

对照redis-6.0.8的代码看

// sds.h/sdshdr8
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
  • len:使用的长度
  • buf:存储的char类型数组

2.2 SDS与C字符串的区别

  • 获取字符串长度的复杂度从O(N)降到O(1)
  • C字符串不记录自身长度:容易造成缓冲区溢出
  • C字符串修改可能会引起内存重分配,Redis数据库可能会出现数据被频繁修改,如果使用SDS可以缓解
    • SDS空间预分配:小于1M,free=len,大于1M,free=1M
    • SDS惰性空间释放:不用空间增加到free里面。也有相应的api可以真正释放空间
  • 二进制安全:C字符串不能包含空字符,否则会认为是字符串的结尾,这导致C字符串不能保存图片、音频等二进制数据,但是SDS可以
  • 兼容部分C字符串函数

安装

  • 依据此链接,安装一个单机版本用作学习:https://redis.io/download
  • 把server跑起来,然后运行一个client,通过6379的端口进行设置

3. 链表

  • 双向链表
  • 包含成员变量和成员函数:表头、表尾、节点数量、节点复制函数、节点释放函数、节点值对比函数
  • 特性:使用void*指针来保存节点值,可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用来保存各种不同类型的值

4. 字典

4.1 相关的数据结构

  • dictht
    • table:哈希表数组
    • size:哈希表的大小
    • sizemask:哈希表大小掩码,用于计算索引值,等于size-1(属性值和hash值一起决定这个键应该被放在table数据的哪些索引)
    • used:目前已有键值对的数量
// dict.h/dictht
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;
  • dictEntry
    • next解决键冲突collosion的问题
typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;
  • dict
    • type属性是一个指向dictType结构的指针,指向不同类型的键值对,为创建多态字典设置的
    • ht是包含两项的数组,每一项都是一个hash表,一般只使用ht[0], ht[0]需要rehash的时候使用ht[1]
    • rehashidx不在进行时,值为-1
typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

4.2 哈希算法

  • hash = dict->type->hashFunction(k0)
  • index = hash & dict->ht[0].sizemask
  • redis使用的MurmurHash2
    • MurmurHash算法由Austin Appleby发明于2008年,是一种非加密hash算法,适用于基于hash查找的场景。murmurhash最新版本是MurMurHash3,支持32位,64位及128位值的产生。
    • MurmurHash标准使用C++实现,但是也有其他主流语言的支持版本,包括:perl、C#、ruby、python、java等。这种算法即使输入的键是有规律的,算法仍能给出一个很好的随机分布性,计算速度非常快,使用简单。因此在多个开源项目中得到应用,包括libstdc、libmemcached、nginx、hadoop等。
    • Redis使用的是MurmurHash2。当字典被用作数据库的底层实现,或者哈希键的底层实现时,使用MurmurHash2算法来计算键的哈希值。

4.3 解决键值冲突

  • 使用next指针解决键冲突【新增加的节点在链表的表头】
  • 单向链表

4.4 rehash(灵魂设计,很巧妙)

  • 为了使哈希表的负载因子维持在一个合理的范围,程序需要对哈希表的大小进行相应的扩容或者收缩,所以就进行rehash
    • ht[1]哈希表空间大小
      • 扩展操作:ht[1]=第一个大于等于ht[0].used*2的2^n
      • 收缩操作:ht[1]=第一个大于等于ht[0].used的2^n
    • 将ht[0]重的键值rehash到ht[1]
    • 数据迁移结束后,释放ht[0],将ht[1]设置成ht[0],并在ht[1]新创建一个空白哈希表,等待下一次rehash
  • 渐进式rehash
    • 如果hash表ht[0]存在很多键值对,如果一次性整到ht[1],服务器将会有一段时间停止服务。所以为了避免rehash对服务器性能造成影响,需要分多次将ht[0]里面的键值对慢慢地rehash到ht[1]
    • rehashidx:值设为0表示rehash正式开始,完成一个键值对,rehashidx+=1,最后所有的键值对都迁移完成,rehashidx设为1
    • 过程中,会有增删查改
      • 增只会保存在ht[1],这样最终会迁移结束
      • 删除、查找、更新会在两个hash表同步进行

4.5 扩展操作和收缩操作的条件

  • 扩展操作
    • 没有执行bgsave或者bgrewriteaof,负载因子大于等于1
    • 执行bgsave和bgrewriteaof,负载因子大于等于5
      • redis需要创建当前服务器进程的子进程(大多数操作系统会使用写时复制技术来优化子进程的使用效率)。在子进程存在期间,服务器会提高执行扩展操作所需要的负载因子,从而尽可能避免在子进程存在期间进行hash表的扩展操作(避免不必要的内存写入操作,较大程度节约内存)
  • 收缩操作
    • 负载因子小于0.1

5. 第5章 跳跃表

  • 感觉自己在学习数据结构,跳跃表看这里:https://blog.csdn.net/qpzkobe/article/details/80056807
  • 跳跃表
    • 跳跃表【每一层有两个属性:前进指针和跨度】
      • 层的跨度level.span 纪录两个节点之间的距离
      • 跨度可以用来计算排位:在查找某个节点的过程中,将沿途访问的层的跨度累计起来,最后得到的就是目标节点在跳跃表中的排位
    • 后退指针:BW字样标记节点的后退指针,当程序从表尾向表头遍历的时候使用
      • 每次只能后退至前一个节点
    • 分值和成员
      • 跳跃表的所有节点按照分值从小到大排序,分值相同按照字典序的大小排序
      • ele是一个sds(简单动态字符串)
    • 跳跃节点的层高:1-32之间的随机数
  • 遗留问题:高并发情况下,怎么给跳跃表加锁
    • 朋友教学:乐观锁
    • 乐观锁:大多是基于数据版本Version记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个“version” 字段来 实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提 交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据 版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
      • https://blog.csdn.net/u012240455/article/details/81625081
// server.h/zskiplistNode
typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned long span;
    } level[];
} zskiplistNode;

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;

6. 第六章 整数集合

6.1 数据结构

  • intset
    • encoding为INTSET_ENC_INT16,contents是int16_t类型的数组
    • encoding为INTSET_ENC_INT32,contents是int32_t类型的数组
    • encoding为INTSET_ENC_INT64,contents是int64_t类型的数组

6.2 升级

  • 如果新加入的元素比整数集合现有的所有元素的类型都要长,整数集合需要先进行升级,然后将新元素添加到整数集合里面
    • 根据新元素的类型,扩展底层数组的空间大小
    • 将底层数组的所有元素转换成新元素相同的类型,保证其有序性
    • 将新元素添加到底层数据中
  • 时间复杂度:最坏O(N)
  • 优点
    • 比较灵活,可以随意在数组里面添加元素
    • 节约内存,有需要的时候再扩展
  • 不支持降级
    • 一旦数组升级,编码会一直保持升级后的状态
typedef struct intset {
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;

7. 第七章 压缩列表

  • 为了节约内存开发的,由一系列特殊编码的连续内存块组成的顺序型数据结构
  • 一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值

7.1 数据结构(压缩列表)

// ziplist.c
/* Return total bytes a ziplist is composed of. */
#define ZIPLIST_BYTES(zl)       (*((uint32_t*)(zl)))

/* Return the offset of the last item inside the ziplist. */
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))

/* Return the length of a ziplist, or UINT16_MAX if the length cannot be
 * determined without scanning the whole ziplist. */
#define ZIPLIST_LENGTH(zl)      (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))

/* The size of a ziplist header: two 32 bit integers for the total
 * bytes count and last item offset. One 16 bit integer for the number
 * of items field. */
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))

/* Size of the "end of ziplist" entry. Just one byte. */
#define ZIPLIST_END_SIZE        (sizeof(uint8_t))

/* Return the pointer to the first entry of a ziplist. */
#define ZIPLIST_ENTRY_HEAD(zl)  ((zl)+ZIPLIST_HEADER_SIZE)

/* Return the pointer to the last entry of a ziplist, using the
 * last entry offset inside the ziplist header. */
#define ZIPLIST_ENTRY_TAIL(zl)  ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))

/* Return the pointer to the last byte of a ziplist, which is, the
 * end of ziplist FF entry. */
#define ZIPLIST_ENTRY_END(zl)   ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)

7.2 数据结构(压缩列表节点)

  • code
// ziplist.c
/* We use this function to receive information about a ziplist entry.
 * Note that this is not how the data is actually encoded, is just what we
 * get filled by a function in order to operate more easily. */
typedef struct zlentry {
    unsigned int prevrawlensize; /* Bytes used to encode the previous entry len*/
    unsigned int prevrawlen;     /* Previous entry len. */
    unsigned int lensize;        /* Bytes used to encode this entry type/len.
                                    For example strings have a 1, 2 or 5 bytes
                                    header. Integers always use a single byte.*/
    unsigned int len;            /* Bytes used to represent the actual entry.
                                    For strings this is just the string length
                                    while for integers it is 1, 2, 3, 4, 8 or
                                    0 (for 4 bit immediate) depending on the
                                    number range. */
    unsigned int headersize;     /* prevrawlensize + lensize. */
    unsigned char encoding;      /* Set to ZIP_STR_* or ZIP_INT_* depending on
                                    the entry encoding. However for 4 bits
                                    immediate integers this can assume a range
                                    of values and must be range-checked. */
    unsigned char *p;            /* Pointer to the very start of the entry, that
                                    is, this points to prev-entry-len field. */
} zlentry;
  • 解释
    • prevrawlen:记录前一节点的长度,所以程序可以通过指针运算,根据当前节点的起始地址计算出前一个节点的起始地址(可以从表尾遍历到表头)
    • encoding:记录content属性所保存的数据类型以及长度

7.3 连锁更新

  • 将某一个长度大于254字节的新节点new设置为压缩列表的表头节点,那么可能引起后面节点e1的prevrawlen变大,从而e2的prevrawlen变大,程序需要不断对压缩列表执行空间重分配操作
  • 最坏情况下的复杂度n^2
  • 添加节点和删除节点,都有可能导致连锁更新

8. 第八章 对象

8.1 对象的类型与编码

  • code
typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;
  • 解释
    • type:键总是一个字符串对象,值可以是字符串对象、列表对象、hash对象、集合对象或者有序集合对象中的某一种。举例:type的属性值有REDIS_STRING
    • encoding:对象使用哪种数据结构作为底层实现。一个type可以对应多种encoding
      • REDIS_STRING, REDIS_ENCODING_INT/REDIS_ENCODING_EMBSTR/REDIS_ENCODING_RAW
      • 可以根据不同的场景为一个对象设置不同的编码

8.2 字符串对象

  • 字符串对象的编码可以是int、raw、embstr
    • 如果一个字符串对象保存的是整数值,并确定用long表示,那么ptr的void*将转换成long,同时将字符串对象的编码设置成int
    • 如果一个字符串对象保存的是字符串值,并这个字符串长度大于32字节,那么ptr的void*将转换成sds,同时将字符串对象的编码设置成raw
    • 如果一个字符串对象保存的是字符串值,并这个字符串长度小于等于32字节,那么ptr的void*将转换成sds,同时将字符串对象的编码设置成embstr
      • embstr编码和raw编码一样,都是使用redisobject和sdshdr来表示字符串对象。raw编码会调用两次内存分配函数来连续创建redisobject和sdsstr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisobject和sdshdr两个结构【一次内存分配函数】
      • 一次内存释放函数,raw编码需要两次
      • 字符串的所有数据都存在一块连续的内存里面,这种编码的字符串对象比起raw编码的字符串对象更能利用缓存优势
  • long double类型表示的浮点数在redis里面也是作为字符串保存的
    • 如果需要对浮点数进行运算操作,程序会将字符串值换回浮点数值,操作结束后,再将浮点数值的结果转换成字符串值
  • long的编码是int,long double的编码是embstr或者raw
8.2.1 编码的转换
  • 如果对一个保存整数值的字符串对象追加了一个字符串值,编码从int将会转换成raw
  • embstr编码的字符串对象是只读的,如果需要修改需要转化成raw【redis3.0版本】

8.3 列表对象

  • 种类
    • ziplist压缩列表(entry是一个元素)
    • linkedlist双向列表(node保存了一个字符串对象:redisobject、sdshdr)
  • 何时使用ziplist(阈值可以修改)
    • 所有字符串元素的长度都小于64字节
    • 对象保存的元素数量小于512

8.4 哈希对象

  • 编码可以是ziplist或者hashtable
  • ziplist
    • 保存键的节点在前,保存值的节点在后
    • 先添加到哈希对象的键值对会放在表头方向,后添加的在表尾
  • hashtable
    • 哈希对象中的每个键值对都用一个字典键值对来保存
  • 何时使用ziplist(阈值可以修改)
    • 所有键值对的键和值的字符串长度小于64字节
    • 键值对数量小于512个

8.5 集合对象

  • 编码可以是intset或者hashtable
  • 何时使用intset(阈值可以修改)
    • 所有键值对都是整数值
    • 键值对数量小于512个

8.6 有序集合对象

  • 编码可以是ziplist或者skiplist
  • ziplist:键值连续存放,由小及大
  • zset(skiplist)【使用指针来共享相同元素的成员和分值,使用zsl和dict不会浪费额外的内存】
    • dict:从成员到分值的映射,可以用O(1)的复杂度查找给定成员的分值
      • 方便通过成员找到分值
    • zsl:按分值从小到大保存了所有集合元素,object属性保存了元素成员,score保存了元素的分值
      • 方便找zrank zrange
  • 何时使用ziplist(阈值可以修改)
    • 有序集合的元素数量小于128个
    • 有序集合成员的长度都小于64字节
// server.h
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned long span;
    } level[];
} zskiplistNode;

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

8.7 类型检查与命令多态

  • 通用命令(基于类型的多态),可以对任何类型的键执行:DEL、EXPIRE、RENAME、TYPE、OBJECT等
  • 只对特定类型的键执行(基于编码的多态)
    • 字符串键:SET GET APPEND STRLEN
    • 哈希键: HDEL HSET HGET HLEN
    • 列表键:RPUSH LPOP LINSERT LLEN
    • 集合键:SADD SPOP SINSERT SCARD
    • 有序集合键:ZADD ZCARD ZRANK ZSCORE
  • 内部的机制:多态命令
    • 一个type可能会有多个encoding,但是只要一个type,对应的命令都可以正确实现,在实现的过程中会进行类型检查、然后根据编码选择实现函数

8.8 内存回收

  • C语言并不具备自动内存回收功能,redis在对象系统中构建了一个引用计数,参考以下代码里面的refcount
    • 创建一个新对象,引用计数初始化为1
    • 被一个程序使用,引用计数+1
    • 不被一个程序使用,引用计数-1
    • 引用计数值变为0,对象所占用的内存被释放
typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;

8.9 对象共享

  • 多个键共享同一个值对象
    • 将数据库键的值指针指向一个现有的值对象
    • 将被共享的值对象的引用计数增一
  • 需要验证共享对象和值所对应的目标对象是否完全相同
    • 目标对象值越复杂,验证操作的时间复杂度越高
  • 受到cpu的限制,redis仅仅对包含整数值的字符串对象进行共享

8.10 对象的空转时长

  • redisObject结构中包含lru属性,该属性记录了对象最后一次被程序访问的时间
  • OBJECT IDLETIME 在访问键的值对象,不会修改lru属性
  • 作用:服务器有mexmemory选项,如果服务器回收内存的算法是volatile-lru或者allkeys-lru,当超出上限,空转时间长的就会被释放
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值