Redis学习-hash类型基本知识与底层实现

Redis学习-hash类型基本知识与底层实现

一、基本知识

1、基本操作

hash一共有15个指令,下面我们来执行以下所有的指令,看看它的效果和作用。
具体指令说明可以参考官方文档说明:http://www.redis.cn/commands.html#hash

127.0.0.1:6379> hset redis string good // 设置redis,string项为good
(integer) 1
127.0.0.1:6379> hget redis string // 获取redis的string项
"good"
127.0.0.1:6379> hmset redis hash notgood set verygood 
OK
127.0.0.1:6379> hmget redis string hash
1) "good"
2) "notgood"
127.0.0.1:6379> hlen redis // 获取redis所有项的数量
(integer) 3
127.0.0.1:6379> hvals redis // 获取redis所有项的值
1) "good"
2) "notgood"
3) "verygood"
127.0.0.1:6379> hgetall redis // 获取redis的全部项和值
1) "string"
2) "good"
3) "hash"
4) "notgood"
5) "set"
6) "verygood"
127.0.0.1:6379> hset redis num 1 
(integer) 1
127.0.0.1:6379> hincrby redis num 1 // 增加整数项num的值
(integer) 2
127.0.0.1:6379> hget redis num 
"2"
127.0.0.1:6379> hexists redis num // 判断redis的num项是否存在
(integer) 1
127.0.0.1:6379> hdel redis num // 删除redis的num项
(integer) 1
127.0.0.1:6379> hget redis num
(nil)
127.0.0.1:6379> hstrlen redis hash // 获取reids的hash项的值长度
(integer) 7

2、hash存储结构对象的好处

string类型和hash类型在实际项目中都可以用于存储一个结构对象,常用的方法有

方法优点缺点
string类型,对象id为key,对象json序列化字符串为value1、可以一次性获取对象的全部属性 2、内存占用小1、修改一个属性需要全部更新
string类型,对象id加属性名为key,属性值为value1、修改单一属性值方便 2、每个属性都可以有独自的过期时间1、要为每个属性创建redisObject,内存占用大 2、获取全部属性麻烦
hash类型,对象id为key,属性为feild,属性值为value1、可以获取单一属性,也可以获取全部属性 2、修改单一属性方便 3、占用内存小1、所有属性共用一个过期时间

3、hash整合数据的优缺点

string类型和hash类型在实际项目中也可以用于统计数据,因为两个类型都带有incr方法,同样的它们各有各的优缺点。

方法优点缺点
string类型,统计类型加对象id为key,统计值为value1、结构简单,容易实现 2、每个统计对象都有独自的过期时间1、获取统计集麻烦,统计项分散
hash类型,统计类型为key,对象为feild,统计值为value1、结构较为简单,容易实现 2、获取所有统计信息容易 3、一个统计类型一个过期时间1、统计过大过量的数据,获取需要花费时间

4、注意事项

  • hash类型的value只能存储字符串,不能存储其他类型
  • 每个hash可以存储2的32次方减一个键值对
  • hash不可存储大量对象
  • hgetall操作可以获取全部属性,如果field过多,遍历效率会过低

二、底层实现

1、redisObject

前面讲解 string类型 的时候已经讲过redisObject各个属性的作用,这里我们就直接看一下刚刚设置的hash键redis的对象属性。

127.0.0.1:6379> type redis // 类型hash
hash
127.0.0.1:6379> object encoding redis  // 这里编码采用了ziplist
"ziplist"
127.0.0.1:6379> object refcount redis // 引用计数为1
(integer) 1
127.0.0.1:6379> object idletime redis 
(integer) 1272
127.0.0.1:6379> hset redis big64 gooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooood // 设置一个超过64长度的值
(integer) 1
127.0.0.1:6379> object encoding redis // 这里编码变为hashtable
"hashtable"

2、编码转换

