1、Redisのデータ構造
単純な動的文字列(SDS)を含むデータ構造の基礎となるのRedisは、整数のセットのようなリスト、辞書、圧縮されたリスト、; 5つのデータ型(オブジェクト)構造のうちの1つ以上で構成されています。
あなたは、キーのデータ構造を表示するには、コマンドラインオブジェクトエンコードキーを使用することができます。
2、シンプルな動的な文字列SDS
RedisのはCで書かれているが、文字列データ型の列C言語を使用していませんが、むしろ簡単なリライト動的な文字列(シンプルダイナミック文字列、SDS)。
図1は、/ * 文字列オブジェクト構造セーブ2 * 3 * /。 4 sdshdr {構造体。 5。 長さ6 // BUF占有空間内の 7 INTがlen; 8 9 // BUF自由空間を残りの長さ ; 10 INT無料 。11 12であります//データ空間 13は、char bufが[]で 14}。
具体的には以下のように、SDSのRedisを使用して文字列を保存します:
黄Jianhongと「Redisの設計と実装」からの写真
-
- それは、配列bufに残りの空き領域の数を表し、
- それは、配列に格納されているbufに長さlenバイトを記録
- アレイBUFチャーは終了識別子として記憶されたデータ、特定の文字列レコードのタイプ、「\ 0」(ヌル文字)であります
C言語と比較してSDS-定義された文字列はほぼ同じ、lenを、自由つ以上の特性である。なぜ直接C言語の文字列にそれを使用していませんか?
1、文字列の長さの複雑さはO(1)で取得します
C言語は、文字列の長さが格納されていないため、マルチごとに取得周期の文字列の長さは、文字列全体を必要な、時間の複雑さはO(N)であり、文字列の長さが記録SDS直接文字列の長さを取得格納されていますLENにプロパティ値を取得し、時間計算量はO(1)であり、SDSの長さを更新する手動操作することなく、自動的にAPIで提供されます。
図2に示すように、バッファオーバーフローを防止するため
2つのC言語の文字列連結を行う際に十分なメモリ空間が割り当てられていないと、それはオーバーフローの原因となり、そしてSDS文字列を変更するとき、LENの値に応じて、第一のメモリ空間をチェックし不足が割り当てられる場合に、十分ですその後、メモリ空間、および文字列を変更するので、バッファオーバーフローに終止符を打ちます。
メモリの再割り当ての文字列の数を減らすために修正をもたらした3、
C言語の文字列の長さは、メモリを再割り当て、変更したとき、記録されていない;それは正常である文字列場合、十分なメモリのオーバーフローが発生している、文字列が短縮されている場合、漏れが重い分布を生じないであろう。
SDSは、空間2つの最適化戦略と不活性空き領域を事前に割り当て
事前割り当てスペース:文字列操作を成長させるには、メモリの場合は将来の拡張、ある程度、周波数及び再配置を低減することができるように、余分な未使用スペースを分配します。
フリースペース不活性:文字列操作を短縮し、そしてすぐにこれらのスペースを解放しませんが、成長操作は、これらのレコードのスペースを再利用することができたときに、空き領域の使用は、これらの数を記録した後、応答SDSの提供それはメモリの無駄が発生することはありませんので、手動で、スペースを解放するAPI。
4、バイナリセーフ
SDSは、裁判官の文字にlenのに基づいており、(C言語では、文字列の末尾を決定するために、ヌル文字であるため)(例えば画像などのヌル文字を含んでいてもよい)あなたには、いくつかのバイナリファイル保存することはできませんので、C言語の文字列は、ヌル文字を含めることはできませんその構造は、写真のSDSを保存することができ、およびバイナリモードで処理されていることを、文字列の終わり、。
図5に示すように、文字列関数互換部分C
SDS BUF構造が同じのヌル終端文字列を格納している、オペレーティング文字列APIのC言語と部分的に両立させることができます。
要約:
表出典:「Redisの設計と実装」
3、リスト
RedisのはCで書かれていないが、何の内蔵リストこのデータ構造を、それらのビルドリンクリストの実装;ノードは、リスト・ノードのリンクリスト構造を形成します。
非常に広い範囲のリストは、そのようなキーのリストとして、公開し、遅いクエリ、モニターなどをサブスクライブします。
typedefは構造体{listnode 1。 2 //ノードの正面 3 *構造体listnode PREV; //ポストノード4 。次にlistnode 5 *構造体; 。ノードの6 //値 7ボイド*値; 8} listnode。
これは、リストの保留リストを使って、ダブルエンドlistNodeのPREVおよびNEXTポインタリンクリストを複数構成することができます
typedefは構造体{リスト1つの。 2 //ヘッダノード 3 * listnodeヘッド; 4 //エンドノード表 5 listNode *テイル、 ノードの数がリスト// 6に含まれる 7 unsigned long型LEN;。 。8 //ノードの値は、機能複製 9ボイド*(DUP *)(ボイド* PTR); 解除機能の10 //ノード値 11ボイド(フリー*)(ボイド* PTR); 12は、コントラスト関数//ノードの値である 13 INT(*一致)(ボイド * ptrは、ボイド* キー); 14}リスト。
-
- ヘッドテーブルポインタ
- テールのテールポインタテーブル
- LENの鎖長カウンタ
- 無料、特定の機能がDUP必要なタイプ、多型リストと一致
以下を達成するための特性のRedisのリスト:
ダブルエンド1、
PREVポインタを有する次のリストノード、前後のノードへの迅速なアクセスが、時間複雑度はO(1)です。
2、非環式
前エンドポイントとして、次のノードにヌルポイントにNULLにリストへのアクセスをノードポインタとテールポインタをヘッド。
図3に示すように、テーブルポインタとテールポインタテーブルと
あなたはすぐにヘッダーノードとテールノードテーブルを取得することができます。
4、鎖長カウンタ
あなたはすぐにリストの長さを得ることができます。
5、多型
値の異なるさまざまな種類のは自由、リストDUPによって保存することができますリストし、ノードタイプの特定の機能のための3つの属性の設計値と一致します。
4、辞書
辞書も連想配列(連想配列)またはマップ(MAP)、(シンボルテーブル)シンボル・テーブルと呼ばれる、キーと値の辞書格納されたキー値ペアとキーを省略する。
Redisの広くRedisのデータベースは、基礎となる実装として辞書を使用することであるような、辞書で使用されます。
Redisのは、達成するために、独自の辞書を構築するために使用RedisのC言語は、このような構造を内蔵しておりません。
底部に再び辞書としてハッシュテーブルを使用して、ハッシュの複数のノード、ノードのペア保存各ハッシュを含むハッシュテーブル。
ハッシュテーブル
。typedefは構造体{dictht 1 2 //ハッシュテーブルアレイ 3 dictEntry **表; 4ハッシュテーブルの//サイズ 5ロング符号なしサイズ; 6 //ハッシュテーブルサイズのマスク指標値を算出するために使用され 、常に7 //を1-等しいサイズ 8のunsigned long sizemask; 番号9 //ハッシュテーブルは、ノードた 10符号なしのロング使用され; 11} dicthtと、
図ブランクは、ハッシュテーブルのサイズである4
-
- テーブルが格納されたキー値ペアdictEntryの各々、配列、配列要素がポインタdictEntry構造であります
- ハッシュテーブルのサイズのレコードのサイズ
- sizemask 值总是等于size-1,这个属性和哈希值一起决定一个键应该被方法table数组的哪个索引上
- used 记录哈希表目前已有节点的数量
哈希表节点
1 typedef struct dictEntry { 2 // 键 3 void *key; 4 // 值 5 union{ 6 void *val; 7 uint64_tu64; 8 int64_ts64; 9 } v; 10 // 指向下个哈希表节点,形成链表 11 struct dictEntry *next; 12 } dictEntry;
-
- key属性保存着键值对中的键,v属性保存着键值对中的值
- 键值对中的值可以使指针val、一个uint64_t整数,或是一个int64_t整数
- next是指向另一个哈希表节点的指针,用以解决多个哈希值冲突问题
下图为将两个索引值相同的键连在一起
字典结构
1 typedef struct dict { 2 // 类型特定函数 3 dictType *type; 4 // 私有数据 5 void *privdata; 6 // 哈希表 7 dictht ht[2]; 8 // rehash索引 9 //当rehash不在进行时,值为-1 10 in trehashidx; /* rehashing not in progress if rehashidx == -1 */ 11 } dict; 12 13 typedef struct dictType { 14 // 计算哈希值的函数 15 unsigned int (*hashFunction)(const void *key); 16 // 复制键的函数 17 void *(*keyDup)(void *privdata, const void *key); 18 // 复制值的函数 19 void *(*valDup)(void *privdata, const void *obj); 20 // 对比键的函数 21 int (*keyCompare)(void *privdata, const void *key1, const void *key2); 22 // 销毁键的函数 23 void (*keyDestructor)(void *privdata, void *key); 24 // 销毁值的函数 25 void (*valDestructor)(void *privdata, void *obj); 26 } dictType;
-
- type 属性是一个指向dictType结构的指针,每个dictType机构保存了一簇用于操作特定类型键值对的函数,Redis货位用途不同的字典设置不同的类型特定函数。
- privdata 属性保存了需要传给那些类型特定函数的可选参数。
- ht 属性是一个长度为2的数组,数组中的每个元素都是一个哈希表,一般情况下自字典只使用ht[0],ht[1]只会在进行rehash时使用.
- trehashidx 属性记录了rehash目前的进度,如果没有进行rehash则它的值为-1。
下图为普通状态下的字典结构
当一个新的键值对要添加到字典中去时,会涉及到一系列的操作,如计算索引、解决冲突、扩容等等,下面对这些操作进行描述。
1、哈希算法
添加键值对时,首先要根据键值对的键计算出哈希值和索引值,然后再根据索引值进行放入
1 #使用字典设置的哈希函数,计算键key的哈希值 2 hash = dict->type->hashFunction(key); 3 #使用哈希表的sizemask属性和哈希值,计算出索引值 4 #根据情况不同,ht[x]可以是ht[0]或者ht[1] 5 index = hash & dict->ht[x].sizemask;
2、结局键冲突
当有两个或以上数量的键值被分配到了哈希表数组的同一个索引上时,就发生了键冲突。
Redis的哈希表使用单向链表解决键冲突问题,每个新的键总是添加到单项链表的表头。
3、rehash(扩展或收缩)
哈希表具有负载因子(load factor),其始终需要保持在一个合理的范围之内,当hashI表保存的键值对过多或过少时,就需要对哈希表进行rehash(重新散列)操作,步骤许下
(1) 为字典的ht[1]分配空间,空间大小:如果是扩展操作则为ht[0].used * 2 ,也就是扩展为当前哈希表已使用空间的1倍;如果是收缩,则减小1倍。
(2) 将ht[0]内的数据重新计算哈希值和索引,并放到新分配的ht[1]空间上。
(3) 全部迁移完成后,将ht[1]设置为ht[0],释放ht[0]并创建一个空白的哈希表为ht[1],为下次rehash做准备。
4、哈希表的扩展与收缩触发条件
(1) 服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等等于1。
(2) 服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5。
以上条件中任意一条被满足,程序自动开始对哈希表进行扩展;
负载因子算法:负载因子 = 哈希表以保存的节点数量 / 哈希表大小
当负载因子小于0.1时,程序自动进行收缩操作。
5、渐进式rehash
渐进式rehash就是,当ht[1]的键值对向ht[1]迁移的过程中,如果数据量过大,则不能一次性迁移, 否则会对服务器性能造成影响,而是分成多次,渐进式的进行迁移。
在rehash期间,会维持一个索引计数器rehashidx,并把每次的迁移工作分配到了添加、删除、查找、更新操作中,当rehash工作完成后rehashidx会增加1,这样所有的ht[0]的值全部迁移完成后,程序会将rehashidx这是为-1,标识最终的rehash完成。
6、渐进式rehash之情期间的表操作
由于渐进式rehash期间,ht[0]和ht[1]中都有数据,当查找时,会先在ht[0]中进行,没找到继续到ht[1]中找;而添加操作一律会添加到ht[1]中。
字典总结:
Redis字典底层机构实现与java(1.6之前) 中的hashmap非常相像,都是使用单项链表解决键冲突问题。
个人疑问:jdk1.8以上已经是用红黑树解决多个键冲突问题,不知redis的键冲突是否也可以用红黑树?
5、跳跃表
跳跃表(skiplist)数据结构特点是每个节点中有多个指向其他节点的指针,从而快速访问节点。
跳跃表结构由跳跃表节点(zskiplistNode)和zskiplist两个结构组成
跳跃表节点
1 typedef struct zskiplistNode { 2 // 层 3 struct zskiplistLevel { 4 // 前进指针 5 struct zskiplistNode *forward; 6 // 跨度 7 unsigned int span; 8 } level[]; 9 // 后退指针 10 struct zskiplistNode *backward; 11 // 分值 12 double score; 13 // 成员对象 14 robj *obj; 15 } zskiplistNode;
-
- 层:为一个数组,数组中的每个数据都包含前进指针和跨度。
- 前进指针:指向表尾方向的其他节点的指针,用于从表头方向到表尾方向快速访问节点。
- 跨度:记录两个节点之间的距离,跨度越大,两个节点相聚越远,所有指向NULL的前进指针的跨度都为0。
- 后退指针:用于从表尾节点向表头节点访问,每个节点都有后退指针,并且每次只能后退一个节点。
- 分值:节点的分值是一个double类型的浮点数,跳跃表中的说有分值按从小到大排列。
- 成员对象:是一个指向字符串的指针,字符串则保存着一个SDS值。
跳跃表
1 typedef struct zskiplist { 2 // 表头节点和表尾节点 3 structz skiplistNode *header, *tail; 4 // 表中节点的数量 5 unsigned long length; 6 // 表中层数最大的节点的层数 7 int level; 8 } zskiplist;
-
- header 指向跳跃表的表头节点,tail指向跳跃表的表尾节点,level记录节点中的最大层数(不含表头节点),length跳跃表包含节点数量(不含表头节点)。
- 跳跃表由很多层构成(L1、L2 ...),每个层都带有两个属性前进指针和跨度。
- 每个节点都包含成员对象(obj)、分值(score)、后退指针(backward),头结点也包含这些属性但不会被用到
在此处只是介绍跳跃表的结构相关,关于跳跃表的层的形成,对象的插入、删除、查询等操作的原理在此处不做详解,另外会有文章进行说明。
6、整数集合
整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数元素,并且元素的个数不多时,Redis就会使用整数集合作为集合键的底层实现。
整数集合可以保存int16_t、int32_t、int64_t的整数值,并且不会出现重复元素
1 typedef struct intset { 2 // 编码方式 3 uint32_t encoding; 4 // 集合包含的元素数量 5 uint32_t length; 6 // 保存元素的数组 7 int8_t contents[]; 8 } intset;
-
- contents数组存储的是集合中的每个元素,他的类型是int8_t,但存储数据的实际类型取决于编码方式encoding
- encoding编码方式有三种INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT64分别对应的是int16_t、int32_t、int64_t类型
- length记录整数集合的元素数量,即contents数组的长度
整数集合的升级操作
整数集合中原来保存的是小类型(如:int16_t)的整数,当插入比其类型大(如:int_64_t)的整数时,会把整合集合里的元素的数据类型都转换成大的类型,这个过程称为升级
升级整数集合并添加新元素步骤如下:
(1)根据新元素的类型,扩展整数集合的底层数据的空间大小,并为新元素分配空间。
(2)将现有的所有元素的类型转换成与新元素相同的类型,保持原有数据有序性不变的情况下,把转换后的元素放在正确的位置上。
(3)将新元素添加到数组里。
新元素引发升级,所以新元素要么比所有元素都大,要么比所有元素都小。
-
- 当小于所有元素时,新元素放在底层数组的最开头
- 当大于所有元素时,新元素放在底层数据的最末尾
升级操作的好处
-
- 提升整数的灵活性,可以任意的向集合中放入3中不同类型的整数,而不用担心类型错误。
- 节约内存,整数集合中只有大类型出现的时候才会进行升级操作。
整数集合不支持降级操作
7、压缩列表
压缩列表(ziplist)是Redis为了节约内存而开发,是一系列特殊编码的连续内存块组成的顺序型数据结构。
一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值。
下图为压缩列表的结构
每个压缩列表含有若干个节点,而每个节点都由三部分构成,previous_entry_length、encoding、content,如图:
-
- previous_entry_length 存储的是前一个节点的长度,由于压缩列表内存块连续,使用此属性值可以计算前一个节点的地址,压缩列表就是使用这一原理进行遍历。
- previous_entry_length 如果前一节点长度小于254字节,那么previous_entry_length属性本身长度为1字节,存储的指就是前一节点的长度;如果大于254个字节,那么previous_entry_length属性本身长度为5个字节,前一个字节为0xFE(十进制254),之后四个字节存储前一节点的长度。
- encoding 记录本节点的content属性所保存数据的类型及长度,其本身长度为一字节、两字节或五字节,值得最高位为00、01或10的是字节数组的编码,最高位以11开头的是整数编码。
- content 保存节点的值,可以是一个字节数组或者整数。
连锁更新
当对压缩列表进行添加节点或删除节点时有可能会引发连锁更新,由于每个节点的 previous_entry_length 存在两种长度1字节或5字节,当所有节点previous_entry_length都为1个字节时,有新节点的长度大于254个字节,那么新的节点的后一个节点的previous_entry_length原来为1个字节,无法保存新节点的长度,这是就需要进行空间扩展previous_entry_length属性由原来的1个字节增加4个字节变为5个字节,如果增加后原节点的长度超过了254个字节则后续节点也要空间扩展,以此类推,最极端的情况是一直扩展到最后一个节点完成;这种现象称为连锁更新。在日常应用中全部连锁更新的情况属于非常极端的,不常出现。
8、总结
Redis的底层数据结构共有六种,简单动态字符串(SDS)、链表、字典、跳跃表、整数集合、压缩列表。
Redis中的五大数据类型的底层就是由他们中的一种或几种实现,数据的存储结构最终也会落到他们上。
可是在redis命令下使用 OBJECT ENCODING 命令查看键值对象的编码方式,也就是是以哪种结构进行的底层编码。