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,,"wmF",,NULL,,,,,},
{"hsetnx",hsetnxCommand,,"wmF",,NULL,,,,,},
{"hget",hgetCommand,,"rF",,NULL,,,,,},
{"hmset",hmsetCommand,-,"wm",,NULL,,,,,},
{"hmget",hmgetCommand,-,"r",,NULL,,,,,},
{"hincrby",hincrbyCommand,,"wmF",,NULL,,,,,},
{"hincrbyfloat",hincrbyfloatCommand,,"wmF",,NULL,,,,,},
{"hdel",hdelCommand,-,"wF",,NULL,,,,,},
{"hlen",hlenCommand,,"rF",,NULL,,,,,},
{"hstrlen",hstrlenCommand,,"rF",,NULL,,,,,},
{"hkeys",hkeysCommand,,"rS",,NULL,,,,,},
{"hvals",hvalsCommand,,"rS",,NULL,,,,,},
{"hgetall",hgetallCommand,,"r",,NULL,,,,,},
{"hexists",hexistsCommand,,"rF",,NULL,,,,,},
{"hscan",hscanCommand,-,"rR",,NULL,,,,,},

  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[];
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[])) == NULL) return;
// 2. 检查2-3个参数是否需要将简单版(ziplist)hash表转换为复杂的hash表,转换后的表通过 o->ptr 体现
hashTypeTryConversion(o,c->argv,,);
// 3. 添加kv到 o 的hash表中
update = hashTypeSet(o,c->argv[]->ptr,c->argv[]->ptr,HASH_SET_COPY);
addReply(c, update ? shared.czero : shared.cone);
// 变更命令传播
signalModifiedKey(c->db,c->argv[]);
notifyKeyspaceEvent(NOTIFY_HASH,"hset",c->argv[],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", , ZIPLIST_TAIL);
zl = ziplistPush(zl, (unsigned char*)"quux", , ZIPLIST_TAIL);
zl = ziplistPush(zl, (unsigned char*)"hello", , ZIPLIST_HEAD);
zl = ziplistPush(zl, (unsigned char*)"", , 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, );
} 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 = ;
if (index < ) {
// 小于0时,反向查找
index = (-index)-;
p = ZIPLIST_ENTRY_TAIL(zl);
if (p[] != ZIP_END) {
ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
while (prevlen > && index--) {
p -= prevlen;
ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
}
}
} else {
p = ZIPLIST_ENTRY_HEAD(zl);
while (p[] != ZIP_END && index--) {
p += zipRawEntryLength(p);
}
}
// 迭代完成还没找到元素 p[0]=ZIP_END
// index 超出整体ziplist大小则遍历完成后 index>0
return (p[] == ZIP_END || index > ) ? 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[] == ZIP_END) {
return NULL;
}
// 当前指针偏移当前元素长度(根据ziplist协议),即到下一元素指针位置
p += zipRawEntryLength(p);
if (p[] == 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[] == ZIP_END) return ;
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 ;
}
// 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) == ) { \
(prevlen) = (ptr)[]; \
} else if ((prevlensize) == ) { \
assert(sizeof((prevlensize)) == ); \
// 当ptr[0]>254时,代表内容有点大,需要使用 5个字符保存上一字符长度
memcpy(&(prevlen), ((char*)(ptr)) + , ); \
memrev32ifbe(&prevlen); \
} \
} while();
/* 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)[] < ZIP_BIGLEN) { \
(prevlensize) = ; \
} else { \
(prevlensize) = ; \
} \
} while();
/* 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) = ; \
(len) = (ptr)[] & 0x3f; \
}
// 1 << 6 =64
else if ((encoding) == ZIP_STR_14B) { \
(lensize) = ; \
(len) = (((ptr)[] & 0x3f) << ) | (ptr)[]; \
}
// 2 << 6 =128
else if (encoding == ZIP_STR_32B) { \
(lensize) = ; \
(len) = ((ptr)[] << ) | \
((ptr)[] << ) | \
((ptr)[] << ) | \
((ptr)[]); \
} else { \
assert(NULL); \
} \
} else { \
// 超过 0xc0 的长度了,直接使用 1,2,3,4 表示len
(lensize) = ; \
(len) = zipIntSize(encoding); \
} \
} while();
/* Extract the encoding from the byte pointed by 'ptr' and set it into
* 'encoding'. */
#define ZIP_ENTRY_ENCODING(ptr, encoding) do { \
(encoding) = (ptr[]); \
if ((encoding) < ZIP_STR_MASK) (encoding) &= ZIP_STR_MASK; \
} while() /* 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 = ;
// 针对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), );
if (fptr != NULL) {
/* Grab pointer to the value (fptr points to the field) */
// value 不可以为null, 否则 ziplist 将无法工作
vptr = ziplistNext(zl, fptr);
serverAssert(vptr != NULL);
update = ; /* 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 = ;
} 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 = ;
unsigned char vencoding = ;
long long vll = ; while (p[] != 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 == ) {
/* Compare current entry with specified entry */
// 针对不同的编码使用不同的比较方式
if (ZIP_IS_STR(encoding)) {
// 找到相应的元素,直接返回 p 指针
if (len == vlen && memcmp(q, vstr, vlen) == ) {
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 == ) {
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 = ;
size_t offset;
int nextdiff = ;
unsigned char encoding = ;
long long value = ; /* 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[] != ZIP_END) {
ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
} else {
unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
if (ptail[] != 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[] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : ; /* 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[] != ZIP_END) {
/* Subtract one because of the ZIP_END bytes */
// 字符拷贝
memmove(p+reqlen,p-nextdiff,curlen-offset-+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 != ) {
// 如果本次更新后数据位置变化,则需要更新后续的元素位置
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,);
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 = ;
} 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 % ) == ) {
addReplyError(c,"wrong number of arguments for HMSET");
return;
}
// 插入方式与 hset 一毛一样,差别在于批量插入时,会循环向 key-hash表中添加field->value
if ((o = hashTypeLookupWriteOrCreate(c,c->argv[])) == NULL) return;
hashTypeTryConversion(o,c->argv,,c->argc-);
// 循环insert
for (i = ; i < c->argc; i += ) {
hashTypeSet(o,c->argv[i]->ptr,c->argv[i+]->ptr,HASH_SET_COPY);
}
addReply(c, shared.ok);
signalModifiedKey(c->db,c->argv[]);
notifyKeyspaceEvent(NOTIFY_HASH,"hset",c->argv[],c->db->id);
server.dirty++;
}

