《Redis 设计与实现》读书笔记(一)

@(Redis)

豆瓣链接

一、数据结构与对象

1.SDS

SDS是Redis实现的一个字符串数据结构。
结构:

struct sdshdr {
	int len; //字符串长度
	int free; //未使用的字符串长度
	char buf[] //保存字符
}

为什么不用c的字符数组

  • SDS记录字符串长度,所以获取字符串长度的操作是O1
  • SDS一次申请较长的内存,例如如果字符串长度是5,它会申请多于5的内存,而下次修改字符串,就不需要重新申请内存了。所以可以减少申请和释放内存的次数,提高效率
  • 由于sds的底层还是c的字符数组,所以可以兼容c的字符串函数

2.链表

数据结构:
链表节点

typedef struct listNode {
	struct
	 listNode *prev //前节点
	 struct listNode * next // 后节点
	 void *value; //节点的值

}

链表

typedef struct list {
		listNode *head; // 头节点
		listNode *tail ;// 尾节点
		unsigned log len;// 节点长度
	
}

3.字典

哈希表节点

typedef struct dictEntry{
	void *key; //键
	union{
		void *val;
		uint64_t u64;
		int64_t s64;
	} v; // 节点的值,可以是指针(val),也可以是数值(u64,s64)
	struct dictEntry *next ; //下一个节点,形成链表

}

哈希表

typedef struct dictht {
	dictEntry **table; //哈希表数组
	unsigned log size; //哈希表大小
	unsigned log sizemask;// 大小掩码,总是等于size-1
	unsigned log used;//已有节点数量

}

字典

typedef struct dict {
	dictType *type; //类型特定函数
	void *pricdata; // 私有数据
	dictht ht[2]; // 哈希表
	int trehashidx; // rehash索引,当rehash不在进行时,值为-1
}

一个字典有两张哈希表,0用于存储数据,1用于rehash。
当要存储k0=v0的时候,首先程序会算出k0的hash值,例如是8,假如sizemask是3,size是4,然后计算index=8&3=0(不知道&是什么操作,但是应该是8mode4=0,取余)。然后去ht[0].table数据中,找index等于0的哈希节点,然后是否有哈希节点,如果有,就看节点的next是否为空,直到找到next为空的节点,然后创建一个新的节点,使上一个节点的next指向新节点
如果是获取k0的值,计算index后,遍历节点连表,直到找到key等于k0的节点。

所以

  • 如果一个index对应的链表很长,其实要花On的复杂度来遍历,然后获取节点的值的
  • 当一个字典的节点数量(used值)增大到一定值时,字典会执行rehash操作,也就是增大size值,table的长度,这样就会减少节点链表的长度

rehash

rehash的过程就是

  • 当触发rehash后,会修改字典的trehashidx值,表示正在执行rehash
  • redis会启动一个新的进程,遍历dt[0]的所有键,然后重新计算index,放到dt[1]中,当然dt[1]的size会比dt[0]大
  • 在rehash的过程中,字典的删除,查找,更新操作会在两个dt里面执行
  • 插入,只在dt1中执行
  • rehash完成后,把dt0执行dt1,把dt1置空。

负载因子= used / size

  • 当负载因子小于1,执行收缩操作
  • 当服务器没有执行BGSAVE或者BGREWRITEAOF操作,而且负载因子大于1,执行扩展操作
  • 当服务器执行BGSAVE或者BGREWRITEAOF操作,而且负载因子大于5,执行扩展操作

4.跳跃表

跳跃表,主要用于加快遍历的速度。
例如有序列表,1,2,3,4,5,50
如果要查找50的节点在哪里,需要对前面的5个节点都遍历一次,比较慢。如果再第一个节点记录了50在哪里,或者5在哪里,程序就可以直接跳过前面的节点,直接到达,或者接近目标节点。
注意列表必须是有序的。

跳跃表结构

  • header 指向头节点
  • tail 指向尾节点
  • level 表示最多层数的节点的层数
  • length,表示节点的数量

跳跃表节点结构

typedef struct zskiplistNode {
	struct zskiplistNode *backward;// 上一个节点的指针
	double score;// 节点的分值
	robj *obj; // 节点的存储对象
	struct zkiplistLevel{// 节点的层,是一个列表
		struct zskiplistNode *forward; //下一个节点的指针
		unsigned int span;  //前进的跨度
	} level[];
}

一个列表有多少个元素,就会有多少个跳跃表节点
当创建一个新节点时,程序会根据幂次定律,随机生成1和32直接的值作为level数组的大小,这个大小就是这个节点的高度。
例如上面的元素1的节点,会有一个level指向50的节点,跨度是5。这样当程序需要找到50的时候,遍历1的节点,发现1节点的值小于50,就去遍历1节点的level,

  • 如果第一个level就是指向50节点,那就立刻找到了。
  • 如果第一个level是指向5,发现5不符合,就找下一个level

