Redis——ハッシュ

序文

ハッシュ テーブルはキーと値のデータ構造であり、O(1) の時間計算量でデータをクエリできます。指定したキーを渡すと、広く使用されている指定した値をクエリできます。

ただし、容量が決まっているため、ハッシュの衝突が発生します。Redis は、ハッシュの衝突を解決するためにジッパーを使用します。

データが増加すると、元の容量ではデータ ストレージを満たせなくなります。Redis は、特定の条件が満たされた後に拡張をトリガーします。拡張方法は、2 つのハッシュ テーブルを使用して段階的に拡張します。

要素を挿入

ハッシュ テーブルの最下層は通常、データ構造として配列を使用します。要素を挿入するときは、最初に要素のハッシュ値が計算され、次にそのハッシュ値を使用して配列の長さを剰余します。得られる結果は次のとおりです。配列に挿入される要素の添字。

画像.png

構造

struct dict {
    // hash表类型,不同类型有不同的比较函数
    dictType *type;
    
    // 2 个 hash 表,
    dictEntry **ht_table[2];
    
    // 2 个哈希 table 里面各自存了多少个元素
    unsigned long ht_used[2];
    
    // 渐进式 rehash 现在处理的哈希槽索引值
    long rehashidx;
    
    // 用来暂停渐进式rehash的开关
    int16_t pauserehash;
    
    // 记录两个哈希table的长度,实际是是记录2的n次方中的 n 这个值
    signed char ht_size_exp[2]; 
};

画像.png

  1. type はハッシュ テーブルの型であり、Java のインターフェイスと同様に、多くの関数インターフェイスを定義する構造体でもあります。インターフェースの実装クラスを渡す必要があります。
  2. ht_table は配列型で、再ハッシュするときに 2 つのハッシュ テーブルを使用する必要があるため、サイズは 2 です。
  3. ht_used、2 つのハッシュ テーブルのそれぞれに要素がいくつあるかを示します。
  4. rehanadx では、rehash によって処理されているハッシュ スロットを記録します。プログレッシブ リハッシュでは各操作に移行が割り当てられるため、現在どのスロットが処理されているかを記録する必要があります。
  5. 一時停止再ハッシュ、再ハッシュを一時停止するかどうか
  6. ht_size_exp、2 つのハッシュ テーブルの容量を示します。

ハッシュ テーブルの各要素の構造は次のとおりです。

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;     /* Next entry in the same hash bucket. */
    void *metadata[];           /* An arbitrary number of bytes (starting at a
                                 * pointer-aligned address) of size as returned
                                 * by dictType's dictEntryMetadataBytes(). */
} dictEntry;

ハッシュ テーブルの各要素には、キー、値、次の要素へのポインタの 3 つの部分が含まれます。次のノードへのポインタは、ハッシュの競合を解決するために使用されます。

value は共用体によって定義されます。つまり、その中の任意の型を指定できます。ポインター、64 ビット符号なし整数、16 ビット整数、または double 値を指定できます。この定義の利点は、整数型の場合、アドレスを記録するためにポインターを使用する必要がないため、値をエントリに直接埋め込んでメモリを節約できることです。

ハッシュ衝突

在插入函数那个段落中,可以知道每一个元素都会使用 key 计算 hash 值,然后再求模得到可以插入的数组坐标。但是 hash 值是有限的、数组容量是有限的,而数据是无限的,那么就会造成两种情况:

  1. 不同的数据存在一样的 hash 值
  2. 不一样的 hash 值求模得到的坐标相同

这两种情况都会将不同的元素都插入到数组的同一个坐标下,这是不允许出现的情况。

为了解决这个问题,redis 使用拉链法。拉链法就是在每一个元素中都记录一个指针,这个指针会指向下一个坐标相同的元素。当一个元素计算得到的坐标下已经有元素时,就会将数组中的元素的指针指向新的元素。从而得到下面这种情况

画像.png

当需要查看 key 为 jshd 的元素时,首先会通过计算找到坐标 1,然后比较 key 值,发现 a 不等于 jshd,那么就沿着链表继续往下比较 key 值。直到找到或者 NULL 为止。

所以,hash 表的底层是数组和链表的组合。

我们可以试想这么一种极端情况,所有的元素都在同一个坐标下,那么 hash 表就会退化成链表,我们执行查询操作时,时间复杂度就会从 O(1) 退化为 O(n)。

为了解决这个问题,我们就需要进行扩容。

扩容

Redis 会在两种情况下进行扩容;

  • 当元素数量 / 容量 >= 1,并且没有执行 RDB 快照和 AOF 重写时,会进行扩容
  • 当元素数量 / 容量 > 5,那么将会直接进行扩容。

还记得之前介绍的,在一个 hash 结构中,会有两个 hash 表吗?正常情况下,只有一个 hash 表1 是正常使用的,另外一个表2 并不会分配空间。

但是当需要扩容的时候,会执行以下步骤:

  1. 给表 2 分配内存空间,通常是表 1 的两倍
  2. 然后将表 1 的数据逐渐迁移到表 2 中,迁移完成后,将表 1 的内存空间释放
  3. 切换表 2 为正常使用的表。

如果表 1 的元素数量很少,那么迁移的过程是非常快的,但是如果表 1 的元素数量很多,那么进行迁移的过程就会阻塞客户端的请求,此时 Redis 对外表现就是无法处理请求。这是无法接受的情况

漸進的な焼き直し

拡張時に表 1 のデータが移行されますが、表 1 のデータ量が多い場合、Redis サービスに影響を与えます。そのため、Redis は移行にプログレッシブ リハッシュと呼ばれる方法を使用します

プログレッシブ リハッシュでは、すべてのデータが一度に移行されるのではなく、ハッシュ テーブルへのアクセスごとに複数の要素が移行されます。ある時点で、表 1 のすべてのデータが表 2 に移行されます。

このようにして、移行作業が各操作に割り当てられるため、操作にかかる時間が大幅に短縮されます。

移行中、両方のテーブルのデータは通常どおり使用されます。削除、更新、およびクエリ操作を実行する場合、これらの操作は両方のテーブルで実行されます。たとえば、要素をクエリする場合、テーブル 1 でクエリが実行され、クエリできない場合はテーブル 2 でクエリが続行されます。

ただし、挿入操作の場合、新しい要素のみが table2 に挿入されます。これにより、表 1 の要素が増加せずに減少するだけになり、表 1 のすべての要素を特定の時点で移行できるようになります。

挿入が十分に速い場合、テーブル 1 のデータがまだ移行されておらず、テーブル 2 を拡張する必要があるということが起こりますか?

Redis が再ハッシュの進行中であることを検出すると、再ハッシュは再展開されません。

おすすめ

転載: juejin.im/post/7245936314850459706