三、hget 获取某字段值


  这种命令的时间复杂度都是 O(1), 所以一般是简单至上。

// t_hash.c
void hgetCommand(client *c) {
robj *o;
// 查找key, 不存在或者类型不一致则直接返回
if ((o = lookupKeyReadOrReply(c,c->argv[],shared.nullbulk)) == NULL ||
checkType(c,o,OBJ_HASH)) return;
// 基于o, 返回 field 对应的元素值即可
addHashFieldToReply(c, o, c->argv[]->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 < ) {
// 响应为空
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), );
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 ;
} return -;
} // 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[]);
if (o != NULL && o->type != OBJ_HASH) {
addReply(c, shared.wrongtypeerr);
return;
}
// 循环输出值
addReplyMultiBulkLen(c, c->argc-);
for (i = ; 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 = ;
int length, count = ; if ((o = lookupKeyReadOrReply(c,c->argv[],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[],&incr,NULL) != C_OK) return;
// 获取原值或者设置为0
if ((o = hashTypeLookupWriteOrCreate(c,c->argv[])) == NULL) return;
if (hashTypeGetValue(o,c->argv[]->ptr,&vstr,&vlen,&value) == C_OK) {
if (vstr) {
if (string2ll((char*)vstr,vlen,&value) == ) {
addReplyError(c,"hash value is not an integer");
return;
}
} /* Else hashTypeGetValue() already stored it into &value */
} else {
value = ;
} oldvalue = value;
if ((incr < && oldvalue < && incr < (LLONG_MIN-oldvalue)) ||
(incr > && oldvalue > && incr > (LLONG_MAX-oldvalue))) {
addReplyError(c,"increment or decrement would overflow");
return;
}
// 将相加后的值重置设置回hash表中
value += incr;
new = sdsfromlonglong(value);
hashTypeSet(o,c->argv[]->ptr,new,HASH_SET_TAKE_VALUE);
addReplyLongLong(c,value);
signalModifiedKey(c->db,c->argv[]);
notifyKeyspaceEvent(NOTIFY_HASH,"hincrby",c->argv[],c->db->id);
server.dirty++;
}

七、hdel 删除某字段


  hdel key field

// t_hash.c,
void hdelCommand(client *c) {
robj *o;
int j, deleted = , keyremoved = ; if ((o = lookupKeyWriteOrReply(c,c->argv[],shared.czero)) == NULL ||
checkType(c,o,OBJ_HASH)) return;
// 循环删除给定字段列表
for (j = ; j < c->argc; j++) {
if (hashTypeDelete(o,c->argv[j]->ptr)) {
deleted++;
// 当没有任何元素后,直接将key删除
if (hashTypeLength(o) == ) {
dbDelete(c->db,c->argv[]);
keyremoved = ;
break;
}
}
}
if (deleted) {
signalModifiedKey(c->db,c->argv[]);
notifyKeyspaceEvent(NOTIFY_HASH,"hdel",c->argv[],c->db->id);
if (keyremoved)
notifyKeyspaceEvent(NOTIFY_GENERIC,"del",c->argv[],
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 = ; 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), );
if (fptr != NULL) {
// ziplistDelete 为原地删除,所以只要调用2次,即把kv删除
zl = ziplistDelete(zl,&fptr);
zl = ziplistDelete(zl,&fptr);
o->ptr = zl;
deleted = ;
}
}
} else if (o->encoding == OBJ_ENCODING_HT) {
if (dictDelete((dict*)o->ptr, field) == C_OK) {
deleted = ; /* 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*/size < HASHTABLE_MIN_FILL));
}

  至此,整个hash数据结构的解析算是完整了。总体来说,hash由两种数据结构承载,ziplist在小数据量时使用,稍微复杂,但对于昂贵的内存来说是值得的。hash表在数据量大时使用,容易理解。通过本文的讲解,相信可以验证了你对redis hash 的实现的猜想了。

