Redis(五):hash/hset/hget 命令原始碼解析
Redis作為nosql資料庫,kv string型資料的支援是最基礎的,但是如果僅有kv的操作,也不至於有redis的成功。(memcache就是個例子)
Redis除了string, 還有hash,list,set,zset。
所以,我們就來看看hash的相關操作實現吧。
首先,我們從作用上理解hash存在的意義:Redis hash 是一個 string 型別的 field 和 value 的對映表,hash 特別適合用於儲存物件。從另一個方面來說是,hash可以聚合很多類似的屬性,這是string中難以實現的。
所以,總體來說,hash的命令與string的命令差不太多。其操作手冊如下:
1> hdel 命令:刪除一個或多個雜湊表欄位
格式:HDEL key field2 [field2]
返回值:被成功刪除欄位的數量,不包括被忽略的欄位。2> hexists 命令:檢視雜湊表 key 中,指定的欄位是否存在
格式:HEXISTS key field
返回值:如果雜湊表含有給定欄位,返回 1 。 如果雜湊表不含有給定欄位,或 key 不存在,返回 0 。3> hget 命令:獲取儲存在雜湊表中指定欄位的值
格式:HGET key field
返回值:返回給定欄位的值。如果給定的欄位或 key 不存在時,返回 nil 。4> hgetall 命令:獲取在雜湊表中指定 key 的所有欄位和值
格式:HGETALL key
返回值:以列表形式返回雜湊表的欄位及欄位值。 若 key 不存在,返回空列表。5> hincrby 命令:為雜湊表 key 中的指定欄位的整數值加上增量 increment
格式:HINCRBY key field increment
返回值:執行 HINCRBY 命令之後,雜湊表中欄位的值。6> hincrbyfloat 命令:為雜湊表 key 中的指定欄位的浮點數值加上增量 increment
格式:HINCRBYFLOAT key field increment
返回值:執行 Hincrbyfloat 命令之後,雜湊表中欄位的值。7> hkeys 命令:獲取所有雜湊表中的欄位
格式:HKEYS key
返回值:包含雜湊表中所有欄位的列表。 當 key 不存在時,返回一個空列表。8> hlen 命令:獲取雜湊表中欄位的數量
格式:HLEN key
返回值:雜湊表中欄位的數量。 當 key 不存在時,返回 0 。9> hmget 命令:獲取所有給定欄位的值
格式:HMGET key field1 [field2]
返回值:一個包含多個給定欄位關聯值的表,表值的排列順序和指定欄位的請求順序一樣。10> hmset 命令:同時將多個 field-value (域-值)對設定到雜湊表 key 中
格式:HMSET key field1 value1 [field2 value2 ]
返回值:如果命令執行成功,返回 OK 。11> hset 命令:將雜湊表 key 中的欄位 field 的值設為 value
格式:HSET key field value
返回值:如果欄位是雜湊表中的一個新建欄位,並且值設定成功,返回 1 。 如果雜湊表中域欄位已經存在且舊值已被新值覆蓋,返回 0 。12> hsetnx 命令:只有在欄位 field 不存在時,設定雜湊表欄位的值
格式:HSETNX key field value
返回值:設定成功,返回 1 。 如果給定欄位已經存在且沒有操作被執行,返回 0 。13> hvals 命令:獲取雜湊表中所有值
格式:HVALS key
返回值:一個包含雜湊表中所有值的表。 當 key 不存在時,返回一個空表。14> hscan 命令:迭代雜湊表中的鍵值對
格式:HSCAN key cursor [MATCH pattern] [COUNT count]
其中,有的是單kv操作有的是指量操作,有的是寫操作有的是讀操作。從實現上看,大體上很多命令是類似的:
比如: hset/hmset/hincrbyXXX 可以是一類的
比如:hget/hgetall/hexists/hkeys/hmget 可以是一類
注意:以上分法僅是為了讓我們看清本質,對實際使用並無實際參考意義。
所以,我們就挑幾個方法來解析下 hash 的操作實現吧。
零、hash資料結構
hash相關的命令定義如下:
{"hset",hsetCommand,4,"wmF",0,NULL,1,1,1,0,0},
{"hsetnx",hsetnxCommand,4,"wmF",0,NULL,1,1,1,0,0},
{"hget",hgetCommand,3,"rF",0,NULL,1,1,1,0,0},
{"hmset",hmsetCommand,-4,"wm",0,NULL,1,1,1,0,0},
{"hmget",hmgetCommand,-3,"r",0,NULL,1,1,1,0,0},
{"hincrby",hincrbyCommand,4,"wmF",0,NULL,1,1,1,0,0},
{"hincrbyfloat",hincrbyfloatCommand,4,"wmF",0,NULL,1,1,1,0,0},
{"hdel",hdelCommand,-3,"wF",0,NULL,1,1,1,0,0},
{"hlen",hlenCommand,2,"rF",0,NULL,1,1,1,0,0},
{"hstrlen",hstrlenCommand,3,"rF",0,NULL,1,1,1,0,0},
{"hkeys",hkeysCommand,2,"rS",0,NULL,1,1,1,0,0},
{"hvals",hvalsCommand,2,"rS",0,NULL,1,1,1,0,0},
{"hgetall",hgetallCommand,2,"r",0,NULL,1,1,1,0,0},
{"hexists",hexistsCommand,3,"rF",0,NULL,1,1,1,0,0},
{"hscan",hscanCommand,-3,"rR",0,NULL,1,1,1,0,0},
ziplist 資料結構
typedef struct zlentry {
unsigned int prevrawlensize, prevrawlen;
unsigned int lensize, len;
unsigned int headersize;
unsigned char encoding;
unsigned char *p;
} zlentry;
#define ZIPLIST_BYTES(zl) (*((uint32_t*)(zl)))
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))
#define ZIPLIST_LENGTH(zl) (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))
#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t))
#define ZIPLIST_END_SIZE (sizeof(uint8_t))
#define ZIPLIST_ENTRY_HEAD(zl) ((zl)+ZIPLIST_HEADER_SIZE)
#define ZIPLIST_ENTRY_TAIL(zl) ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))
#define ZIPLIST_ENTRY_END(zl) ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)
hashtable 資料結構:
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
typedef struct dictEntry {
void *key;
void *val;
struct dictEntry *next;
} dictEntry;
一、hset 設定單個 field -> value
“增刪改查”中的“增改” 就是它了。
// t_hash.c, set key field value
void hsetCommand(client *c) {
int update;
robj *o;
// 1. 查詢hash的key是否存在,不存在則新建一個,然後在其上進行資料操作
if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
// 2. 檢查2-3個引數是否需要將簡單版(ziplist)hash錶轉換為複雜的hash表,轉換後的表通過 o->ptr 體現
hashTypeTryConversion(o,c->argv,2,3);
// 3. 新增kv到 o 的hash表中
update = hashTypeSet(o,c->argv[2]->ptr,c->argv[3]->ptr,HASH_SET_COPY);
addReply(c, update ? shared.czero : shared.cone);
// 變更命令傳播
signalModifiedKey(c->db,c->argv[1]);
notifyKeyspaceEvent(NOTIFY_HASH,"hset",c->argv[1],c->db->id);
server.dirty++;
}
// 1. 獲取db外部的key, 即整體hash資料例項
// t_hash.c
robj *hashTypeLookupWriteOrCreate(client *c, robj *key) {
robj *o = lookupKeyWrite(c->db,key);
if (o == NULL) {
// 此處建立的hashObject是以 ziplist 形式的
o = createHashObject();
dbAdd(c->db,key,o);
} else {
// 不是hash型別的鍵已存在,不可覆蓋,返回錯誤
if (o->type != OBJ_HASH) {
addReply(c,shared.wrongtypeerr);
return NULL;
}
}
return o;
}
// object.c, 建立hashObject, 以 ziplist 形式建立
robj *createHashObject(void) {
unsigned char *zl = ziplistNew();
robj *o = createObject(OBJ_HASH, zl);
o->encoding = OBJ_ENCODING_ZIPLIST;
return o;
}
// ziplist.c
static unsigned char *createList() {
unsigned char *zl = ziplistNew();
zl = ziplistPush(zl, (unsigned char*)"foo", 3, ZIPLIST_TAIL);
zl = ziplistPush(zl, (unsigned char*)"quux", 4, ZIPLIST_TAIL);
zl = ziplistPush(zl, (unsigned char*)"hello", 5, ZIPLIST_HEAD);
zl = ziplistPush(zl, (unsigned char*)"1024", 4, ZIPLIST_TAIL);
return zl;
}
// 2. 檢查引數,是否需要將 ziplist 形式的hash錶轉換為真正的hash表
/* Check the length of a number of objects to see if we need to convert a
* ziplist to a real hash. Note that we only check string encoded objects
* as their string length can be queried in constant time. */
void hashTypeTryConversion(robj *o, robj **argv, int start, int end) {
int i;
if (o->encoding != OBJ_ENCODING_ZIPLIST) return;
for (i = start; i <= end; i++) {
// 引數大於設定的 hash_max_ziplist_value (預設: 64)時,會直接將 ziplist 轉換為 ht
// OBJ_ENCODING_RAW, OBJ_ENCODING_EMBSTR
// 迴圈檢查引數,只要發生了一次轉換就結束檢查(沒必要繼續了)
if (sdsEncodedObject(argv[i]) &&
sdslen(argv[i]->ptr) > server.hash_max_ziplist_value)
{
// 這個轉換過程很有意思,我們深入看看
hashTypeConvert(o, OBJ_ENCODING_HT);
break;
}
}
}
// t_hash.c, 轉換編碼方式 (如上, ziplist -> ht)
void hashTypeConvert(robj *o, int enc) {
if (o->encoding == OBJ_ENCODING_ZIPLIST) {
// 此處我們只處理這種情況
hashTypeConvertZiplist(o, enc);
} else if (o->encoding == OBJ_ENCODING_HT) {
serverPanic("Not implemented");
} else {
serverPanic("Unknown hash encoding");
}
}
// t_hash.c, 轉換編碼 ziplist 為目標 enc (實際只能是 OBJ_ENCODING_HT)
void hashTypeConvertZiplist(robj *o, int enc) {
serverAssert(o->encoding == OBJ_ENCODING_ZIPLIST);
if (enc == OBJ_ENCODING_ZIPLIST) {
/* Nothing to do... */
} else if (enc == OBJ_ENCODING_HT) {
hashTypeIterator *hi;
dict *dict;
int ret;
// 迭代器建立
hi = hashTypeInitIterator(o);
// 一個hash的資料結構就是一個 dict, 從這個級別來說, hash 與 db 是一個級別的
dict = dictCreate(&hashDictType, NULL);
// 依次迭代 o, 賦值到 hi->fptr, hi->vptr
// 依次新增到 dict 中
while (hashTypeNext(hi) != C_ERR) {
sds key, value;
// 從 hi->fptr 中獲取key
// 從 hi->vptr 中獲取value
key = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_KEY);
value = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_VALUE);
// 新增到 dict 中
ret = dictAdd(dict, key, value);
if (ret != DICT_OK) {
serverLogHexDump(LL_WARNING,"ziplist with dup elements dump",
o->ptr,ziplistBlobLen(o->ptr));
serverPanic("Ziplist corruption detected");
}
}
// 釋放迭代器
hashTypeReleaseIterator(hi);
zfree(o->ptr);
// 將變更反映到o物件上返回
o->encoding = OBJ_ENCODING_HT;
o->ptr = dict;
} else {
serverPanic("Unknown hash encoding");
}
}
// 2.1. 迭代ziplist元素
// t_hash.c, 迭代器
/* 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) {
if (hi->encoding == OBJ_ENCODING_ZIPLIST) {
unsigned char *zl;
unsigned char *fptr, *vptr;
// 每次都是基於原始字元器進行計算偏移
// 迭代的是 fptr,vptr
zl = hi->subject->ptr;
fptr = hi->fptr;
vptr = hi->vptr;
// 第一次查詢時使用index查詢,後續則使用 fptr,vptr 進行迭代
if (fptr == NULL) {
/* Initialize cursor */
serverAssert(vptr == NULL);
fptr = ziplistIndex(zl, 0);
} else {
/* Advance cursor */
serverAssert(vptr != NULL);
fptr = ziplistNext(zl, vptr);
}
if (fptr == NULL) return C_ERR;
/* Grab pointer to the value (fptr points to the field) */
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;
}
// ziplist.c, 查詢 index 的元素
/* Returns an offset to use for iterating with ziplistNext. When the given
* index is negative, the list is traversed back to front. When the list
* doesn't contain an element at the provided index, NULL is returned. */
unsigned char *ziplistIndex(unsigned char *zl, int index) {
unsigned char *p;
unsigned int prevlensize, prevlen = 0;
if (index < 0) {
// 小於0時,反向查詢
index = (-index)-1;
p = ZIPLIST_ENTRY_TAIL(zl);
if (p[0] != ZIP_END) {
ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
while (prevlen > 0 && index--) {
p -= prevlen;
ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
}
}
} else {
p = ZIPLIST_ENTRY_HEAD(zl);
while (p[0] != ZIP_END && index--) {
p += zipRawEntryLength(p);
}
}
// 迭代完成還沒找到元素 p[0]=ZIP_END
// index 超出整體ziplist大小則遍歷完成後 index>0
return (p[0] == ZIP_END || index > 0) ? NULL : p;
}
// ziplist.c, 由 fptr,vptr 進行迭代元素
/* Return pointer to next entry in ziplist.
*
* zl is the pointer to the ziplist
* p is the pointer to the current element
*
* The element after 'p' is returned, otherwise NULL if we are at the end. */
unsigned char *ziplistNext(unsigned char *zl, unsigned char *p) {
((void) zl);
/* "p" could be equal to ZIP_END, caused by ziplistDelete,
* and we should return NULL. Otherwise, we should return NULL
* when the *next* element is ZIP_END (there is no next entry). */
if (p[0] == ZIP_END) {
return NULL;
}
// 當前指標偏移當前元素長度(根據ziplist協議),即到下一元素指標位置
p += zipRawEntryLength(p);
if (p[0] == ZIP_END) {
return NULL;
}
return p;
}
/* Return the total number of bytes used by the entry pointed to by 'p'. */
static unsigned int zipRawEntryLength(unsigned char *p) {
unsigned int prevlensize, encoding, lensize, len;
ZIP_DECODE_PREVLENSIZE(p, prevlensize);
ZIP_DECODE_LENGTH(p + prevlensize, encoding, lensize, len);
return prevlensize + lensize + len;
}
// 2.2. t_hash.c, 獲取 hashTypeIterator 的具體值,寫入 vstr, vlen 中
/* Return the key or value at the current iterator position as a new
* SDS string. */
sds hashTypeCurrentObjectNewSds(hashTypeIterator *hi, int what) {
unsigned char *vstr;
unsigned int vlen;
long long vll;
hashTypeCurrentObject(hi,what,&vstr,&vlen,&vll);
if (vstr) return sdsnewlen(vstr,vlen);
return sdsfromlonglong(vll);
}
/* Higher level function of hashTypeCurrent*() that returns the hash value
* at current iterator position.
*
* The returned element is returned by reference in either *vstr and *vlen if
* it's returned in string form, or stored in *vll if it's returned as
* a number.
*
* If *vll is populated *vstr is set to NULL, so the caller
* can always check the function return by checking the return value
* type checking if vstr == NULL. */
void hashTypeCurrentObject(hashTypeIterator *hi, int what, unsigned char **vstr, unsigned int *vlen, long long *vll) {
if (hi->encoding == OBJ_ENCODING_ZIPLIST) {
*vstr = NULL;
hashTypeCurrentFromZiplist(hi, what, vstr, vlen, vll);
} else if (hi->encoding == OBJ_ENCODING_HT) {
sds ele = hashTypeCurrentFromHashTable(hi, what);
*vstr = (unsigned char*) ele;
*vlen = sdslen(ele);
} else {
serverPanic("Unknown hash encoding");
}
}
// t_hash.c, 從ziplist中獲取某個 hashTypeIterator 的具體值,結果定稿 vstr, vlen
/* Get the field or value at iterator cursor, for an iterator on a hash value
* encoded as a ziplist. Prototype is similar to `hashTypeGetFromZiplist`. */
void hashTypeCurrentFromZiplist(hashTypeIterator *hi, int what,
unsigned char **vstr,
unsigned int *vlen,
long long *vll)
{
int ret;
serverAssert(hi->encoding == OBJ_ENCODING_ZIPLIST);
// OBJ_HASH_KEY 從 fptr 中獲取, 否則從 vptr 中獲取
if (what & OBJ_HASH_KEY) {
ret = ziplistGet(hi->fptr, vstr, vlen, vll);
serverAssert(ret);
} else {
ret = ziplistGet(hi->vptr, vstr, vlen, vll);
serverAssert(ret);
}
}
// ziplist.c,
/* Get entry pointed to by 'p' and store in either '*sstr' or 'sval' depending
* on the encoding of the entry. '*sstr' is always set to NULL to be able
* to find out whether the string pointer or the integer value was set.
* Return 0 if 'p' points to the end of the ziplist, 1 otherwise. */
unsigned int ziplistGet(unsigned char *p, unsigned char **sstr, unsigned int *slen, long long *sval) {
zlentry entry;
if (p == NULL || p[0] == ZIP_END) return 0;
if (sstr) *sstr = NULL;
// 按照ziplist的編碼協議, 獲取頭部資訊
zipEntry(p, &entry);
if (ZIP_IS_STR(entry.encoding)) {
if (sstr) {
*slen = entry.len;
*sstr = p+entry.headersize;
}
} else {
if (sval) {
*sval = zipLoadInteger(p+entry.headersize,entry.encoding);
}
}
return 1;
}
// ziplist.c, 解析原始字串為 zlentry
/* Return a struct with all information about an entry. */
static void zipEntry(unsigned char *p, zlentry *e) {
// 按照ziplist的編碼協議,依次讀取 prevrawlensize, prevrawlen
ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen);
// 指向下一位置偏移,按照ziplist的編碼協議,依次讀取 encoding, lensize, len
ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len);
// 除去header得到 body偏移
e->headersize = e->prevrawlensize + e->lensize;
e->p = p;
}
具體header解析如下, 有興趣的點開瞅瞅:
// ziplist.c
/* Decode the length of the previous element, from the perspective of the entry
* pointed to by 'ptr'. */
#define ZIP_DECODE_PREVLEN(ptr, prevlensize, prevlen) do { \
// 解析第1個字元為 prevlensize
ZIP_DECODE_PREVLENSIZE(ptr, prevlensize); \
if ((prevlensize) == 1) { \
(prevlen) = (ptr)[0]; \
} else if ((prevlensize) == 5) { \
assert(sizeof((prevlensize)) == 4); \
// 當ptr[0]>254時,代表內容有點大,需要使用 5個字元儲存上一字元長度
memcpy(&(prevlen), ((char*)(ptr)) + 1, 4); \
memrev32ifbe(&prevlen); \
} \
} while(0);
/* Decode the number of bytes required to store the length of the previous
* element, from the perspective of the entry pointed to by 'ptr'. */
#define ZIP_DECODE_PREVLENSIZE(ptr, prevlensize) do { \
if ((ptr)[0] < ZIP_BIGLEN) { \
(prevlensize) = 1; \
} else { \
(prevlensize) = 5; \
} \
} while(0);
/* Decode the length encoded in 'ptr'. The 'encoding' variable will hold the
* entries encoding, the 'lensize' variable will hold the number of bytes
* required to encode the entries length, and the 'len' variable will hold the
* entries length. */
#define ZIP_DECODE_LENGTH(ptr, encoding, lensize, len) do { \
// 解析第1個字元為 編碼格式 &ZIP_STR_MASK=0xc0
ZIP_ENTRY_ENCODING((ptr), (encoding)); \
if ((encoding) < ZIP_STR_MASK) { \
// 0 << 6 =0
// 具體解析如下程式碼,
if ((encoding) == ZIP_STR_06B) { \
(lensize) = 1; \
(len) = (ptr)[0] & 0x3f; \
}
// 1 << 6 =64
else if ((encoding) == ZIP_STR_14B) { \
(lensize) = 2; \
(len) = (((ptr)[0] & 0x3f) << 8) | (ptr)[1]; \
}
// 2 << 6 =128
else if (encoding == ZIP_STR_32B) { \
(lensize) = 5; \
(len) = ((ptr)[1] << 24) | \
((ptr)[2] << 16) | \
((ptr)[3] << 8) | \
((ptr)[4]); \
} else { \
assert(NULL); \
} \
} else { \
// 超過 0xc0 的長度了,直接使用 1,2,3,4 表示len
(lensize) = 1; \
(len) = zipIntSize(encoding); \
} \
} while(0);
/* Extract the encoding from the byte pointed by 'ptr' and set it into
* 'encoding'. */
#define ZIP_ENTRY_ENCODING(ptr, encoding) do { \
(encoding) = (ptr[0]); \
if ((encoding) < ZIP_STR_MASK) (encoding) &= ZIP_STR_MASK; \
} while(0)
/* Different encoding/length possibilities */
#define ZIP_STR_MASK 0xc0
#define ZIP_INT_MASK 0x30
#define ZIP_STR_06B (0 << 6) // 0x00
#define ZIP_STR_14B (1 << 6) // 0x40
#define ZIP_STR_32B (2 << 6) // 0x80
#define ZIP_INT_16B (0xc0 | 0<<4) // 0xc0
#define ZIP_INT_32B (0xc0 | 1<<4) // 0xd0
#define ZIP_INT_64B (0xc0 | 2<<4) // 0xe0
#define ZIP_INT_24B (0xc0 | 3<<4) // 0xf0
#define ZIP_INT_8B 0xfe // 0xfe
新增kv到對應的key例項中:
// 3. 新增kv到 hash表中, 稍微複雜
// t_hash.c, 做變更到hash表中
int hashTypeSet(robj *o, sds field, sds value, int flags) {
int update = 0;
// 針對ziplist 的新增, 與 ht 編碼的新增, 自然是分別處理
if (o->encoding == OBJ_ENCODING_ZIPLIST) {
unsigned char *zl, *fptr, *vptr;
zl = o->ptr;
// 找到ziplist 的頭節點指標
fptr = ziplistIndex(zl, ZIPLIST_HEAD);
if (fptr != NULL) {
// 嘗試查詢該 field 對應的元素(從1開始),如果找到則先刪除原值,然後統一新增
fptr = ziplistFind(fptr, (unsigned char*)field, sdslen(field), 1);
if (fptr != NULL) {
/* Grab pointer to the value (fptr points to the field) */
// value 不可以為null, 否則 ziplist 將無法工作
vptr = ziplistNext(zl, fptr);
serverAssert(vptr != NULL);
update = 1;
/* Delete value */
// 先刪除舊的 value, 再以插入的形式更新, 後續講刪除時再詳解
zl = ziplistDelete(zl, &vptr);
/* Insert new value */
// 重點,將value新增到 ziplist 中
zl = ziplistInsert(zl, vptr, (unsigned char*)value,
sdslen(value));
}
}
// 沒有找到對應元素,則直接將元素新增到尾部即可
if (!update) {
/* Push new field/value pair onto the tail of the ziplist */
zl = ziplistPush(zl, (unsigned char*)field, sdslen(field),
ZIPLIST_TAIL);
zl = ziplistPush(zl, (unsigned char*)value, sdslen(value),
ZIPLIST_TAIL);
}
o->ptr = zl;
/* Check if the ziplist needs to be converted to a hash table */
// 大於設定的閥值後,轉換ziplist為ht(預設: 512)
if (hashTypeLength(o) > server.hash_max_ziplist_entries)
hashTypeConvert(o, OBJ_ENCODING_HT);
} else if (o->encoding == OBJ_ENCODING_HT) {
dictEntry *de = dictFind(o->ptr,field);
if (de) {
sdsfree(dictGetVal(de));
if (flags & HASH_SET_TAKE_VALUE) {
dictGetVal(de) = value;
value = NULL;
} else {
dictGetVal(de) = sdsdup(value);
}
update = 1;
} else {
sds f,v;
if (flags & HASH_SET_TAKE_FIELD) {
f = field;
field = NULL;
} else {
f = sdsdup(field);
}
if (flags & HASH_SET_TAKE_VALUE) {
v = value;
value = NULL;
} else {
v = sdsdup(value);
}
dictAdd(o->ptr,f,v);
}
} else {
serverPanic("Unknown hash encoding");
}
/* Free SDS strings we did not referenced elsewhere if the flags
* want this function to be responsible. */
if (flags & HASH_SET_TAKE_FIELD && field) sdsfree(field);
if (flags & HASH_SET_TAKE_VALUE && value) sdsfree(value);
return update;
}
// 3.1. 使用ziplist進行儲存 field -> value
// ziplist.c, 查詢某個 field 是否存在於ziplist中
/* Find pointer to the entry equal to the specified entry. Skip 'skip' entries
* between every comparison. Returns NULL when the field could not be found. */
unsigned char *ziplistFind(unsigned char *p, unsigned char *vstr, unsigned int vlen, unsigned int skip) {
int skipcnt = 0;
unsigned char vencoding = 0;
long long vll = 0;
while (p[0] != ZIP_END) {
unsigned int prevlensize, encoding, lensize, len;
unsigned char *q;
// 解析整個字串p的 prevlensize,encoding,lensize,len
ZIP_DECODE_PREVLENSIZE(p, prevlensize);
ZIP_DECODE_LENGTH(p + prevlensize, encoding, lensize, len);
q = p + prevlensize + lensize;
// 傳入1, 代表要跳過一個元素, 比如: 查詢key時,跳過1個v,然後繼續迭代
// 跳過了n個元素後,再從此開始key的比對過程
if (skipcnt == 0) {
/* Compare current entry with specified entry */
// 針對不同的編碼使用不同的比較方式
if (ZIP_IS_STR(encoding)) {
// 找到相應的元素,直接返回 p 指標
if (len == vlen && memcmp(q, vstr, vlen) == 0) {
return p;
}
} else {
/* Find out if the searched field can be encoded. Note that
* we do it only the first time, once done vencoding is set
* to non-zero and vll is set to the integer value. */
if (vencoding == 0) {
if (!zipTryEncoding(vstr, vlen, &vll, &vencoding)) {
/* If the entry can't be encoded we set it to
* UCHAR_MAX so that we don't retry again the next
* time. */
vencoding = UCHAR_MAX;
}
/* Must be non-zero by now */
assert(vencoding);
}
/* Compare current entry with specified entry, do it only
* if vencoding != UCHAR_MAX because if there is no encoding
* possible for the field it can't be a valid integer. */
if (vencoding != UCHAR_MAX) {
long long ll = zipLoadInteger(q, encoding);
if (ll == vll) {
return p;
}
}
}
/* Reset skip count */
// 查詢一次,跳過skip次
skipcnt = skip;
} else {
/* Skip entry */
skipcnt--;
}
/* Move to next entry */
p = q + len;
}
return NULL;
}
// ziplist.c, 新增value到ziplist中
// zl:ziplist例項, p:要插入的key字串, s:要插入的value字串, len:要插入的value的長度
/* Insert an entry at "p". */
unsigned char *ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
return __ziplistInsert(zl,p,s,slen);
}
/* Insert item at "p". */
static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen;
unsigned int prevlensize, prevlen = 0;
size_t offset;
int nextdiff = 0;
unsigned char encoding = 0;
long long value = 123456789; /* initialized to avoid warning. Using a value
that is easy to see if for some reason
we use it uninitialized. */
zlentry tail;
/* Find out prevlen for the entry that is inserted. */
if (p[0] != ZIP_END) {
ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
} else {
unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
if (ptail[0] != ZIP_END) {
prevlen = zipRawEntryLength(ptail);
}
}
/* See if the entry can be encoded */
if (zipTryEncoding(s,slen,&value,&encoding)) {
/* 'encoding' is set to the appropriate integer encoding */
reqlen = zipIntSize(encoding);
} else {
/* 'encoding' is untouched, however zipEncodeLength will use the
* string length to figure out how to encode it. */
reqlen = slen;
}
/* We need space for both the length of the previous entry and
* the length of the payload. */
// 加上prevlen,encoding,slen 的長度,以計算value的存放位置
reqlen += zipPrevEncodeLength(NULL,prevlen);
reqlen += zipEncodeLength(NULL,encoding,slen);
/* When the insert position is not equal to the tail, we need to
* make sure that the next entry can hold this entry's length in
* its prevlen field. */
nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
/* Store offset because a realloc may change the address of zl. */
// 儲存當前偏移位置,以便在擴容之後,還能找到相應位置
// p = p -zl + zl
offset = p-zl;
zl = ziplistResize(zl,curlen+reqlen+nextdiff);
p = zl+offset;
/* Apply memory move when necessary and update tail offset. */
if (p[0] != ZIP_END) {
/* Subtract one because of the ZIP_END bytes */
// 字元拷貝
memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);
/* Encode this entry's raw length in the next entry. */
zipPrevEncodeLength(p+reqlen,reqlen);
/* Update offset for tail */
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);
/* When the tail contains more than one entry, we need to take
* "nextdiff" in account as well. Otherwise, a change in the
* size of prevlen doesn't have an effect on the *tail* offset. */
zipEntry(p+reqlen, &tail);
if (p[reqlen+tail.headersize+tail.len] != ZIP_END) {
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
}
} else {
/* This element will be the new tail. */
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
}
/* When nextdiff != 0, the raw length of the next entry has changed, so
* we need to cascade the update throughout the ziplist */
if (nextdiff != 0) {
// 如果本次更新後資料位置變化,則需要更新後續的元素位置
offset = p-zl;
zl = __ziplistCascadeUpdate(zl,p+reqlen);
p = zl+offset;
}
/* Write the entry */
// 將 value 寫入 p 中, 即寫入了 ziplist 中
p += zipPrevEncodeLength(p,prevlen);
p += zipEncodeLength(p,encoding,slen);
if (ZIP_IS_STR(encoding)) {
memcpy(p,s,slen);
} else {
zipSaveInteger(p,value,encoding);
}
ZIPLIST_INCR_LENGTH(zl,1);
return zl;
}
// 另外,如果沒有舊的元素值時,直接在hash表的末尾新增對應的field->value 即可
// ziplist.c, 在尾部進行新增元素,沒有許多的情況要考慮,但是程式碼完全複用 __ziplistInsert()
unsigned char *ziplistPush(unsigned char *zl, unsigned char *s, unsigned int slen, int where) {
unsigned char *p;
p = (where == ZIPLIST_HEAD) ? ZIPLIST_ENTRY_HEAD(zl) : ZIPLIST_ENTRY_END(zl);
return __ziplistInsert(zl,p,s,slen);
}
鑑於插入過程稍微複雜,我們們畫個圖重新理一下思路:
看起來沒ziplist好像沒那麼簡單呢,為啥還要搞這麼複雜呢?其實以上程式碼,僅是在人看來複雜,對機器來說就是更多的移位計算操作,多消耗點cpu就換來了空間上的節省,是可以的。軟體本身的複雜性帶來了效益,是軟體的價值體現,所以,並非所有的東西都是簡單即美。
接下來,我們來看一下使用 HT 的編碼又如何儲存field->value呢?
// 3.2. OBJ_ENCODING_HT 的 field -> value 的新增
if (o->encoding == OBJ_ENCODING_HT) {
// hash 表中查詢對應的 field
dictEntry *de = dictFind(o->ptr,field);
if (de) {
sdsfree(dictGetVal(de));
// hset 時使用 HASH_SET_COPY, 所以直接使用 sdsdup() 即可
if (flags & HASH_SET_TAKE_VALUE) {
dictGetVal(de) = value;
value = NULL;
} else {
dictGetVal(de) = sdsdup(value);
}
update = 1;
} else {
// 新增 field -> value
sds f,v;
if (flags & HASH_SET_TAKE_FIELD) {
f = field;
field = NULL;
} else {
f = sdsdup(field);
}
if (flags & HASH_SET_TAKE_VALUE) {
v = value;
value = NULL;
} else {
v = sdsdup(value);
}
// 新增到 hash 表中,前些篇章講解過,大概就是計算hash,放入v的過程
dictAdd(o->ptr,f,v);
}
}
如此看來,OBJ_ENCODING_HT 的實現反而簡單了哦。
總結下 hash的插入過程,hash 初始建立時都是使用ziplist 進行容納元素的,在特定情況下會觸發 ziplist 為 ht 的編碼方式, 比如:
1. hset時自身的引數大於設定值(預設: 64)時直接轉換 ziplist -> ht;
2. hash表的元素數量大於設定值(預設: 512)時轉換 ziplist -> ht;
這麼設計的原因是,元素較少且佔用空間較小時,使用ziplist會節省空間,且時間消耗與hash表相關並不大,所以 ziplist 是優先的選擇了。但是大量資料還是必須要使用hash表儲存的。
二、hmset 批量新增元素
hset 和 hmset 在實現上基本如出一轍,所以簡單瞅瞅就得了。
// t_hash.c, hmset key f1 v1 f2 v2
void hmsetCommand(client *c) {
int i;
robj *o;
// 引數個數檢查,必定是2n
if ((c->argc % 2) == 1) {
addReplyError(c,"wrong number of arguments for HMSET");
return;
}
// 插入方式與 hset 一毛一樣,差別在於批量插入時,會迴圈向 key-hash表中新增field->value
if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
hashTypeTryConversion(o,c->argv,2,c->argc-1);
// 迴圈insert
for (i = 2; i < c->argc; i += 2) {
hashTypeSet(o,c->argv[i]->ptr,c->argv[i+1]->ptr,HASH_SET_COPY);
}
addReply(c, shared.ok);
signalModifiedKey(c->db,c->argv[1]);
notifyKeyspaceEvent(NOTIFY_HASH,"hset",c->argv[1],c->db->id);
server.dirty++;
}
三、hget 獲取某欄位值
這種命令的時間複雜度都是 O(1), 所以一般是簡單至上。
// t_hash.c
void hgetCommand(client *c) {
robj *o;
// 查詢key, 不存在或者型別不一致則直接返回
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL ||
checkType(c,o,OBJ_HASH)) return;
// 基於o, 返回 field 對應的元素值即可
addHashFieldToReply(c, o, c->argv[2]->ptr);
}
// t_hash.c
static void addHashFieldToReply(client *c, robj *o, sds field) {
int ret;
if (o == NULL) {
addReply(c, shared.nullbulk);
return;
}
if (o->encoding == OBJ_ENCODING_ZIPLIST) {
unsigned char *vstr = NULL;
unsigned int vlen = UINT_MAX;
long long vll = LLONG_MAX;
// 基於 ziplist,
ret = hashTypeGetFromZiplist(o, field, &vstr, &vlen, &vll);
if (ret < 0) {
// 響應為空
addReply(c, shared.nullbulk);
} else {
// 新增到輸出緩衝
if (vstr) {
addReplyBulkCBuffer(c, vstr, vlen);
} else {
addReplyBulkLongLong(c, vll);
}
}
} else if (o->encoding == OBJ_ENCODING_HT) {
// hash 表型別則查詢 hash 表即可
sds value = hashTypeGetFromHashTable(o, field);
// 新增到輸出緩衝
if (value == NULL)
// 響應為空
addReply(c, shared.nullbulk);
else
addReplyBulkCBuffer(c, value, sdslen(value));
} else {
serverPanic("Unknown hash encoding");
}
}
// t_hash.c, 從 ziplist 中查詢 field 值
/* Get the value from a ziplist encoded hash, identified by field.
* Returns -1 when the field cannot be found. */
int hashTypeGetFromZiplist(robj *o, sds field,
unsigned char **vstr,
unsigned int *vlen,
long long *vll)
{
unsigned char *zl, *fptr = NULL, *vptr = NULL;
int ret;
serverAssert(o->encoding == OBJ_ENCODING_ZIPLIST);
zl = o->ptr;
fptr = ziplistIndex(zl, ZIPLIST_HEAD);
if (fptr != NULL) {
fptr = ziplistFind(fptr, (unsigned char*)field, sdslen(field), 1);
if (fptr != NULL) {
/* Grab pointer to the value (fptr points to the field) */
vptr = ziplistNext(zl, fptr);
serverAssert(vptr != NULL);
}
}
if (vptr != NULL) {
ret = ziplistGet(vptr, vstr, vlen, vll);
serverAssert(ret);
return 0;
}
return -1;
}
// t_hash.c, 從hash表中查詢 field 欄位的值
/* Get the value from a hash table encoded hash, identified by field.
* Returns NULL when the field cannot be found, otherwise the SDS value
* is returned. */
sds hashTypeGetFromHashTable(robj *o, sds field) {
dictEntry *de;
serverAssert(o->encoding == OBJ_ENCODING_HT);
de = dictFind(o->ptr, field);
if (de == NULL) return NULL;
return dictGetVal(de);
}
四、hmget 批量獲取值
與hget如出一轍。
// t_hash.c
void hmgetCommand(client *c) {
robj *o;
int i;
/* Don't abort when the key cannot be found. Non-existing keys are empty
* hashes, where HMGET should respond with a series of null bulks. */
o = lookupKeyRead(c->db, c->argv[1]);
if (o != NULL && o->type != OBJ_HASH) {
addReply(c, shared.wrongtypeerr);
return;
}
// 迴圈輸出值
addReplyMultiBulkLen(c, c->argc-2);
for (i = 2; i < c->argc; i++) {
addHashFieldToReply(c, o, c->argv[i]->ptr);
}
}
五、hgetall 獲取所有hash的kv
hgetall 和 hmget 方式稍微有點不一樣,原因是為了讓 hkeysCommand/hvalsCommand 進行復用。
// t_hash.c
void hgetallCommand(client *c) {
genericHgetallCommand(c,OBJ_HASH_KEY|OBJ_HASH_VALUE);
}
void genericHgetallCommand(client *c, int flags) {
robj *o;
hashTypeIterator *hi;
int multiplier = 0;
int length, count = 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);
hi = hashTypeInitIterator(o);
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);
}
static void addHashIteratorCursorToReply(client *c, hashTypeIterator *hi, int what) {
if (hi->encoding == OBJ_ENCODING_ZIPLIST) {
unsigned char *vstr = NULL;
unsigned int vlen = UINT_MAX;
long long vll = LLONG_MAX;
hashTypeCurrentFromZiplist(hi, what, &vstr, &vlen, &vll);
if (vstr)
addReplyBulkCBuffer(c, vstr, vlen);
else
addReplyBulkLongLong(c, vll);
} else if (hi->encoding == OBJ_ENCODING_HT) {
sds value = hashTypeCurrentFromHashTable(hi, what);
addReplyBulkCBuffer(c, value, sdslen(value));
} else {
serverPanic("Unknown hash encoding");
}
}
六、hincrby 增加x某欄位
hincrby key field 1
// t_hash.c,
void hincrbyCommand(client *c) {
long long value, incr, oldvalue;
robj *o;
sds new;
unsigned char *vstr;
unsigned int vlen;
// 解析增加欄位值到 incr 中
if (getLongLongFromObjectOrReply(c,c->argv[3],&incr,NULL) != C_OK) return;
// 獲取原值或者設定為0
if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
if (hashTypeGetValue(o,c->argv[2]->ptr,&vstr,&vlen,&value) == C_OK) {
if (vstr) {
if (string2ll((char*)vstr,vlen,&value) == 0) {
addReplyError(c,"hash value is not an integer");
return;
}
} /* Else hashTypeGetValue() already stored it into &value */
} else {
value = 0;
}
oldvalue = value;
if ((incr < 0 && oldvalue < 0 && incr < (LLONG_MIN-oldvalue)) ||
(incr > 0 && oldvalue > 0 && incr > (LLONG_MAX-oldvalue))) {
addReplyError(c,"increment or decrement would overflow");
return;
}
// 將相加後的值重置設定回hash表中
value += incr;
new = sdsfromlonglong(value);
hashTypeSet(o,c->argv[2]->ptr,new,HASH_SET_TAKE_VALUE);
addReplyLongLong(c,value);
signalModifiedKey(c->db,c->argv[1]);
notifyKeyspaceEvent(NOTIFY_HASH,"hincrby",c->argv[1],c->db->id);
server.dirty++;
}
七、hdel 刪除某欄位
hdel key field
// t_hash.c,
void hdelCommand(client *c) {
robj *o;
int j, deleted = 0, keyremoved = 0;
if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.czero)) == NULL ||
checkType(c,o,OBJ_HASH)) return;
// 迴圈刪除給定欄位列表
for (j = 2; j < c->argc; j++) {
if (hashTypeDelete(o,c->argv[j]->ptr)) {
deleted++;
// 當沒有任何元素後,直接將key刪除
if (hashTypeLength(o) == 0) {
dbDelete(c->db,c->argv[1]);
keyremoved = 1;
break;
}
}
}
if (deleted) {
signalModifiedKey(c->db,c->argv[1]);
notifyKeyspaceEvent(NOTIFY_HASH,"hdel",c->argv[1],c->db->id);
if (keyremoved)
notifyKeyspaceEvent(NOTIFY_GENERIC,"del",c->argv[1],
c->db->id);
server.dirty += deleted;
}
addReplyLongLong(c,deleted);
}
// 具體刪除 field, 同樣區分編碼型別,不同處理邏輯
/* Delete an element from a hash.
* Return 1 on deleted and 0 on not found. */
int hashTypeDelete(robj *o, sds field) {
int deleted = 0;
if (o->encoding == OBJ_ENCODING_ZIPLIST) {
unsigned char *zl, *fptr;
zl = o->ptr;
fptr = ziplistIndex(zl, ZIPLIST_HEAD);
if (fptr != NULL) {
// ziplist 刪除,依次刪除 field, value
fptr = ziplistFind(fptr, (unsigned char*)field, sdslen(field), 1);
if (fptr != NULL) {
// ziplistDelete 為原地刪除,所以只要呼叫2次,即把kv刪除
zl = ziplistDelete(zl,&fptr);
zl = ziplistDelete(zl,&fptr);
o->ptr = zl;
deleted = 1;
}
}
} else if (o->encoding == OBJ_ENCODING_HT) {
if (dictDelete((dict*)o->ptr, field) == C_OK) {
deleted = 1;
/* Always check if the dictionary needs a resize after a delete. */
// hash 刪除的,可能需要進行縮容操作,這種處理方法相對特殊些
if (htNeedsResize(o->ptr)) dictResize(o->ptr);
}
} else {
serverPanic("Unknown hash encoding");
}
return deleted;
}
// server.c, 是否需要進行 resize
int htNeedsResize(dict *dict) {
long long size, used;
size = dictSlots(dict);
used = dictSize(dict);
// HASHTABLE_MIN_FILL=10, 即使用率小於 1/10 時,可以進行縮容操作了
return (size && used && size > DICT_HT_INITIAL_SIZE &&
(used*100/size < HASHTABLE_MIN_FILL));
}
至此,整個hash資料結構的解析算是完整了。總體來說,hash由兩種資料結構承載,ziplist在小資料量時使用,稍微複雜,但對於昂貴的記憶體來說是值得的。hash表在資料量大時使用,容易理解。通過本文的講解,相信可以驗證了你對redis hash 的實現的猜想了。
如果你想學好JAVA這門技術,也想在IT行業拿高薪,可以進來看看 ,群裡有:Java工程化、高效能及分散式、高效能、深入淺出。高架構。效能調優、Spring,MyBatis,Netty原始碼分析和大資料等多個知識點。
如果你想拿高薪的,想學習的,想就業前景好的,想跟別人競爭能取得優勢的,想進阿里面試但擔心面試不過的,你都可以來,加V:msb-shishi (小白和廣告勿擾)
看完三件事❤️
如果你覺得這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:
-
點贊,轉發,有你們的 『點贊和評論』,才是我創造的動力。
-
關注公眾號 『 java爛豬皮 』,不定期分享原創知識。
-
同時可以期待後續文章ing?
相關文章
- Redis系列(九):資料結構Hash(ZipList、HashTable)原始碼解析和HSET、HGET命令Redis資料結構原始碼
- Redis命令——雜湊(Hash)Redis
- Redis radix tree原始碼解析Redis原始碼
- dubbo原始碼解析-spi(五)原始碼
- diffusers-原始碼解析-五-原始碼
- 【原始碼】Redis exists命令bug分析原始碼Redis
- redis原始碼解析----epoll的使用Redis原始碼
- ChatGLM3 原始碼解析(五)原始碼
- Redis系列(五):資料結構List雙向連結串列中基本操作操作命令和原始碼解析Redis資料結構原始碼
- redis hset hmset過期時間Redis
- 【原始碼】Redis命令處理過程原始碼Redis
- redis原始碼分析(五):資料持久化Redis原始碼持久化
- [Redis原始碼閱讀]實現一個redis命令--nonzerodecrRedis原始碼
- Redis原始碼解析之跳躍表(一)Redis原始碼
- Redis原始碼解析之跳躍表(三)Redis原始碼
- Hystrix 原始碼解析 —— 命令合併執行原始碼
- 《閒扯Redis六》Redis五種資料型別之Hash型Redis資料型別
- Non-static method Redis::hSet () cannot be called staticallyRedis
- Redis系列(十一):資料結構Set原始碼解析和SADD、SINTER、SDIFF、SUNION、SPOP命令Redis資料結構原始碼
- redis個人原始碼分析筆記4--hash物件的儲存Redis原始碼筆記物件
- TiKV 原始碼解析(五)fail-rs 介紹原始碼AI
- redis api hashRedisAPI
- Go執行指令碼命令用例及原始碼解析Go指令碼原始碼
- Redis系列(十二):資料結構SortedSet跳躍表中基本操作命令和原始碼解析Redis資料結構原始碼
- Redis五大資料型別之 Hash(雜湊)Redis大資料資料型別
- Redis中的Hash型別12個常用命令Redis型別
- [原始碼解析] NVIDIA HugeCTR,GPU版本引數伺服器--- (6) --- Distributed hash表原始碼GPU伺服器
- [原始碼解析] NVIDIA HugeCTR,GPU 版本引數伺服器 --(9)--- Local hash表原始碼GPU伺服器
- Android技術棧(五)HashMap和ArrayMap原始碼解析AndroidHashMap原始碼
- Android進階:五、RxJava2原始碼解析 2AndroidRxJava原始碼
- 【Redis原始碼】Redis 6 ACL原始碼詳解Redis原始碼
- 熔斷器 Hystrix 原始碼解析 —— 執行命令方式原始碼
- mybatis原始碼配置檔案解析之五:解析mappers標籤流程圖MyBatis原始碼APP流程圖
- redis - hash 實戰Redis
- 走近原始碼:Redis命令執行過程(客戶端)原始碼Redis客戶端
- Redis 原始碼解析之通用雙向連結串列(adlist)Redis原始碼
- ThinkPHP6 原始碼閱讀(五):多應用解析PHP原始碼
- springboot原始碼解析-管中窺豹系列之排序(五)Spring Boot原始碼排序