redis给hash类型设置了两种编码方式,一种是OBJ_ENCODING_ZIPLIST-- 压缩链表,一种是OBJ_ENCODING_HT – 字典。在一般情况下,redis都会采用压缩链表的方式进行编码,但是当下列两种情况任意一种发生,redis都会转换编码方式,将压缩链表转换为字典
①hash对象保存的键值对的键或值的大小大于hash_max_ziplist_value
②hash对象保存的键值对数量大于hash_max_ziplist_entries
redis会在两个地方检验是否进行编码转换:
①每次set前,都先判断客户端传入的参数列表中是否有大于hash_max_ziplist_value,有一个就转换编码
②在set之后,判断键值对数量是否大于hash_max_ziplist_entries,是的话就转换编码

/* redis6.2.5中,hash_max_ziplist_value 与  hash_max_ziplist_entries 的配置 */
createSizeTConfig("hash-max-ziplist-entries", NULL, MODIFIABLE_CONFIG, 0, LONG_MAX, server.hash_max_ziplist_entries, 512, INTEGER_CONFIG, NULL, NULL),
createSizeTConfig("hash-max-ziplist-value", NULL, MODIFIABLE_CONFIG, 0, LONG_MAX, server.hash_max_ziplist_value, 64, MEMORY_CONFIG, NULL, NULL),

3、ziplist – 压缩列表

前面讲了,redis对hash有两种编码,一种就是压缩列表,下面我们就对压缩列表进一步了解。

3.1 压缩列表的结构

ziplist是一种顺序型链表,它的一般结构如下所示:
|zlbytes | zltail | zllen | entry | entry | … | entry | zlend|

字段类型说明
zlbytesuint32_t压缩列表占用的内存字节数
zltailuint32_t表尾节点与列表起始地址的偏移字节量
zllenuint16_t小于UINT16_MAX下的节点数量
entry节点压缩列表的实际节点
zlenduint8_t压缩列表结尾标志,0xFF=255

下面看一下实际节点entry的结构,包括三个属性,
| prevlen | encoding | entry-data |

字段说明
prevlen记录前一节点的长度,便于程序指针运算获取前一节点的地址,实现表尾到表头的查询,1字节或者5字节
encoding记录保存数据的类型与长度,ziplist可以保持整数和string,具体编码可以查看ziplist.c的说明
entry-data记录保存的值
3.2 压缩列表的连锁更新

压缩列表的entry中的prevlen记录了前一节点的长度,当前一节点的长度小于254字节的时候,它会使用一个字节的空间保存这个值,否则使用5字节的空间保存这个值。因为这个机制,在极端的情况下,有可能导致连锁更新
考虑下面两种情况:

  • 在压缩列表中,有连续的多个节点长度介于250-253字节,这个时候在前面插入一个大于254字节的新节点,后面的节点prevlen都会变成5字节,引发连锁更新
  • 在压缩列表中,小于254字节节点后面跟有连续的多个节点长度介于254-257字节,这个时候删除该节点后的第一个节点,后面的节点prevlen都会变成1字节,引发连锁更新

不管删除还是插入操作,都有可能引发连锁更新,最坏的情况下,可能执行N次更新,而每次更新的复杂度o(N),使得整个连锁更新复杂度达到o(N^2)。
但是我们注意到,实际应用中发生连锁更新的可能性是比较低的,正常情况下删除与插入操作的复杂度都是o(N)。

4、dict – 字典

redis中的字典应用比较广泛,不仅仅用在hash类型的实现,还用在数据库键值对存储、过期键记录等。一个优秀的字典,增删改查的速度都非常快,平均在o(1)的复杂度。下面我们来看一下redis的字典实现。

4.1 字典的结构
字段说明
typedictType的指针,dictType是用于保存特定类型键值对的函数
privdata保存传输特定函数的可选参数
ht哈希表数组,ht[0]是当前使用哈希表,ht[1]重新散列使用哈希表
rehashidx目前重新散列的进度,正常情况下为-1
pauserehash重新散列是否暂停
typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
} dict;

typedef struct dictType {
    uint64_t (*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); /* 值析构 */
    int (*expandAllowed)(size_t moreMem, double usedRatio); /* 扩展 */
} dictType;
4.2 哈希表的结构

redis的哈希存储也是数组链表的结构,在发生冲突的情况下,会将新的节点接到对应索引的老节点后面,使得索引所对应的数据是一个链表结构,链表里都是通过哈希算法计算得到该索引的对象。哈希表的结构包括下面4个字段。