Redis(五):hash/hset/hget 命令源码解析的更多相关文章

  1. Spring系列(五):Spring AOP源码解析

    一.@EnableAspectJAutoProxy注解 在主配置类中添加@EnableAspectJAutoProxy注解,开启aop支持,那么@EnableAspectJAutoProxy到底做了什 ...

  2. Redis(七):set/sadd/sismember/sinter/sdiffstore 命令源码解析

    上两篇我们讲了hash和list数据类型相关的主要实现方法,同时加上前面对框架服务和string相关的功能介绍,已揭开了大部分redis的实用面纱. 现在还剩下两种数据类型: set, zset. 本 ...

  3. Redis(六):list/lpush/lrange/lpop 命令源码解析

    上一篇讲了hash数据类型的相关实现方法,没有茅塞顿开也至少知道redis如何搞事情的了吧. 本篇咱们继续来看redis中的数据类型的实现: list 相关操作实现. 同样,我们以使用者的角度,开始理 ...

  4. Redis(四):del/unlink 命令源码解析

    上一篇文章从根本上理解了set/get的处理过程,相当于理解了 增.改.查的过程,现在就差一个删了.本篇我们来看一下删除过程. 对于客户端来说,删除操作无需区分何种数据类型,只管进行 del 操作即可 ...

  5. Redis(八):zset/zadd/zrange/zrembyscore 命令源码解析

    前面几篇文章,我们完全领略了redis的string,hash,list,set数据类型的实现方法,相信对redis已经不再神秘. 本篇我们将介绍redis的最后一种数据类型: zset 的相关实现. ...

  6. 第五章 类加载器ClassLoader源码解析

    说明:了解ClassLoader前,先了解 第四章 类加载机制 1.ClassLoader作用 类加载流程的"加载"阶段是由类加载器完成的. 2.类加载器结构 结构:Bootstr ...

  7. Celery 源码解析五: 远程控制管理

    今天要聊的话题可能被大家关注得不过,但是对于 Celery 来说确实很有用的功能,曾经我在工作中遇到这类情况,就是我们将所有的任务都放在同一个队列里面,然后有一天突然某个同学的代码写得不对,导致大量的 ...

  8. dubbo源码解析五 --- 集群容错架构设计与原理分析

    欢迎来我的 Star Followers 后期后继续更新Dubbo别的文章 Dubbo 源码分析系列之一环境搭建 博客园 Dubbo 入门之二 --- 项目结构解析 博客园 Dubbo 源码分析系列之 ...

  9. [源码解析] NVIDIA HugeCTR,GPU 版本参数服务器 --(9)--- Local hash表

    [源码解析] NVIDIA HugeCTR,GPU 版本参数服务器 --(9)--- Local hash表 目录 [源码解析] NVIDIA HugeCTR,GPU 版本参数服务器 --(9)--- ...

