Redis 哈希鍵命令實現(t_hash)
1. 哈希命令介紹
Redis 所有哈希命令如下表所示:Redis 哈希命令詳解
序號 | 命令及描述 |
---|---|
1 | HDEL key field2 [field2]:刪除一個或多個哈希表字段 |
2 | HEXISTS key field:查看哈希表 key 中,指定的字段是否存在。 |
3 | HGET key field:獲取存儲在哈希表中指定字段的值。 |
4 | HGETALL key:獲取在哈希表中指定 key 的所有字段和值 |
5 | HINCRBY key field increment:爲哈希表 key 中的指定字段的整數值加上增量 increment 。 |
6 | HINCRBYFLOAT key field increment:爲哈希表 key 中的指定字段的浮點數值加上增量 increment 。 |
7 | HKEYS key:獲取所有哈希表中的字段 |
8 | HLEN key:獲取哈希表中字段的數量 |
9 | HMGET key field1 [field2]:獲取所有給定字段的值 |
10 | HMSET key field1 value1 [field2 value2 ]:同時將多個 field-value (域-值)對設置到哈希表 key 中。 |
11 | HSET key field value:將哈希表 key 中的字段 field 的值設爲 value 。 |
12 | HSETNX key field value:只有在字段 field 不存在時,設置哈希表字段的值。 |
13 | HVALS key:獲取哈希表中所有值 |
14 | HSCAN key cursor [MATCH pattern][COUNT count]: 迭代哈希表中的鍵值對。 |
2. 哈希類型的實現
之前在redis對象系統源碼剖析和註釋中提到,一個哈希類型的對象的編碼有兩種,分別是OBJ_ENCODING_ZIPLIST和OBJ_ENCODING_HT。
編碼—encoding | 對象—ptr |
---|---|
OBJ_ENCODING_ZIPLIST | 壓縮列表實現的哈希對象 |
OBJ_ENCODING_HT | 字典實現的哈希對象 |
但是默認創建的哈希類型的對象編碼爲OBJ_ENCODING_ZIPLIST,OBJ_ENCODING_HT類型編碼是通過達到配置的閾值條件後,進行轉換得到的。
閾值條件爲:
/* redis.conf文件中的閾值 */
hash-max-ziplist-value 64 // ziplist中最大能存放的值長度
hash-max-ziplist-entries 512 // ziplist中最多能存放的entry節點數量
一個哈希對象的結構定義如下:
typedef struct redisObject {
//對象的數據類型,字符串對象應該爲 OBJ_HASH
unsigned type:4;
//對象的編碼類型,分別爲 OBJ_ENCODING_ZIPLIST 或 OBJ_ENCODING_HT
unsigned encoding:4;
//暫且不關心該成員
unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */
//引用計數
int refcount;
//指向底層數據實現的指針,指向一個dict的字典結構
void *ptr;
} robj;
例如,我們創建一個 user:info 哈希鍵,有三個字段,分別是name,sex,passwd。
127.0.0.1:6379> HMSET user:info name Mike sex male passwd 123456
OK
127.0.0.1:6379> HGETALL user:info
1) "name"
2) "Mike"
3) "sex"
4) "male"
5) "passwd"
6) "123456"
我們以此爲例,查看redis的哈希對象的空間結構。
根據這些信息的大小,redis應該爲其創建一個編碼爲OBJ_ENCODING_ZIPLIST的哈希對象。如下圖所示:
壓縮列表中的entry節點,兩兩組成一個鍵值對。
如果這個哈希對象所存儲的鍵值對或者ziplist的長度超過配置的限制,則會轉換爲字典結構,這寫閾值條件上面已經列出,而爲了說明編碼爲 OBJ_ENCODING_HT 類型的哈希對象,我們仍用上面的 user:info 對象來表示一個字典結構的哈希對象,哈希對象中的鍵值對都是字符串類型的對象。如下圖:
和列表數據類型一樣,哈希數據類型基於ziplist和hash table進行封裝,實現了哈希數據類型的接口:
/* Hash data type */
// 轉換一個哈希對象的編碼類型,enc指定新的編碼類型
void hashTypeConvert(robj *o, int enc);
// 檢查一個數字對象的長度判斷是否需要進行類型的轉換,從ziplist轉換到ht類型
void hashTypeTryConversion(robj *subject, robj **argv, int start, int end);
// 對鍵和值的對象嘗試進行優化編碼以節約內存
void hashTypeTryObjectEncoding(robj *subject, robj **o1, robj **o2);
// 從一個哈希對象中返回field對應的值對象
robj *hashTypeGetObject(robj *o, robj *key);
// 判斷field對象是否存在在o對象中
int hashTypeExists(robj *o, robj *key);
// 將field-value添加到哈希對象中,返回1,如果field存在更新新的值,返回0
int hashTypeSet(robj *o, robj *key, robj *value);
// 從一個哈希對象中刪除field,成功返回1,沒找到field返回0
int hashTypeDelete(robj *o, robj *key);
// 返回哈希對象中的鍵值對個數
unsigned long hashTypeLength(robj *o);
// 返回一個初始化的哈希類型的迭代器
hashTypeIterator *hashTypeInitIterator(robj *subject);
// 釋放哈希類型迭代器空間
void hashTypeReleaseIterator(hashTypeIterator *hi);
// 講哈希類型迭代器指向哈希對象中的下一個節點
int hashTypeNext(hashTypeIterator *hi);
// 從ziplist類型的哈希類型迭代器中獲取對應的field或value,保存在參數中
void hashTypeCurrentFromZiplist(hashTypeIterator *hi, int what, unsigned char **vstr, unsigned int *vlen, long long *vll);
// 從ziplist類型的哈希類型迭代器中獲取對應的field或value,保存在參數中
void hashTypeCurrentFromHashTable(hashTypeIterator *hi, int what, robj **dst);
// 從哈希類型的迭代器中獲取鍵或值
robj *hashTypeCurrentObject(hashTypeIterator *hi, int what);
// 以寫操作在數據庫中查找對應key的哈希對象,如果不存在則創建
robj *hashTypeLookupWriteOrCreate(client *c, robj *key);
這些函數接口的註釋請上github查看:哈希類型函數接口的註釋
3. 哈希類型的迭代器
和列表類型一樣,哈希數據類型也實現自己的迭代器,而且也是基於ziplist和字典結構的迭代器封裝而成。
typedef struct {
robj *subject; // 哈希類型迭代器所屬的哈希對象
int encoding; // 哈希對象的編碼類型
// 用ziplist編碼
unsigned char *fptr, *vptr; // 指向當前的key和value節點的地址,ziplist類型編碼時使用
// 用於字典編碼
dictIterator *di; // 迭代HT類型的哈希對象時的字典迭代器
dictEntry *de; // 指向當前的哈希表節點
} hashTypeIterator;
#define OBJ_HASH_KEY 1 // 哈希鍵
#define OBJ_HASH_VALUE 2 // 哈希值
- 創建一個迭代器
// 返回一個初始化的哈希類型的迭代器
hashTypeIterator *hashTypeInitIterator(robj *subject) {
// 分配空間初始化成員
hashTypeIterator *hi = zmalloc(sizeof(hashTypeIterator));
hi->subject = subject;
hi->encoding = subject->encoding;
// 根據不同的編碼設置不同的成員
if (hi->encoding == OBJ_ENCODING_ZIPLIST) {
hi->fptr = NULL;
hi->vptr = NULL;
} else if (hi->encoding == OBJ_ENCODING_HT) {
// 初始化一個字典迭代器返回給di成員
hi->di = dictGetIterator(subject->ptr);
} else {
serverPanic("Unknown hash encoding");
}
return hi;
}
- 釋放迭代器
// 釋放哈希類型迭代器空間
void hashTypeReleaseIterator(hashTypeIterator *hi) {
// 如果是字典,則需要先釋放字典迭代器的空間
if (hi->encoding == OBJ_ENCODING_HT) {
dictReleaseIterator(hi->di);
}
zfree(hi);
}
- 迭代
/* Move to the next entry in the hash. Return C_OK when the next entry
* could be found and C_ERR when the iterator reaches the end. */
//講哈希類型迭代器指向哈希對象中的下一個節點
int hashTypeNext(hashTypeIterator *hi) {
// 迭代ziplist
if (hi->encoding == OBJ_ENCODING_ZIPLIST) {
unsigned char *zl;
unsigned char *fptr, *vptr;
// 備份迭代器的成員信息
zl = hi->subject->ptr;
fptr = hi->fptr;
vptr = hi->vptr;
// field的指針爲空,則指向第一個entry,只在第一次執行時,初始化指針
if (fptr == NULL) {
/* Initialize cursor */
serverAssert(vptr == NULL);
fptr = ziplistIndex(zl, 0);
} else {
/* Advance cursor */
// 獲取value節點的下一個entry地址,即爲下一個field的地址
serverAssert(vptr != NULL);
fptr = ziplistNext(zl, vptr);
}
// 迭代完畢或返回C_ERR
if (fptr == NULL) return C_ERR;
/* Grab pointer to the value (fptr points to the field) */
// 保存下一個value的地址
vptr = ziplistNext(zl, fptr);
serverAssert(vptr != NULL);
/* fptr, vptr now point to the first or next pair */
// 更新迭代器的成員信息
hi->fptr = fptr;
hi->vptr = vptr;
// 如果是迭代字典
} else if (hi->encoding == OBJ_ENCODING_HT) {
// 得到下一個字典節點的地址
if ((hi->de = dictNext(hi->di)) == NULL) return C_ERR;
} else {
serverPanic("Unknown hash encoding");
}
return C_OK;
}
4. 哈希命令的實現
上面都給出了哈希類型的接口,所以哈希類型命令實現很容易看懂,而且哈希類型命令沒有阻塞版的。
具體所有註釋請看:哈希類型命令的註釋
- Hgetall一類命令的底層實現
HKEYS、HVALS、HGETALL
void genericHgetallCommand(client *c, int flags) {
robj *o;
hashTypeIterator *hi;
int multiplier = 0;
int length, count = 0;
// 以寫操作取出哈希對象,若失敗,或取出的對象不是哈希類型的對象,則發送0後直接返回
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.emptymultibulk)) == NULL
|| checkType(c,o,OBJ_HASH)) return;
// 計算一對鍵值對要返回的個數
if (flags & OBJ_HASH_KEY) multiplier++;
if (flags & OBJ_HASH_VALUE) multiplier++;
// 計算整個哈希對象中的所有鍵值對要返回的個數
length = hashTypeLength(o) * multiplier;
addReplyMultiBulkLen(c, length); //發get到的個數給client
// 創建一個哈希類型的迭代器並初始化
hi = hashTypeInitIterator(o);
// 迭代所有的entry節點
while (hashTypeNext(hi) != C_ERR) {
// 如果取哈希鍵
if (flags & OBJ_HASH_KEY) {
// 保存當前迭代器指向的鍵
addHashIteratorCursorToReply(c, hi, OBJ_HASH_KEY);
count++; //更新計數器
}
// 如果取哈希值
if (flags & OBJ_HASH_VALUE) {
// 保存當前迭代器指向的值
addHashIteratorCursorToReply(c, hi, OBJ_HASH_VALUE);
count++; //更新計數器
}
}
//釋放迭代器
hashTypeReleaseIterator(hi);
serverAssert(count == length);
}
- HSTRLEN 命令實現
Redis 3.2版本以上新加入的
void hstrlenCommand(client *c) {
robj *o;
// 以寫操作取出哈希對象,若失敗,或取出的對象不是哈希類型的對象,則發送0後直接返回
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
checkType(c,o,OBJ_HASH)) return;
// 發送field對象的值的長度給client
addReplyLongLong(c,hashTypeGetValueLength(o,c->argv[2]));
}
- HDEL命令實現
void hdelCommand(client *c) {
robj *o;
int j, deleted = 0, keyremoved = 0;
// 以寫操作取出哈希對象,若失敗,或取出的對象不是哈希類型的對象,則發送0後直接返回
if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.czero)) == NULL ||
checkType(c,o,OBJ_HASH)) return;
// 遍歷所有的字段field
for (j = 2; j < c->argc; j++) {
// 從哈希對象中刪除當前字段
if (hashTypeDelete(o,c->argv[j])) {
deleted++; //更新刪除的個數
// 如果哈希對象爲空,則刪除該對象
if (hashTypeLength(o) == 0) {
dbDelete(c->db,c->argv[1]);
keyremoved = 1; //設置刪除標誌
break;
}
}
}
// 只要刪除了字段
if (deleted) {
// 發送信號表示鍵被改變
signalModifiedKey(c->db,c->argv[1]);
// 發送"hdel"事件通知
notifyKeyspaceEvent(NOTIFY_HASH,"hdel",c->argv[1],c->db->id);
// 如果哈希對象被刪除
if (keyremoved)
// 發送"hdel"事件通知
notifyKeyspaceEvent(NOTIFY_GENERIC,"del",c->argv[1],
c->db->id);
server.dirty += deleted; // 更新髒鍵
}
addReplyLongLong(c,deleted); //發送刪除的個數給client
}
- HINCRBYFLOAT 命令的實現
void hincrbyfloatCommand(client *c) {
double long value, incr;
robj *o, *current, *new, *aux;
// 得到一個long double類型的增量increment
if (getLongDoubleFromObjectOrReply(c,c->argv[3],&incr,NULL) != C_OK) return;
// 以寫方式取出哈希對象,失敗則直接返回
if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
// 返回field在哈希對象o中的值對象
if ((current = hashTypeGetObject(o,c->argv[2])) != NULL) {
//從值對象中得到一個long double類型的value,如果不是浮點數的值,則發送"hash value is not a valid float"信息給client
if (getLongDoubleFromObjectOrReply(c,current,&value,
"hash value is not a valid float") != C_OK) {
decrRefCount(current); //取值成功,釋放臨時的value對象空間,直接返回
return;
}
decrRefCount(current); //取值失敗也要釋放空間
} else {
value = 0; //如果沒有值,則設置爲默認的0
}
value += incr; //備份原先的值
// 將value轉換爲字符串類型的對象
new = createStringObjectFromLongDouble(value,1);
//將鍵和值對象的編碼進行優化,以節省空間,是以embstr或raw或整型存儲
hashTypeTryObjectEncoding(o,&c->argv[2],NULL);
// 設置原來的key爲新的值對象
hashTypeSet(o,c->argv[2],new);
// 講新的值對象發送給client
addReplyBulk(c,new);
// 修改數據庫的鍵則發送信號,發送"hincrbyfloat"事件通知,更新髒鍵
signalModifiedKey(c->db,c->argv[1]);
notifyKeyspaceEvent(NOTIFY_HASH,"hincrbyfloat",c->argv[1],c->db->id);
server.dirty++;
/* Always replicate HINCRBYFLOAT as an HSET command with the final value
* in order to make sure that differences in float pricision or formatting
* will not create differences in replicas or after an AOF restart. */
// 用HSET命令代替HINCRBYFLOAT,以防不同的浮點精度造成的誤差
// 創建HSET字符串對象
aux = createStringObject("HSET",4);
// 修改HINCRBYFLOAT命令爲HSET對象
rewriteClientCommandArgument(c,0,aux);
// 釋放空間
decrRefCount(aux);
// 修改increment爲新的值對象new
rewriteClientCommandArgument(c,3,new);
// 釋放空間
decrRefCount(new);
}
- HSCAN 命令實現
// HSCAN key cursor [MATCH pattern] [COUNT count]
// HSCAN 命令實現
void hscanCommand(client *c) {
robj *o;
unsigned long cursor;
// 獲取scan命令的遊標cursor
if (parseScanCursorOrReply(c,c->argv[2],&cursor) == C_ERR) return;
// 以寫操作取出哈希對象,若失敗,或取出的對象不是哈希類型的對象,則發送0後直接返回
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.emptyscan)) == NULL ||
checkType(c,o,OBJ_HASH)) return;
// 調用底層實現
scanGenericCommand(c,o,cursor);
}