在当今互联网时代,数据的快速存储和高效检索是任何成功的应用程序的关键。Redis(Remote Dictionary Server)作为一款高性能、开源的键值存储系统,以其快速的读写速度和丰富的数据结构而备受开发者青睐。
本文将深入探讨Redis的数据结构,揭示其背后的原理。
Redis使用对象来表示数据库中键和值,每个对象都有type,encoding,ptr属性。
对象类型有字符串string,列表list,哈希hash,集合set,有序集合zset
Encoding记录了对象使用的编码,使用object?encoding命令可以查看
ptr指向了对象的底层实现数据结构
不同的对象类型,使用不同的编码可以进行优化
编码可以是int,raw,embstr
如果字符串对象保存的是可以用long类型来表示的整数值,编码为int,整数值保存在对象的ptr内
如果字符串对象保存的是长度大于32字节的字符串值,使用简单动态字符串SDS来保存,编码为raw
SDS(简单动态字符串):有len(字符串长度),free(未使用字节数),buf(保存字符串)属性,中间可以有空字符,空字符结尾
与c字符串区别:?
获取长度复杂度为o(1),
杜绝缓冲区溢出(拼接时),
减少内存重分配次数(长度变化时,通过未使用空间,实现了空间预分配和惰性空间释放的优化,空间分配时len小于1mb,free等于len,否则free等于1mb),
二进制安全(sds中间可以有空字符,可以保存图片,音频等二进制文件),
空字符结尾,可以使用部分c字符串函数
如果字符串对象保存的是长度小于32字节的字符串值,编码为embstr
raw调用2次内存分配函数来分别创建redisObject结构和sdshdr结构,但Embstr只调用一次内存分配函数获得一块连续空间来存放redisObject结构和sdshdr结构
好处:内存分配和释放从2次变为1次,保存在连续的内存中,能更好利用缓存带来的优势
注意:浮点数用字符串值来保存,必要时转换为浮点型再转换为字符串型
Int和ember满足条件时会转换为raw:Int编码追加字符串值,ember(只读)进行修改时
编码可以是ziplist,linkedlist
链表:有head表头节点和tail表尾节点和len长度属性,有节点复制,释放,对比函数。为双端链表,无环(null为终点),多态(可以通过设置不同类型的函数保存不同类型的值)
压缩列表(ziplist):连续内存,顺序,节点保存一个字节数组或者一个整数
属性有zlbytes内存字节数,zltail表尾节点距离起始地址字节数,zllen节点数,entryx节点,zlend列表末尾标记
压缩列表节点属性有前节点长度,编码,内容(可从尾到头遍历)
连锁更新:前节点长度小于254字节,previous_entry_length存储长度为1字节,否则为5字节。如果有连续节点长度为250-253字节,进行添加或删除让这连续节点的前一个节点长度大于等于254字节,会发生连锁更新,复杂度高,但很难出现,不用担心影响性能
?
字符串对象是redis中唯一可以嵌套的对象
编码转换:列表对象字符串元素长度都小于64字节,数量小于512,使用ziplist编码,不满足条件使用linkedlist编码
编码可以是Ziplist,hashtable
使用ziplist时先将键的节点推到列表尾,再将值的节点保存到列表尾
哈希对象键和值长度都小于64字节,键值对数量小于512,使用ziplist编码,不满足条件使用hashtable编码
字典:哈希表作为底层实现?,属性有table哈希表数组,size哈希表大小,sizemask哈希表掩码(size-1),used已有节点数
哈希表节点dictEntry对应键值对,有key,v,next指向下一个哈希表节点
字典由dict结构表示,属性有type类型特定函数(多态字典),privdata私有数据,ht[2]哈希表,Trehashidx rehash索引,不进行时为-1
一般使用ht[0],rehash时使用ht[1]
哈希算法:计算出哈希值然后与哈希掩码进行与运算得到索引值
链地址法:哈希值相同键冲突时使用链表连接起来
Rehash:扩展和收缩使用rehash来实现
先分配空间给ht[1],扩容为第一个大于等于ht[0].used*2的2的倍数,收缩为第一个大于等于ht[0].used的2的倍数。然后将ht0的所有键值对rehash(重新计算哈希值和索引值,放入ht1指定位置)到ht1上。迁移完成后释放ht0,将ht1设置为ht0,为ht1新建一个空的哈希表
负载因子小于0.1,进行自动收缩
渐进rehash:rehashidx值设为0表示rehash开始,crud时顺便会将ht0的键值对rehash到ht1,然后后rehashidx加1,迁移完成后rehashidx设为-1(crud先在ht0找,没有再到ht1找)
编码可以是intset,hashtable(键为字符串保存元素,值为null)
整数集合:?intset属性有encoding编码方式
(INTAET_ENC_INT16,?INTAET_ENC_INT32,?INTAET_ENC_INT64),length元素数量,contents元素的数组(有序,不重复)
添加的新元素过长,需要先升级。集合不支持降级
升级: 先重分配空间,再对元素类型转换放到合适的位置,这个过程保证有序,最后添加新元素(新元素要么在开头,要么在末尾)
升级的好处:使用不同的类型,提升灵活性;使用合适的类型,节约内存
编码转换:元素都是整数,数量小于512使用intset,否则使用hashtable
成员为字符串,分值为浮点型
编码可以是ziplist(先保存成员再保存分值,根据分值从小到大)和skiplist(跳跃表根据分值从小到大保存,方便范围操作。dict字典创建了成员到分值的映射,查找方便o(1))
编码转换:有序集合元素成员长度都小于64字节,元素数量小于512,使用ziplist编码,不满足条件使用skiplist编码
跳跃表:由zskiplist和zskiplistNode结构定义
Zskiplist属性有header跳跃表头节点,tail尾节点,level节点最大层数(不算表头节点),length表长度即节点数
ZskiplistNode属性有level层数组(有前进指针和跨度,跨度记录两个节点的距离),backward后退指针,score分值,obj成员对象(保存一个sds值)。分值可以相同,成员对象唯一。分值相同时,根据成员对象排序
层高是1-32的随机数,有后退指针和前进指针可以从前往后遍历,也可以从后往前遍历
类型检查与命令多态
有些命令只有特定类型能够执行,通过值的类型判断是否能够执行(检查键类型就是键的值对象类型);根据值对象的编码方式使用不同的方式执行
内存回收
Redis对象构建了引用计数来实现内存回收:
构建新对象时,值为1;被新程序使用时加1;不再被一个程序使用时减1;值为0时释放对象内存
对象共享
Redis初始化时创建了一万个字符串对象,包含了0-9999的整数值。需要用到0-9999的字符串对象时,使用这些共享对象,不创建新对象
不使用保存字符串的字符串对象是因为验证与所需对象是否相同的复杂度高,消耗cpu时间多
对象空转时长
RedisObject的lru记录了对象最后一次被程序使用的时间,可以计算对象的空转时长