天天看點

Redis哈希表的實作要點

Redis哈希表的實作要點

雜湊演算法的選擇

針對不同的key使用不同的hash算法,如對整型、字元串以及大小寫敏感的字元串分别使用不同的hash算法;

整型的Hash算法使用的是Thomas Wang's 32 Bit / 64 Bit Mix Function ,這是一種基于位移運算的散列方法。基于移位的散列是使用Key值進行移位操作。通常是結合左移和右移。每個移位過程的結果進行累加,最後移位的結果作為最終結果。這種方法的好處是避免了乘法運算,進而提高Hash函數本身的性能。

unsigned int dictIntHashFunction(unsigned int key)
{
    key += ~(key << 15);
    key ^=  (key >> 10);
    key +=  (key << 3);
    key ^=  (key >> 6);
    key += ~(key << 11);
    key ^=  (key >> 16);
    return key;
}           

字元串使用的MurmurHash算法,MurmurHash算法具有高運算性能,低碰撞率的特點,由Austin Appleby建立于2008年,現已應用到Hadoop、libstdc++、nginx、libmemcached等開源系統。2011年Appleby被Google雇傭,随後Google推出其變種的CityHash算法。

murmur是 multiply and rotate的意思,因為算法的核心就是不斷的乘和移位(x *= m; k ^= k >> r;)

unsigned int dictGenHashFunction(const void *key, int len) {
    /* 'm' and 'r' are mixing constants generated offline.
     They're not really 'magic', they just happen to work well.  */
    uint32_t seed = dict_hash_function_seed;
    const uint32_t m = 0x5bd1e995;
    const int r = 24;

    /* Initialize the hash to a 'random' value */
    uint32_t h = seed ^ len;

    /* Mix 4 bytes at a time into the hash */
    const unsigned char *data = (const unsigned char *)key;

    while(len >= 4) {
        uint32_t k = *(uint32_t*)data;

        k *= m;
        k ^= k >> r;
        k *= m;

        h *= m;
        h ^= k;

        data += 4;
        len -= 4;
    }

    /* Handle the last few bytes of the input array  */
    switch(len) {
    case 3: h ^= data[2] << 16;
    case 2: h ^= data[1] << 8;
    case 1: h ^= data[0]; h *= m;
    };

    /* Do a few final mixes of the hash to ensure the last few
     * bytes are well-incorporated. */
    h ^= h >> 13;
    h *= m;
    h ^= h >> 15;

    return (unsigned int)h;
}           

一個好的hash算法需要滿足兩個條件:

1) 性能高,運算足夠快;

2) 相鄰的資料hash後分布廣;即使輸入的鍵是有規律的,算法仍然能給出一個很好的随機分布性;

比如:murmur計算"abc"是1118836419,"abd"是413429783。而使用Horner算法,"abc"是96354, "abd"就比它多1(96355);

rehash

負載因子 = 目前結點數/桶的大小,超過1表示肯定有碰撞了;碰撞的結點,通過連結清單拉鍊起來;

所有哈希表的初始桶的大小為4,根據負載因子的變化進行rehash,重新配置設定空間(擴充或收縮)

當hash表的負載因子超過1後,進行擴充(小于0.01時,進行收縮);

所謂擴充,就是建立一個hash表2,将桶的數量增大(具體增大為:第一個大于等于usedSize的2的n次冥);然後将hash表1中結點都轉移到hash表2中;

rehash的觸發條件:

當做BGSAVE或BGREWRITEEOF時,負載因子超過5時觸發rehash,

沒有BGSAVE或BGREWRITEEOF時,負載因子超過1時觸發rehash;

在BGSAVE或BGREWRITEEOF時,使用到Linux的寫時複制,如果這時候做rehash,将會好用更多的記憶體空間(沒有變化的結點用一份,變化的結點複制一份)

漸進式rehash

一個hash表中的資料可能有幾百上千萬,不可能一次rehash轉移完,需要分批逐漸轉移;

在rehash的過程中,對redis的查詢、更新操作首先會在hash0中查找,沒有找到,然後轉到hash1中操作;

對于插入操作,直接插入到hash1中;最終目标是将hash表1變為空表,rehash完成;

value的存儲

鍵值對的實作,value 是一個union,對整型和字元串使用不同的存儲對象;

// 鍵
void *key;

// 值
union {
    void *val;
    uint64_t u64;
    int64_t s64;
} v;           

ref:

《Hash 函數概覽》http://www.oschina.net/translate/state-of-hash-functions

《redis設計與實作》

Posted by: 大CC | 18NOV,2015

部落格:blog.me115.com [訂閱]

Github:大CC

繼續閱讀