5.整数集合

当一个集合(Set)只包含整数元素,而且数量不多时,就会使用整数集合。

type struct intset {
	uint32_t encoding;//编码方式,整数的大小,例如32位,16位
	uint32_t length;//集合的长度
	int8_t contents[];//集合的元素,类型取决于encoding属性的值

}

升级

如果集合的encoding是16位,当要插入一个32位的整数时,数据结构就会升级,例如升级为32位的。
但是不会进行降级操作。

6.压缩列表

当一个列表和字典的长度比较短,就会使用压缩列表。

属性:

  • zlbyte,uint32_t 类型,记录压缩列表的字节数
  • zltail uint32_t 记录尾元素节点距离起始地址有多少字节,通过这个可以快速定位到尾节点
  • zllen 节点数
  • entryX 节点元素
  • zlend 压缩列表的末端

压缩列表不同上面的数据结构是redis里面的struct,压缩列表是一段内存数据。

1.节点的构成,也就是entryX里面的结构

  • previous_entry_length 前一个节点的长度,单位是字节
  • encoding,保存的数据类型
  • content ,内容

如果要查找一个元素,

  • 首先查看zltail,定位到尾节点
  • 读取尾节点的encoding,确定content的长度
  • 读取content的内容
  • 如果内容不符合
  • 根据previous_entry_length,,定位前一个节点的位置,重复上面的过程

如果新增元素,会在头部插入,然后更新前一个节点的previous_entry_length 。
如果删除元素,修改前一个节点的previous_entry_length为空。

压缩列表的优点

  • 实现简单,可以节约内存
  • 读写都比较简单,不需要改太多值

7.对象

Redis有五种对象

  • 字符串
  • 列表
  • 哈希
  • 集合
  • 有序集合

redis中,通过type命令,可以查看一个key的对象类型

这5中对象在不同的时候,会采用上面介绍的数据结构的其中一种。而且在操作的过程中,数据结构也会变化。

通过命令object encoding可以查看key使用的数据结构

redis 127.0.0.1:6701> set test 'aaa'
OK
redis 127.0.0.1:6701> type test
string
redis 127.0.0.1:6701> object encoding test
"embstr"
redis 127.0.0.1:6701> 

embstr就是上面的SDS。

redis使用的数据结构有

  • 整数 int
  • SDS enbstr,底层使用sds
  • 简单动态字符串 raw ,底层使用sds
  • 字典 hashtable
  • 双端链表 linkedlist
  • 压缩列表 ziplist
  • 整数集合 intset
  • 跳跃表和字典 skiplist

对象的数据结构

typedef struct redisObject {
	unsigned type:4; //对象的类型,对应上面的五种对象
	unsigned encoding:4; //使用的数据结构,对应上面的几种结构
	void *ptr ;//底层的数据结构的指针
}

1.字符串对象

数据结构可以是int raw和embstr
如果字符串全是数字,就会使用int类型
如果字符串的大小大于39字节,使用raw来存储
如果大小小于39字节,使用embstr来存储

raw和embstr的区别是,raw需要申请两次内存,embstr只需要一次

2.列表对象

列表对象可以是ziplist或者linkedlist
如果列表元素的长度都小于64字节,而且元素的数量小于512个,就会使用ziplist,否则使用linkedlist

3.哈希对象

哈希对象可以是ziplist或者hashtable
如果键值对的长度都小于64字节,而且键值对的数量小于512个,就会使用ziplist,否则使用linkedlist

4.集合对象

可以是intset 或者hashtable

当元素都是整数值,元素不超过512,使用intset,否则使用hashtable

5.有序集合

数据结构可以是ziplist或者skiplist
当元素小于128个,而且元素长度都小于64字节,使用ziplist,否则使用skiplist。

6.内存回收

redis使用引用计数器来实现内存的回收

每个对象都有一个int refcount属性,来记录被引用的次数。
在业务端,

  • 每次引用对象,需要调用对象的incrRefCount函数,使用对象的引用计算加一,
  • 每次不引用对象的时候,都需要调用对象的decrRefCount函数,来令refcount属性减1,如果引用计数小于1,就会回收内存。

7.对象共享

数字0到9999,这些值的字符串对象,会在redis启动的时候就被创建,然后被共享。

为什么其他值不共享,因为如果要共享其他值,每次创建的时候,都有查看内存中有没有这个值的对象,这个查询的效率是很低的。

8.对象的空转时长

每个对象都有个属性 unsigned lru:22;,用于记录对象最后一次被访问的时间

通过object idletime命令,可以查看一个key的空转时长,其实就是当前时间戳减去lru。
在redis回收内存的时候,而且当回收算法是volatile-lru或者allkeys-lru时,会优先回收空转时长较长的key。

posted @ 2019-12-23 17:51  Xjng  阅读(671)  评论(0编辑  收藏  举报