字段说明
table哈希表数组
size哈希表大小
sizemask哈希表大小掩码,用于计算索引值
used哈希表已有节点数量,会用于计算负载因子,判断是否重新散列
/* 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;
4.3 哈希节点
typedef struct dictEntry {
    void *key; /* 保存键 */
    union { 
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v; /* 保存值,值可以是一个指针,一个uint64_t整数,或int64_t整数 */
    struct dictEntry *next;  /* 指向链表的下一个节点,用于解决键冲突 */
} dictEntry;
4.4 字典重新散列 – rehash

redis的重新散列是通过负载因子来判定的,负载因子的大小 = used / size, 当负载因子太大或太小的情况下,会对哈希表的大小进行对应的扩展或收缩。

  • 当服务器没有执行持久化命令的情况下,负载因子大于等于1,执行扩展rehash
  • 如果服务器正在执行持久化命令,为节约内存,避免同时操作,负载因子要大于等于5,才会执行扩展rehash

redis的rehash并不是一次性做完的,而是分多次渐进的进行的,主要是因为当redis的hash键值对数量过于庞大,一次性执行,可能会导致服务器阻塞一段时间不能服务。
rehash具体流程:

  1. 为ht[1]分配空间,空间大小为min(2^n) > h[0].used
  2. 维护rehashindex,将它的值设置为0,开始rehash
  3. 每执行一步rehash,rehashindex增1
  4. 全部执行完,将rehashindex置为-1,将ht[0]释放,ht[1]改为ht[0],生成一个新的ht[1]
/* rehash执行ms毫秒,每次执行100步--dictRehash(d,100),最后返回rehash步数 */
int dictRehashMilliseconds(dict *d, int ms) {
    if (d->pauserehash > 0) return 0;

    long long start = timeInMilliseconds();
    int rehashes = 0;

    while(dictRehash(d,100)) {
        rehashes += 100;
        if (timeInMilliseconds()-start > ms) break;
    }
    return rehashes;
}

/* 执行N步rehash。如果还有未转移的key,则返回1,否则返回0
 * 如果原hash表中的空位置太多,则最多访问10*n次空位置,避免该函数执行时间过长造成阻塞 */
int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* 最大空位置访问数量 */
    if (!dictIsRehashing(d)) return 0;

    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;
        assert(d->ht[0].size > (unsigned long)d->rehashidx);
        /* 找到对应索引 */
        while(d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        de = d->ht[0].table[d->rehashidx];
        /* 转移该索引下的所有链表节点 */
        while(de) {
            uint64_t h;

            nextde = de->next;
            /* Get the index in the new hash table */
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            d->ht[0].used--;
            d->ht[1].used++;
            de = nextde;
        }
        d->ht[0].table[d->rehashidx] = NULL;
        d->rehashidx++;
    }

    /* 检测是否所有的key已经转移,是返回0 */
    if (d->ht[0].used == 0) {
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1;
        return 0;
    }
    return 1;
}

5、创建hash

void hsetCommand(client *c) {
    int i, created = 0;
    robj *o;

    if ((c->argc % 2) == 1) {
        addReplyErrorFormat(c,"wrong number of arguments for '%s' command",c->cmd->name);
        return;
    }

    if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
    hashTypeTryConversion(o,c->argv,2,c->argc-1);

    for (i = 2; i < c->argc; i += 2)
        created += !hashTypeSet(o,c->argv[i]->ptr,c->argv[i+1]->ptr,HASH_SET_COPY);

    /* HMSET (deprecated) and HSET return value is different. */
    char *cmdname = c->argv[0]->ptr;
    if (cmdname[1] == 's' || cmdname[1] == 'S') {
        /* HSET */
        addReplyLongLong(c, created);
    } else {
        /* HMSET */
        addReply(c, shared.ok);
    }
    signalModifiedKey(c,c->db,c->argv[1]);
    notifyKeyspaceEvent(NOTIFY_HASH,"hset",c->argv[1],c->db->id);
    server.dirty += (c->argc - 2)/2;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值