Redis數據類型之Hash(二)


前言:

     Redis hash是一個String類型的field和value的映射表。添加、刪除操作復雜度平均為O(1),為什么是平均呢?因為Hash的內部結構包含zipmap和hash兩種。hash特別適合用於存儲對象。相對於將對象序列化存儲為String類型,將一個對象存儲在hash類型中會占用更少的內存,並且可以方便的操作對象。為什么省內存,因為對象剛開始使用zipmap存儲的。

    1. zipmap

        zipmap其實並不是hashtable,zip可以節省hash本身需要的一些元數據開銷。zipmap的添加、刪除、查找復雜度為O(n),但是filed數量都不多,所以可以說平均是O(1)。

        默認配置:
            hash-max-ziplist-entries 512  //filed最多512個
            hash-max-ziplist-value 64     //value最大64字節

        內存分配如下:

            例:"foo" => "bar", "hello" => "world":<zmlen><len>"foo"<len><free>"bar"<len>"hello"<len><free>"world"

          (1)zmlen:記錄當前zipmap的key-value對的數量。一個字節,因此規定其表示的數量只能為0~254,當zmlen>254時,就需要遍歷整個zipmap來得到key-value對的個數
          (2)len:記錄key或value的長度,有兩種情況,當len的第一個字節為0~254(注釋是253,我們以代碼為准)時,那么len就只占用這一個字節。若len的第一個字節為254時,那么len將用后面的4個字節來表示。因此len要么占用1字節,要么占用5字節。
          (3)free:記錄value后面的空閑字節數,將”foo” => “world”變為”foo” => “me” ,那么會導致3個字節的空閑空間。當free的字節數過大用1個字節不足以表示時,zipmap就會重新分配內存,保證字符串盡量緊湊。
          (4)end: 記錄zipmap的結束,0xFF

        zipmap創建:

    2.hash

       在Redis中,hash表被稱為字典(dictionary),采用了典型的鏈式解決沖突方法,即:當有多個key/value的key的映射值(每對key/value保存之前,會先通過類似HASH(key) MOD N的方法計算一個值,
  以便確定其對應的hash table的位置)相同時,會將這些value以單鏈表的形式保存;同時為了控制哈希表所占內存大小,redis采用了雙哈希表(ht[2])結構,並逐步擴大哈希表容量(桶的大小)的策略,
  即:剛開始,哈希表ht[0]的桶大小為4,哈希表ht[1]的桶大小為0,待沖突嚴重(redis有一定的判斷條件)后,ht[1]中桶的大小增為ht[0]的兩倍,並逐步(注意這個詞:”逐步”)將哈希表ht[0]中元素遷移(稱為“再次Hash”)到ht[1],
  待ht[0]中所有元素全部遷移到ht[1]后,再將ht[1]交給ht[0](這里僅僅是C語言地址交換),之后重復上面的過程。

     

  Redis哈希表的實現位於文件dict.h和dict.c中,主要數據結構如下:

#define DICT_NOTUSED(V) ((void) V)

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

typedef struct dictType {
    unsigned int (*hashFunction)(const void *key);
    void *(*keyDup)(void *privdata, const void *key);
    void *(*valDup)(void *privdata, const void *obj);
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    void (*keyDestructor)(void *privdata, void *key);
    void (*valDestructor)(void *privdata, void *obj);
} dictType;

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    int iterators; /* number of iterators currently running */
} dict;

  基本操作:
        Redis中hash table主要有以下幾個對外提供的接口:dictCreate、dictAdd、dictReplace、dictDelete、dictFind、dictEmpty等,而這些接口調用了一些基礎操作,包括:_dictRehashStep,_dictKeyIndex等

        Hash Table在一定情況下會觸發rehash操作,即:將第一個hash table中的數據逐步轉移到第二個hash table中。
      (1)觸發條件 當第一個表的元素數目大於桶數目且元素數目與桶數目比值大於5時,hash 表就會擴張,擴大后新表的大小為舊表的2倍。