随机推荐

  1. Storm使用总结

    Strom安装 Strom启动 ./zkServer.sh start 启动nimbus主节点: nohup bin/storm nimbus >> /dev/null & 启动s ...

  2. Java 学习笔记(9)——java常用类

    之前将Java的大部分语法都回顾完了,后面添加一些常见的操作,基础语法就结束了.至于在这里再次提到常用类是由于有一部分体现在使用它的继承类或者接口之类的.这些需要有面向对象编程的基础 Object类 ...

  3. Visual Studio Team Services and Team Foundation Server官方资料入口

    Team Foundation Server msdn 中文文档入口 Visual Studio Team Services or Team Foundation Server www.visuals ...

  4. Team Foundation Server 2015使用教程【2】:默认团队成员添加

    官方文档:https://www.visualstudio.com/en-us/docs/setup-admin/add-users

  5. CSDN博客 专用备份工具

    CSDN博客 专用备份工具 用要的朋友可下载. 本程序为个人所用,仅供学习.作者:潇湘博客网站:http://blog.csdn.NET/fkedwgwy默认文件存放位置为用户名文件夹下,也可以直接自 ...

  6. Struts2 基于XML校验(易百教程)

    以下是的各类字段级和非字段级验证在Struts2列表: date validator: <field name="birthday"> <field-valida ...

  7. 记录我的 python 学习历程-Day11 两个被忽视的坑、补充知识点、函数名的应用、新版格式化输出、迭代器

    补充知识点 函数形参中默认参数的陷阱 针对不可变数据类型,它是没有陷阱的 def func(name, sex='男'): print(name) print(sex) func('Dylan') # ...

  8. java中使用javaMail工具类发送邮件

    1.引入依赖 <!--javaMail--> <dependency> <groupId>javax.mail</groupId> <artifa ...

  9. ABP-多个DbContext实现事物更新

    1.在ABP中其实多个DbContext并没有在同一个事物中执行,那他是如何操作的,我的理解是 在不使用事物的时候 把多个DbContext存放在ActiveDbContexts 在调用工作单元的时候 ...

  10. 013 CephFS文件系统

    一.Ceph文件系统简介 CephFS提供兼容POSIX的文件系统,将其数据和与那数据作为对象那个存储在Ceph中 CephFS依靠MDS节点来协调RADOS集群的访问 元数据服务器 MDS管理元数据 ...