Redisには6つの基本的なデータ構造があります。
- シンプルダイナミックストリング(SDS)
- 二重リンクリスト(リンクリスト/クイックリスト)
- ハッシュ
- スキップリスト(焦点を当て、よくあるインタビュー)
- 整数のリスト(intset)
- 圧縮リスト(ziplist)
- 基数木(ストリームで使用)
1.シンプルダイナミックストリング(SDS)
1.データ構造
RedisのすべてのキータイプはSDSであり、値は異なるタイプである可能性があります。もちろん、一般的な値タイプはSDSです。
(画像ソース:Geek Time Redis列)
といった:RPUSH zoo "dog" "panda" "cat"
キー:zooは文字列オブジェクトタイプ、値:dog、panda、catはすべて文字列オブジェクトタイプです。
SDS構造の定義とデータ構造は次のとおりです。
- 無料:未使用のスペースを割り当てます。
- len:割り当てられたスペースのサイズ
- buf:char型の配列
注:SDS構造は、新しいバージョンで調整されています。
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; // 已使用的空间大小
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[]; // 字节数组 保存字符串
};
2. C文字列とは異なります(文字列がそのようなデータ構造として定義されているのはなぜですか?)
-
O(n)からO(1)までの文字列の長さを取得すると、lenフィールドを直接読み取ることができます(Redisはパフォーマンスを重視します)。
-
スペース割り当て中のバッファオーバーフローの回避:Redisは、データを格納する前にSDSスペースが十分かどうかを確認し、十分でない場合は、最初にスペースを拡張してからスプライスします。
Redisがメモリを割り当てると、追加のメモリが割り当てられます。例:SDSの長さが1M未満の場合、lenサイズの未使用スペースがさらに割り当てられ、buf配列の長さが2 * len +1になります。SDSの長さが1Mを超える場合、1M多くが割り当てられます。このようにして、Redisは、文字列拡張操作を継続的に実行するために必要なメモリ再割り当ての数を減らすことができます。
2、二重リンクリストリンクリスト
Redisのリンクリスト構造は、リスト構造+二重リンクリストノードで構成されています。
Redisのリンクリストの特徴は次のとおりです。
- ダブルエンド:各リンクリストノードにはprevとnextポインターがあり、ノードのフロントノードとバックノードを取得する時間計算量はO(1)です。
- テーブルヘッドポインタとテーブルテールポインタ:ヘッドとテールを介してリンクリストのヘッドノードとテールノードを取得する複雑さはO(1)です。
- len:リンクリストノードの長さを取得します
注:バージョン3.2以降では、リンクリストのフロントポインターとバックポインターが多くのスペースを占めるため、リンクリストの代わりにクイックリストが使用されます。
クイックリストはリストをセグメントに分割します。各セグメントはポインタで接続されたzipリストです。ziplistが大きすぎる場合は、さらに圧縮されます。
typedef struct quicklistNode {
struct quicklistNode *prev;
struct quicklistNode *next;
unsigned char *zl; // 保存的数据 指向ziplist
unsigned int sz; /* ziplist size in bytes */
unsigned int count : 16; /* ziplist数量 */
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
unsigned int recompress : 1; /* was this node previous compressed? */
unsigned int attempted_compress : 1; /* node can't compress; too small */
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count; /* 所有ziplists中元素数量 */
unsigned long len; /*quicklistNodes 个数 */
int fill : QL_FILL_BITS; /* fill factor for individual nodes */
unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off */
unsigned int bookmark_count: QL_BM_BITS;
quicklistBookmark bookmarks[];
} quicklist;
3、ハッシュテーブル-ハッシュテーブル
1.ハッシュテーブルのデータ構造
Redisのディクショナリdictは、ハッシュテーブルdictht(テーブル+エントリ配列)を使用して実装されます。(辞書は、ハッシュテーブルの再ハッシュを管理するときに使用されるものとして理解できます。ハッシュテーブルの理解に焦点を合わせてください)
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2]; // 哈希表,只会用ht[0], ht[1]用于rehash
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;
/* 每个字典会有2个hash表的结构,为rehashing的时候使用。 */
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
// 哈希表中的entry节点
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
Redisディクショナリは2つのハッシュテーブルを定義します。通常、ht [0]ハッシュテーブルのみが使用され、再ハッシュ時にはht [1]ハッシュテーブルが使用されます。
(1)ハッシュアルゴリズム-要素を追加するプロセス
- まず、キーに従ってハッシュ値を計算します:ハッシュ
- 次に、ハッシュバケット内の位置を計算します:hash&ht [0] .sizemark。(sizemark = size-1)
(2)キーの競合を解決する
Redisハッシュテーブルはチェーンアドレス方式を使用してキーの競合を解決し、新しいノードはチェーンの先頭に配置されます。
(3)リハッシュ(プログレッシブハッシュ)
ハッシュテーブルの負荷率が大きすぎて、キーと値のペアの数が多すぎるか少なすぎる場合、Redisはハッシュテーブルを調整して、使用メモリの2倍のサイズを作成します。Redisはht [1]> =を割り当てます。 ht [0] .used * 2 2 ^ nスペース、
ただし、要素の再ハッシュとコピーのプロセスには時間がかかるため、Redisスレッドがブロックされ、他のリクエストを処理できなくなります。
Redisは、サーバーのパフォーマンスに対する再ハッシュの影響を回避するために、プログレッシブアプローチを採用しています。
- 検索、更新、削除はすべて最初にht [0]ハッシュテーブルで操作され、ht [0]のht [1]では操作されません。
- 新しい要素はht [1]でのみ操作されます。
- この期間中、ht [0]のキー値は徐々に再ハッシュされ、その後ht [1]にコピーされます。
注:ハッシュテーブルの再ハッシュプロセスのソースコードは、記事の最後にあります。
4、スキップリスト(スキップリスト)
ジャンプテーブルは順序付けられたデータ構造です。マルチレベルインデックスは、時間空間のアイデアを通じてリンクリストに追加されます。インデックスは他のノードのデータをすばやく見つけることができます。時間の複雑さはO(logN)です。 〜O(N)。
ジャンプテーブルは、主に順序セット(Sorted Set-Zset)で使用されます。
// 跳表
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
// 跳表节点
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
// Zset数据结构使用hash表和跳表两种数据结构
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
1.ジャンプテーブルのデータ構造
スキップリストは、zskiplist(スキップリスト)とzskiplistNode(スキップリストノード)で構成されています。
zskiplistは、スキップリスト情報(ヘッドノード、テールノード、長さ)を保存するために使用され、zskiplistNodeは、スキップリストノードを表すために使用されます。
zskiplist:スキップリストのヘッドノードとテールノードにすばやくアクセスして、スキップリストノードの数を取得できます。
- header:ジャンプテーブルのヘッダーノードを指します。
- tail:ジャンプリストのテールノードをポイントします。
- level:現在のホップテーブルノードで最大レベルのノードを記録します(1〜32の間では、ヘッドノードはカウントされません)。
- 長さ:ホップテーブルノードの数(テーブルヘッドノードはカウントされません)
zskipListNode:
-
レベル:ノードはL1、L2 ..で表されます。各レイヤーには、フォワードポインターとスパンの2つの属性があります。フォワードポインタは同じ層数の次のノードを指し、その数はスパンを表します。
-
後方ポインタ(後方):現在のノードの前のノードを指します。
-
スコア:ノードはスコアの観点から小さいものから大きいものへとソートされます。
-
メンバーオブジェクト(ele):各ノードのo1とo2は、ノードによって保存されたメンバーオブジェクトです。メンバーオブジェクトはポインタであり、最終的にはSDS文字列オブジェクトを指します。
スコアは同じでもかまいませんが、メンバーオブジェクトは一意である必要があります。同じスコアが、ディクショナリ内のメンバーオブジェクトのサイズで並べ替えられます。
Redisが赤黒木ではなくスキップテーブルを使用するのはなぜですか?
- ジャンプテーブルクエリの効率Ologn
- スキップテーブルは、赤黒木よりも効率的なOlogn時間範囲内で検索できます。
- ジャンプテーブルのコードは、赤と黒の木よりも単純ではありません
Redisジャンプテーブルの紹介:
https://redisbook.readthedocs.io/en/latest/internal-datastruct/skiplist.html
5、整数セット(intset)
整数セットは、セットキー(セット)の基本的な実装の1つであり、セット内の要素が繰り返されないこと、配列要素が小さいものから大きいものへと配置され、16(短い)、32の数を格納できることを保証します。 (int)、および64(long)サイズ。
エンコーディングに応じて、16バイト、32バイト、または64バイトの整数を格納するかどうかを決定します。
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
1.アップグレードルール
新しい要素を追加するときに、新しい要素タイプが既存の要素タイプよりも長い場合は、アップグレードします。最初にスペースを割り当てて基になる要素を再割り当てし、次に新しく追加された要素を含む元の要素を新しいスペースInに配置します。順序は変更しないでください。
例:元のセットが3つのint_16要素を保存すると仮定すると、占有スペースは3 * 16 = 48ビットです。新しいint-32要素を追加する場合は、アップグレードする必要があります。まず、スペースを再割り当てします:4 * 32 = 128ビット、次に要素を新しい位置に徐々に移動し、最後に整数セットのエンコーディングをint_32に変更します。 。
アップグレードにより柔軟性が向上します。3つの要素の異なるタイプによって引き起こされるエラーを心配する必要がなく、メモリを節約することもできます(最初はint_16であり、必要な場合にのみアップグレードされます)。
6つの圧縮リスト(ziplist)
リストを圧縮する場合、リストキーとセットキーの基本的な実装の1つは配列として理解できますが、リストの長さと数は最初と最後に格納されます。小さな要素または整数値を格納するときに使用されます。最初と最後のノードを見つける時間の複雑さはO(1)であり、他の要素を見つけることの複雑さはO(N)です。
データ構造
1.圧縮リストには任意の数のノードを含めることができ、各ノードにはバイト配列または整数値が格納されます。
- zlbytes:圧縮リストの全長
- zltail:テーブルのエンドノードのentry5までの距離。
- zllen:リストノードの数。
- エントリ:圧縮リストノード。
- zlend:終了フラグ。
2.圧縮リストノード
圧縮リストノードエントリは、バイト配列または整数値を格納できます。
- previous_entry_length:前のノードの長さ。
- エンコーディング:コンテンツデータ型を記録します。たとえば、11000000はint16型の整数を表します。
- content:バイト配列「helloworld」または整数にすることができます。
ハッシュテーブル再ハッシュのソースコード
int dictRehash(dict *d, int n) {
int empty_visits = n*10; /* Max number of empty buckets to visit. */
if (!dictIsRehashing(d)) return 0;
// 确定 哈希表ht[0]有数据:used != 0
while(n-- && d->ht[0].used != 0) {
dictEntry *de, *nextde;
// rehashidx: 找到哈希桶中第1个元素不为null的位置
while(d->ht[0].table[d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}
// de:需要拷贝的哈希桶位置的首节点
de = d->ht[0].table[d->rehashidx];
while(de) {
uint64_t h;
// 先保存下个节点
nextde = de->next;
// 计算待拷贝的该节点de在新哈希表ht[1]在的hash值
h = dictHashKey(d, de->key) & d->ht[1].sizemask;
// 放到哈希桶的头位置,指向已经存在的节点
de->next = d->ht[1].table[h];
// 赋值
d->ht[1].table[h] = de;
// 更新ht新、老表已使用内存值
d->ht[0].used--;
d->ht[1].used++;
// 循环复制哈希桶中的下个结点
de = nextde;
}
// 复制完的哈希桶置手动释放内存,rehashidx+1 指向下个哈希桶
d->ht[0].table[d->rehashidx] = NULL;
d->rehashidx++;
}
/* 老表的used=0时表示完成扩容,将ht[1]的地址赋值给ht[0] */
if (d->ht[0].used == 0) {
zfree(d->ht[0].table);
d->ht[0] = d->ht[1];
_dictReset(&d->ht[1]);
d->rehashidx = -1;
return 0;
}
/* More to rehash... */
return 1;
}