/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
    /* Incremental rehashing already in progress. Return. */
    if (dictIsRehashing(d)) return DICT_OK;

    /* If the hash table is empty expand it to the initial size. */
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

    /* If we reached the 1:1 ratio, and we are allowed to resize the hash
     * table (global setting) or we should avoid it but the ratio between
     * elements/buckets is over the "safe" threshold, we resize doubling
     * the number of buckets. */
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
    {
        return dictExpand(d, d->ht[0].used*2);
    }
    return DICT_OK;
}

      (2)轉移策略 為了避免一次性轉移帶來的開銷,Redis采用了平攤開銷的策略,即:將轉移代價平攤到每個基本操作中,如:dictAdd、dictReplace、dictFind中,每執行一次這些基本操作會觸發一個桶中元素的遷移操作。在此,有讀者可能會問,如果這樣的話,如果舊hash table非常大,什么時候才能遷移完。為了提高前移速度,Redis有一個周期性任務serverCron,每隔一段時間會遷移100個桶。

    相關操作:

         1.hset,hmset,hsetnx
            hset命令用來將某個hash指定鍵的值,如果鍵不存在,則創建並設置對應的值,返回一個整數1,如果鍵已經存在,則對應的值將被覆蓋並返回整數0.
            hset hash_name field value 

127.0.0.1:6379> hset userid:1000 age 100
(integer) 1
127.0.0.1:6379> hset userid:1000 age 10
(integer) 0

            hmset命令和hset命令的作用相似,可以用來設置hash的鍵和值。不同的是hmset可以同時設置多個鍵值對。操作成功后hmset命令返回一個簡單的字符串“OK”。
            hset hash_name field value

127.0.0.1:6379> hmset userid:1000 name zhangsan age 10
OK

            hsetnx命令也用來在指定鍵不存在的情況下設置鍵值信息。如果鍵不存在,則Redis會先創建鍵,然后設置對應的值,操作成功后返回整數1。如果該鍵已經存在,則該命令不進行任何操作,返回值為0
            hsetnx hash_name field value

127.0.0.1:6379> HSETNX userid:1000 age 10
(integer) 0
127.0.0.1:6379> HSETNX userid:1000 weight 100
(integer) 1

         2.hget,hmget,hgetall
            hget命令用來獲取某個hash指定key的值。如果該鍵存在,直接返回對應的值,否則返回nil。
            hget hash_name field

127.0.0.1:6379> hget user:1000 name
(nil)
127.0.0.1:6379> hget userid:1000 name
"zhangsan"

          hmget命令和hget命令類似,用來返回某個hash多個鍵的值的列表,對於不存在的鍵,返回nil值。
          hmget hash_name field1 field2...

127.0.0.1:6379> hmget userid:1000 name age
1) "zhangsan"
2) "10"

          hgetall命令返回一個列表,該列表包含了某個hash的所有鍵和值。在返回值中,先是鍵,接下來的一個元素是對應的值,所以hgetall命令返回的列表長度是hash大小的兩倍。
          hgetall hash_name

127.0.0.1:6379> HGETALL userid:1000
1) "age"
2) "10"
3) "name"
4) "zhangsan"
5) "weight"
6) "100"

        3.hexists
           hexists命令用來判斷某個hash指定鍵是否存在,若存在返回整數1,否則返回0。
           hexists hash_name field

127.0.0.1:6379> HEXISTS userid:1000 name
 integer) 1
127.0.0.1:6379> HEXISTS userid:1000 sex
(integer) 0

        4.hlen
           hlen命令用來返回某個hash中所有鍵的數量。
           hlen hash_name

127.0.0.1:6379> hlen userid:1000
(integer) 3

         5.hdel
            hdel命令用來刪除某個hash指定的鍵。如果該鍵不存在,則不進行任何操作。hdel命令的返回值是成功刪除的鍵的數量(不包括不存在的鍵)。
            hdel hash_name field      

127.0.0.1:6379> hlen userid:1000
(integer) 3
127.0.0.1:6379> hdel userid:1000 age
(integer) 1
127.0.0.1:6379> hlen userid:1000
(integer) 2

         6.Hkeys,hvals
            hkeys命令返回某個hash的所有鍵,如果該hash不存在任何鍵則返回一個空列表。
            hkeys hash_name
            hvals命令返回某個hash的所有值的列表。
            hvals hash_name

127.0.0.1:6379> hkeys userid:1000
1) "name"
2) "weight"
127.0.0.1:6379> hvals userid:1000
1) "zhangsan"
2) "100"

         7.hincrby,hincrbyfloat
            這兩個命令都用來對指定鍵進行增量操作,不同的是hincrby命令每次加上一個整數值,而hincrbyfloat命令每次加上一個浮點值。操作成功后返回增量操作后的最終值
            hincrby hash_name field i
            hincrbyfloat hash_name field f

127.0.0.1:6379> HINCRBY userid:1000 weight 10
(integer) 110
127.0.0.1:6379> HINCRBYFLOAT userid:1000 weight 10.0
"120"

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM