前言:redis
Redis hash是一個String類型的field和value的映射表。添加、刪除操做複雜度平均爲O(1),爲何是平均呢?由於Hash的內部結構包含zipmap和hash兩種。hash特別適合用於存儲對象。相對於將對象序列化存儲爲String類型,將一個對象存儲在hash類型中會佔用更少的內存,而且能夠方便的操做對象。爲何省內存,由於對象剛開始使用zipmap存儲的。數據結構
1. zipmapui
zipmap其實並非hashtable,zip能夠節省hash自己須要的一些元數據開銷。zipmap的添加、刪除、查找複雜度爲O(n),可是filed數量都很少,因此能夠說平均是O(1)。this
默認配置:
hash-max-ziplist-entries 512 //filed最多512個
hash-max-ziplist-value 64 //value最大64字節spa
內存分配以下:3d
例:"foo" => "bar", "hello" => "world":<zmlen><len>"foo"<len><free>"bar"<len>"hello"<len><free>"world"code
(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的結束,0xFFserver
zipmap建立:對象
2.hashblog
在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"