形象化设计模式实战 HELLO!架构 redis命令源码解析
前面讲过了字典和压缩列表的实现,redis的哈希数据就是存储在这两种结构之中的,如果对这两种结构都非常清楚了,那么对哈希命令的实现的理解将会非常简单。
一、hset
void hsetCommand(redisClient *c) {
int update;
robj *o;
// 取出或新创建哈希对象
if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
// 如果需要的话,转换哈希对象的编码
hashTypeTryConversion(o,c->argv,2,3);
// 编码 field 和 value 对象以节约空间
hashTypeTryObjectEncoding(o,&c->argv[2], &c->argv[3]);
// 设置 field 和 value 到 hash
update = hashTypeSet(o,c->argv[2],c->argv[3]);
// 返回状态:显示 field-value 对是新添加还是更新
addReply(c, update ? shared.czero : shared.cone);
// 发送键修改信号
signalModifiedKey(c->db,c->argv[1]);
// 发送事件通知
notifyKeyspaceEvent(REDIS_NOTIFY_HASH,"hset",c->argv[1],c->db->id);
// 将服务器设为脏
server.dirty++;
}
这里深入hashTypeTryConversion函数看下是如何进行转换的:
void hashTypeTryConversion(robj *o, robj **argv, int start, int end) {
int i;
// 如果对象不是 ziplist 编码,那么直接返回
if (o->encoding != REDIS_ENCODING_ZIPLIST) return;
// 检查所有输入对象,看它们的字符串值是否超过了指定长度
for (i = start; i <= end; i++) {
if (sdsEncodedObject(argv[i]) &&
sdslen(argv[i]->ptr) > server.hash_max_ziplist_value)
{
// 将对象的编码转换成 REDIS_ENCODING_HT
hashTypeConvert(o, REDIS_ENCODING_HT);
break;
}
}
}
可以看到如果哈希表中的某个键或值大于server.hash_max_ziplist_value,就会被转换为REDIS_ENCODING_HT(字典)类型。
再深入hashTypeSet函数中,会发现这么一段代码:
if (hashTypeLength(o) > server.hash_max_ziplist_entries)
hashTypeConvert(o, REDIS_ENCODING_HT);
可以看出如果压缩列表中的节点数大于server.hash_max_ziplist_entries,也将会被转换为REDIS_ENCODING_HT(字典)类型。
由上可见:server.hash_max_ziplist_value和server.hash_max_ziplist_entries是影响哈希表选择何种数据结构的重要参数。
二、hash_max_ziplist_value与hash_max_ziplist_entries
在redis.conf中,我们可以找到这两参数的定义:
大致是说对于数量少的数据来说,使用ziplist的结构全更加节省内存。
我们可以根据自己的需要修改这两参数,但是切勿认为ziplist节省内存就将这两参数修改过大。
三、hdel命令,转换回REDIS_ENCODING_ZIPLIST?
上面说压缩节点过多就会转换为字典,那么如果hdel命令将节点减少了,会不会又转换回压缩列表呢?
int hashTypeDelete(robj *o, robj *field) {
int deleted = 0;
// 从 ziplist 中删除
if (o->encoding == REDIS_ENCODING_ZIPLIST) {
unsigned char *zl, *fptr;
field = getDecodedObject(field);
zl = o->ptr;
fptr = ziplistIndex(zl, ZIPLIST_HEAD);
if (fptr != NULL) {
// 定位到域
fptr = ziplistFind(fptr, field->ptr, sdslen(field->ptr), 1);
if (fptr != NULL) {
// 删除域和值
zl = ziplistDelete(zl,&fptr);
zl = ziplistDelete(zl,&fptr);
o->ptr = zl;
deleted = 1;
}
}
decrRefCount(field);
// 从字典中删除
} else if (o->encoding == REDIS_ENCODING_HT) {
if (dictDelete((dict*)o->ptr, field) == REDIS_OK) {
deleted = 1;
/* Always check if the dictionary needs a resize after a delete. */
// 删除成功时,看字典是否需要收缩
if (htNeedsResize(o->ptr)) dictResize(o->ptr);
}
} else {
redisPanic("Unknown hash encoding");
}
return deleted;
}
我们看到字典中删除成功并没有检查节点的数量从而选择是否转换为压缩列表,而是对它本身字典进行resize。
由于可见
压缩列表转换为字典是不可逆的。至于为什么?这就有点像ziplist的只扩展不收缩,防止哈希表在这种两种结构中转来转去,降低性能。