Redis笔记(1)数据结构与对象
1.前言
此系列博客记录redis设计与实现一书的笔记,提取书本中的知识点,省略相关说明,方便查阅。
2.基本数据结构
2.1 简单动态字符串SDS(simple dynamic string)
结构体定义:
len: buf数组中已使用字节的数量,使用len判断实际内容长度,而不是'\0'字符
free: 未使用字节的数量,查询该值,杜绝内存溢出
buf[]: 实际分配空间及存储内容(字节数组,保证二进制安全,怎么存怎么取)
保留C语言的习惯,字符串以'\0'结束,好处在于可以兼容使用C的API。
分配策略:
1.修改后小于1MB,需要扩展,分配与修改后len相同长度的额外空间,即buf总大小变成len+len+1(结尾字符)
2.修改后大于等于1MB,需要扩展,分配1MB,即buf大小变成len+1MB
释放策略:惰性释放,长度变短时不进释放空间,有需要时释放。
2.2 链表
在redis中使用广泛,列表键底层实现之一就是链表,当一个列表键包含了数量较多的元素,或者元素都是比较长的字符串,会使用链表作为列表键的底层实现。
LLEN,LRANGE等命令:即list
链表节点结构体定义listNode:
listNode *prev: 前置节点
listNode *next: 后置节点
void *valud: 节点的值
链表节点有前后节点,可以构成双向链表
链表的结构体list定义如下:
listNode *head: 表头节点
listNode *tail: 表尾节点
unsigned long len: 节点数量
void *(*dup)(void *ptr): 节点值复制函数
void (*free) (void *ptr): 节点值释放函数
int (*match)(void *ptr, void *key): 节点值对比函数
特性:
双端:前后查询O(1)
无环:以NULL节点终结
头尾指针:获取头尾迅速O(1)
长度计数:O(1)
多态: 三个函数可以支持保存各种不通类型的值
2.3 字典map
使用广泛,比如数据库就是使用字典作为底层实现的。SET msg "hello world" 或者 HLEN HGETALL等命令。就是map和普通键值对。
哈希表dictht定义:
dictEntry **table: 哈希表数组
unsigned long size: 哈希表大小
unsigned long sizemask: 哈希表大小掩码,用于计算索引值,等于size-1
unsigned long used: 哈希表有节点的数量
哈希节点dictEntry结构定义:
void *key: 键
union {
void *val;
unit64_t u64;
int64_t s64;
}v:值
dictEntry *next:下一个哈希表节点,形成链表,解决hash冲突
字典dict结构定义:
dictType *type: 类型特定函数,多态,针对不同类型的键值对
void *privdata: 私有数据
dictht ht[2]: 哈希表 2个空间用于rehash操作,一般使用0,下标1的在rehash时使用
int trehashidx: rehash索引,不进行时,为-1
表位置计算:
1.hash值 MurmurHash2算法
2.hash & sizemask
rehash步骤:
0.rehash条件:
负载因子: used / 表大小
满足一个即可:
1)没有执行BGSAVE或者BGREWRITEAOF命令时,负载因子大于等于1
2)执行了上诉两个命令,且负载因子大于等于5
3)负载因子小于0.1时,自动收缩
1.为ht[1]哈希表分配空间:
扩展操作或收缩操作,ht[1]的大小>=ht[0].used*2^n(n取值使得右边最小)
比如used为7 那么新表大小为8=2^3>7
2.设置rehashidx,将其设置为0,表示开始rehash
3.从hash表的rehashidx下标的链表开始,重新计算hash值,将其完全移动至另一个hash表,之后rehashidx增加1
4.rehash期间,所有的增删改查操作会在两个hash表上进行:
新增的只会添加在ht[1]表上,查找先在ht[0]上进行,没找到再去ht[1]查找。修改删除一样。
单线程执行保证没有并发问题,渐进式rehash也是为了避免数据太大,造成一段时间内停止服务,所以一个下标一个下标移动。扩容机制以及hash算法保证hash碰撞不会过于集中,决定了单个下标数据不会很多。
5.ht[0]上的所有键值对都放入到ht[1]后,rehash完成,rehashidx置为-1。
6.转移完毕后释放ht[0]空间,将ht[1]设置成ht[0],在ht[1]新创建一个空白的hash表,为下一次rehash准备。
2.4 跳跃表
跳跃表只有在有序集合键中和集群节点中使用到了,其余时候没有作用。有序集合键如ZRANGE ZCARD命令相关。
跳跃表节点zskiplistNode结构:
zskiplistNode *backward: 后退指针
double socre: 分值
robj *obj: 成员对象
zskiplistLevel {
zskiplistNode *forward: 前进指针
unsigned int span: 跨度
} level[]; // 层
层level数组可以包含多个元素,每个元素包含一个指向其他节点的指针,可以通过层加快访问速度。层数越多,速度越快。创建节点时会随机生成一个1~32的数值作为该节点的level数组大小。
跨度指的是两个节点之间的距离,NULL的前指针跨度为0。跨度可用来计算目标节点在跳跃表中的位置。
后退指针只有一个,只能退到上一个节点。
跳跃表中的成员通过分值从小到大排列,成员对象指向一个字符串对象,即SDS值。成员对象唯一,多个成员对象可以有相同的分值
跳跃表zskiplist结构:
zskiplistNode *header, *tail:头尾节点
unsigned long length: 表中节点数量
int level: 表中层数最大的节点的层数
2.5 整数集合
整数集合用于数量不多,且都是整型的情况,比如 SADD numbers 1 3 5 7 9, OBJECT ENCODING numbers可以显示 "intset".
可以保存int16_t、int32_t、int64_t类型的值。
整数集合intset结构:
uint32_t encoding: 编码方式
unit32_t length: 包含的元素数量
int8_t contents[]: 保存元素的数组
集合中每一个元素都是contents中的一个项item,从小到大排列,不能重复。contents声明为int8_t,但是实际上不保存这个类型的值,真正类型取决于encoding,INTSET_ENC_INT(16,32,64)。contents的数组大小等于encoding*length。比如是16位的整型,5个元素,contents大小就是80。
升级过程:比如原本encoding是16的整型,现在新增一个32的,就需要升级。
1.根据新类型和元素数量,扩展contents的大小,分配空间。
2.将原元素转换成新元素类型,放入正确的位置,保证顺序不变。
3.将新元素添加到底层数组里面。
4.修改encoding的值,length+1
每次添加元素都可能造成升级,每次升级要处理所有的元素,时间复杂度为o(N)。
升级提升灵活度,随意将16,32,64位的整型添加到集合中。节约内存,要存放16,32,64位的整型最好的方法是直接使用64位的,升级可以减少内存消耗。
inset集合不支持降级操作。
2.6 压缩列表
压缩列表是列表键和哈希键的底层实现之一。列表键中包含少量列表项,并且列表项是小整数值或长度较短的字符串,就会使用压缩列表。
比如RPUSH lst 1 3 5 10086 "hello" "world" OBJECT ENCODING lst 输出”ziplist"
HMSET profile “name" "jack" ...
压缩列表用于节约内存,特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以保存一个字节数组或者整数值。
具体结构按照下列顺序排列:
zlbytes unit32_t 4字节 记录整个压缩列表占用内存字节数,在进行内存重分配或计算zlend位置时使用。
zltail unit32_t 4字节 记录压缩列表尾节点距起始地址有多少字节,通过这个程序无需遍历整个压缩列表可以确定尾节点位置。
zllen unit16_t 2字节 记录压缩列表包含的节点数量,小于65535时为真,等于需要遍历才能计算出来。
entryX 列表节点 不定 各个节点,长度由节点保存内容决定
zlend unit8_t 1字节 特殊值0xFF 用于标记压缩列表的末端
压缩节点的构成entryX:
1.字节数组长度为下面3种之一:
长度小于等于63 2^6-1字节的字节数组
长度小于等于16383 2^14-1字节的字节数组
长度小于等于2^32-1字节的字节数组
2.整型可以是下面6种之一:
4位长度,0~12之间的无符号整型
1字节长的有符合整数
3字节长的有符号整数
int16_t类型整数
int32_t类型整数
int64_t类型整数
3.由下面三个内容构成一个节点:
previous_entry_length: 记录了压缩列表中前一个节点的长度,该字段长度可以是1字节(前节点长度小于254)或5字节(大于等于254,第一个节点会设置成254后面4个节点保存前一个节点长度)。通过这个属性可以遍历到头节点。
encoding:保存数据的类型以及长度。由开头前2位判断类型及该字段的长度,后面的判断长度
00:该字段占用一个字节,后面6个比特位是数组长度,即长度小于等于63的字节数组
01:该字段占用2个字节,后面6+8个比特位是数组长度,即长度小于等于2^14-1个字节数组
10:该字段占用5个字节,后面4个字节记录长度,即长度小于等于2^32-1个字节数组
11000000: int16_t类型整数
11010000: int32_t类型整数
11100000: int64_t类型整数
11110000: 24位有符号整数
11111110: 8位有符号整数
1111xxxx: 该值的时候就没有content属性了,因为本身xxxx就足够保存0~12之间的值了。
content:具体的内容。由encoding决定
连锁更新:
由于previous_entry_length记录了之前的节点长度,但是其有两种形态1字节和5字节。这里就会产生一个麻烦,原本前一个节点是小于254个字节的,本节点使用的1字节形态的previous_entry_length记录了这个情况,现在在该节点前插入了一个大于254个字节长度的节点,要将其改成5字节形态。但是这又产生了一个麻烦,比如当前节点是253个字节,由于previous_entry_length由1变成了5,增加了4个字节长度,导致该节点超过了254个字节,进而后置节点的previous_entry_length也要改变形态,这样可能会发生连锁反应。
删除节点一样会导致连锁反应,都称之为连锁更新。最坏的情况下需要N次空间重新分配,每次最坏O(N),所以最坏为O(N^2)。但是由于可能性太小了,而且只要不是大面积的连锁更新都是可以接受的。所以这个不需要过度担心。
3.对象
redis中没有直接使用上述数据结构来实现键值对数据库,而是基于此实现了一个对象系统,包含:字符串对象,列表对象,哈希对象,集合对象和有序集合对象。redis使用了引用计数技术来控制内存回收机制,不再使用的对象会被释放。
对象基本结构redisObject:
unsigned type:4 类型
unsigned encoding:4 编码
void *ptr 指向底层的数据结构的指针
...
类型有5种:REDIS_STRING 字符串对象、REDIS_LIST 列表对象、REDIS_HASH 哈希对象、REDIS_SET 集合对象、REDIS_ZSET 有序集合对象。使用type命令可以查看对象类型。
encoding决定ptr指向的数据结构,一共有以下几种数据结构:
REDIS_ENCODING_INT long类型的整数
REDIS_ENCODING_EMBSTR embstr编码的简单动态字符串
REDIS_ENCODING_RAW 简单动态字符串
REDIS_ENCODING_HT 字典
REDIS_ENCODING_LINKEDLIST 双端链表
REDIS_ENCODING_ZIPLIST 压缩列表
REDIS_ENCODING_INSET 整数集合
REDIS_ENCODING_SKIPLIST 跳跃表和字典
不同的对象类型有相关的encoding,下面是一个对应关系:
String: int、embstr、raw
list: ziplist、linkedlist
hash: ziplist、ht
set: intset、ht
zset: ziplist、 skiplist
使用OBJECT ENCODING可以看见对象当前编码。
3.1 字符串对象
字符串对象的编码可以是int、raw、embstr。
如果一个字符串对象保存的是整型,并且可以用long类型表示,就会设置成int编码。
如果一个字符串对象保存的是字符串值,并且长度大于39字节,那么使用SDS来保存这个字符串值,设置编码为raw
如果一个字符串对象保存的是字符串值,并且长度小于等于39字节,那么使用embstr来保存这个字符串值。
raw和embstr的区别在于,raw会开辟两次空间,创建redisObject和sdshdr结构,但是embstr只分配一块连续的区间,依次包含redisObject和sdshdr。对应的释放空间也只需要一次。
编码的转换:
int和embstr在条件满足的情况下会被转换成raw编码。
假如一个字符串对象中保存的是整数值,但是使用了append命令追加了字符串,就会变成raw:set number 123, append number " xxx"
embstr编码的字符串没有编写任何的修改程序,所以该类型实际上是只读的。修改的时候都会变成raw,再执行修改命令。
3.2 列表对象
列表对象的编码可以是ziplist或者是linkedlist。
编码的转换:
满足以下2个条件的时候会使用ziplist:
列表对象保存的所有字符串元素长度都小于64个字节。
列表对象保存的元素数量小于512个。
不满足上述条件的会转成linkedlist。
这两个条件可以进行修改,配置list-max-ziplist-value和list-max-ziplist-entries。
3.3 哈希对象
哈希对象的编码可以是ziplist或者是hashtable。
ziplist的时候,键值是紧挨在一起的,先键放入尾端,再把值放入尾端。
编码的转换:
满足以下2个条件的时候会使用ziplist:
列表对象保存的所有字符串元素长度都小于64个字节。
列表对象保存的元素数量小于512个。
不满足上述条件会转成hashtable。
这两个条件可以进行修改,配置hash-max-ziplist-value和hash-max-ziplist-entries。
3.4 集合对象
集合对象的编码可以是intset或者是hashtable
编码的转换:
满足以下两个条件时,对象使用intset编码:
集合对象保存的所有元素都是整数值
集合对象保存的元素数量不超过512个
不满足上述条件的会使用hashtable编码
第二个上限值是可以修改的,配置set-max-intset-entries选项。
3.5 有序集合对象
有序集合的编码可以是ziplist或者skiplist。
ziplist作为实现的时候,第一个节点保存元素的成员,第二个元素则保存元素的分值。
zset结构包含一个zsl跳跃表和一个dict字典表。跳跃表使得有序集合可以进行范围型操作,如ZRANK和ZRANGE命令。字典表可以O(1)复杂度查找给定成员的分值,ZSCORE命令。
编码的转化:
满足以下两个条件时,对象使用ziplist编码:
有序集合保存的元素数量小于128个
有序集合保存的所有元素成员的长度都小于64字节。
不能满足以上两个条件的有序集合对象使用skiplist编码。
以上两个条件的值可以修改,配置zset-max-ziplist-entries和zset-max-ziplist-value。
4.其它概念
4.1 类型检查和命令多态
redis的命令基本上分两种类型:
所有键都能执行,比如delete、expire、rename、type、object。
特定类型键执行,比如:
SET、GET、APPEND、STRLEN只能针对字符串键执行
HDEL、HSET、HGET、HLEN只能针对哈希键执行
RPUSH、LPOP、LINSERT、LLEN只能针对列表键执行
SADD、SPOP、SINTER、SCARD只能针对集合键执行
ZADD、ZCARD、ZRANK、ZSCORE只能针对有序集合键执行
为了不执行错误的命令,都会先进行类型检查,通过redisObject type来实现。
相同的类型但是编码是不同的,意味着命令要适应不同编码结构,这就是命令的多态。
4.2 内存回收
redis构建了一个引用计数技术来实现内存回收机制,在适当的时候自动释放对象,进行内存回收。
redisObject中有一个refcount字段用于引用计数。
创建对象时,引用计数的值会被初始化为1.
被使用时,+1
不被使用时,-1
为0时,释放内存。
4.3 对象共享
键A创建了一个整型100,键B也要创建一个整型100,做法有两种:新建一个或者使用A的。当然后者更节省内存。
1.B指向A的值
2.值的引用计数+1
redis在初始化服务器的时候,会创建一万个字符串对象,包含从0~9999所有整数值。
redis只共享整型值的字符串对象,因为字符串类型的验证相同操作复杂,多个对象时更复杂。
4.4 对象的空闲时长
redisObject还有一个属性是lru属性,记录最后一次被程序访问的时间。OBJECT IDLETIME 可以打印出对象从当前时间到最后一次访问时间的空闲时长。这个命令不会修改lru的值。
空闲时长的一个作用在于,如果服务器打开了maxmemory选项,并且服务器用于回收内存的算法是volatile-lru或者是allkeys-lru,当占用内存超过了maxmemory设置的上限,空转时长较高的键会被优先释放,从而回收内存。
配置文件中maxmemory和maxmeory-policy选项介绍了相关信息。
Redis笔记(1)数据结构与对象的更多相关文章
- Redis 的底层数据结构(对象)
目前为止,我们介绍了 redis 中非常典型的五种数据结构,从 SDS 到 压缩列表,这都是 redis 最底层.最常用的数据结构,相信你也掌握的不错. 但 redis 实际存储键值对的时候,是基于对 ...
- Redis学习笔记一:数据结构与对象
1. String(SDS) Redis使用自定义的一种字符串结构SDS来作为字符串的表示. 127.0.0.1:6379> set name liushijie OK 在如上操作中,name( ...
- [redis读书笔记] 第一部分 数据结构与对象 对象类型
- 从前面redis的基本数据结构来看,可以看出,redis都是在基本结构(string)的基础上,封装了一层统计的结构(SDS),这样让对基本结构的访问能够更快更准确,提高可控制度. - redis ...
- Redis(一):数据结构与对象
前言 本书是Redis设计与实现的读书笔记,旨在对Redis底层的数据结构及实现有一定了解.本书所有的代码基于Redis 3.0. 简单动态字符串 SDS Redis没有直接使用C语言中的字符串,而是 ...
- Redis | 第一部分:数据结构与对象 上篇《Redis设计与实现》
目录 前言 1. 简单动态字符串 1.1 SDS的定义 1.2 空间预分配与惰性空间释放 1.3 SDS的API 2. 链表 2.1 链表与节点的定义 2.2 链表的API 3. 字典 3.1 哈希表 ...
- Redis | 第一部分:数据结构与对象 下篇《Redis设计与实现》
目录 前言 1. Redis对象概述 1.1 对象的定义 2. 字符串对象 3. 列表对象 3.1 quicklist 快速链表 4. 哈希对象 5. 集合对象 6. 有序集合对象 7. Redis对 ...
- Redis | 第一部分:数据结构与对象 中篇《Redis设计与实现》
目录 前言 1. 跳跃表 1.1 跳跃表与其节点的定义 1.2 跳跃表的API 2. 整数集合 2.1 整数集合的实现 2.2 整数集合的类型升级 2.3 整数集合的API 3. 压缩列表 3.1 压 ...
- Redis 基础数据结构与对象
Redis用到的底层数据结构有:简单动态字符串.双端链表.字典.压缩列表.整数集合.跳跃表等,Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包 ...
- redis 笔记02 对象、数据库
对象 Redis并没有使用之前介绍的数据结构来实现键值对数据库,而是基于那些数据结构创建了一个对象系统,这个系统包含字符串对象.列表对象.哈希对象.集合对象和有序集合对象这五种类型对象, 每种对象都用 ...
随机推荐
- zl
https://mooc.study.163.com/course/2001281002?tid=2001392029&_trace_c_p_k2_=a1ef6cb9a64342008c8f5 ...
- 学以致用五----centos7+python3.6.2+django2.1.1
目的,在python 3.6的基础上搭建 django 2.x 一.使用pip安装django ,但是使用pip命令的时候报错,解决方法,做软连接 ln -s /usr/local/python/bi ...
- UISegmentedControl 改变选中字体的颜色
//设置选中的字体颜色为蓝色 [segmentControll setTitleTextAttributes:@{NSForegroundColorAttributeName:[UIColor ...
- 网络通信协议简介(TCP与UDP)
通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样.在计算机网络中,这些连接和通信的规则被称为网络通 ...
- Protocol Buffer 序列化原理大揭秘 - 为什么Protocol Buffer性能这么好?
前言 习惯用 Json.XML 数据存储格式的你们,相信大多都没听过Protocol Buffer Protocol Buffer 其实 是 Google出品的一种轻量 & 高效的结构化数据存 ...
- android textview支持多种格式跳转
http://www.linuxidc.com/Linux/2011-08/40530p2.htm 1.android:autoLink属性,使TextView中链接手机号码/网页/邮件/地图 and ...
- 分频器的verilog设计
笔者最近由于实验室老师的任务安排重新又看了一下分频器的verilog实现,现总结如下,待以后查看之用(重点是查看计数器计到哪个值clk_out进行状态翻转) 1.偶数分频占空比为50% 其实质还是一个 ...
- 《javascript高级程序设计》 touch事件的一个小错误
最近一段时候都在拜读尼古拉斯大神的<javascript高级程序设计>,真的是一本好书,通俗易懂,条理比<javascript权威指南>好理解一些,当然<javascri ...
- [NewCode 4] 替换空格
题目描述 请实现一个函数,将一个字符串中的空格替换成"%20".例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy. 最直接的方式, ...
- 实验5 IIC通讯与AD/接DA接口
1.利用单片机控制PCF8591的AD转换,控制AD0和AD1电位器,在数码光上显示DA转换的值. 2.利用单片机控制PCF8591的DA转换,让发光二极管D1由暗到亮变化,整个过程时间差不多2s左右 ...