Redis之字典

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: Redis之字典

字典结构

字典(dict)是Redis中使用最频繁的数据结构,除了hash会使用到字典(dict),整个Redis数据库中的所有key/value也组成了一个全局字典,

带有过期时间的key集合也是一个字典,zset集合中的value和score值映射也是通过字典实现的,Set也是字典实现,只是Value都是NULL。

字典结构中包含两个hashtable,一般情况下只有一个hashtable有值,在字典拓容缩容时,需要分配新的hashtable, 此时需要进行渐进式搬迁,

这时候两个hashtable一个存储旧数据,另一个存储新数据,搬迁结束后,旧hashtable被删除,新hashtable取而代之。

struct dict {

  dictht ht[2];
  ....

}

image.png

hashtable结构与Java的HashMap基本类似,都通过分桶的方式解决hash冲突,一维结构是数组,二维结构是链表,数组每个节点存储链表的第一个元素指针。

struct dictht {
    dictEntry** table; //链表
    long size; // 一维数组长度
    long used; // hash表中的元素个数
    ....
}


struct dictEntry {
    void* key;
    void* val;
    dictEntry* next; //下一个entry
}

image.png

渐进式hash

字典比较大的情况下,扩容非常耗费时间,因为要重新申请新的数组,然后将旧字典中的所有链表重新hash到新数组中,是O(n)级别的操作,会引起Redis阻塞,因此redis使用渐进式rehash,一点一点扩容。

dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    long index;
    dictEntry *entry;
    dictht *ht;

    // 渐进式rehash
    if (dictIsRehashing(d)) _dictRehashStep(d);

    /* Get the index of the new element, or -1 if
     * the element already exists. */
    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
        return NULL;

    /* Allocate the memory and store the new entry.
     * Insert the element in top, with the assumption that in a database
     * system it is more likely that recently added entries are accessed
     * more frequently. */
    // 如果字典处于搬迁中,将新元素挂在新的数组下面
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    entry = zmalloc(sizeof(*entry));
    entry->next = ht->table[index];
    ht->table[index] = entry;
    ht->used++;

    /* Set the hash entry fields. */
    dictSetKey(d, entry, key);
    return entry;
}

搬迁操作可以在对字典操作的hset、hdel等指令中进行,如果没有指令到来,Redis继续在定时任务中对字典进行主动搬迁,直到搬迁完毕。

// 定时任务
void databasesCron(void) {
    ......
        /* Rehash 继续搬迁 */
        if (server.activerehashing) {
            for (j = 0; j < dbs_per_call; j++) {
                int work_done = incrementallyRehash(rehash_db);
                if (work_done) {
                    /* If the function did some work, stop here, we'll do
                     * more at the next cron loop. */
                    break;
                } else {
                    /* If this db didn't need rehash, we'll try the next one. */
                    rehash_db++;
                    rehash_db %= server.dbnum;
                }
            }
        }
}

查找过程

新增和删除操作都必须先定位元素所在的链表上,然后对链表元素进行操作,redis中的hash函数会将key映射为一个整数,不同的key会被分布成比较均匀散乱的整数,只有hash值均匀,字典才是稳定平衡的,链表中的元素不会过多,查找起来速度也就更快。

func get(key) {
    let index = hash_func(key) % size;
    let entry = table[index];
    while(entry != NULL) {
        if entry.key == target {
            return entry.value;
        }
        entry = entry.next;
    }

}

hash函数与hash攻击

字典的性能好不好取决于hash函数,key被分布的越均匀性能就越好,redis默认的hash函数是siphash, 该算法在输入的key很小的情况下,也可以产生随机性很好的结果。

如果hash函数存在偏向性,此时客户端大量输入特定的key,将会导致字典的二维链表元素长度过长,当很多的元素在链表中时,字典的查找复杂度由O(1)退化的O(N),服务器可能会被拖垮,这种就是hash攻击。

拓容条件与缩容条件

正常情况下,当字典中的元素个数超过第一维数组时,将进行拓容,扩容的新数组大小是原来的2倍,但是redis如果正在做bgsave,为了减少内存页过多的COW,则尽量不去拓容,但是如果字典中的元素个数达到了一维数组的5倍,此时将会强制拓容。

static int _dictExpandIfNeeded(dict *d)
{
    /* Incremental rehashing already in progress. Return. */
    if (dictIsRehashing(d)) return DICT_OK;

    /* If the hash table is empty expand it to the initial size. */
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

    /* If we reached the 1:1 ratio, and we are allowed to resize the hash
     * table (global setting) or we should avoid it but the ratio between
     * elements/buckets is over the "safe" threshold, we resize doubling
     * the number of buckets. */
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
    {
        return dictExpand(d, d->ht[0].used*2);
    }
    return DICT_OK;
}

当字典中的元素被删除的越来越稀疏时,redis将对字典进行缩容操作,减少一维数组空间占用,缩容条件是元素个数低于数组长度的10%,并且缩容并不会考虑Redis是否在bgsave。

int htNeedsResize(dict *dict) {
    long long size, used;

    size = dictSlots(dict);
    used = dictSize(dict);
    return (size > DICT_HT_INITIAL_SIZE &&
            (used*100/size < HASHTABLE_MIN_FILL));
}
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
6月前
|
存储 NoSQL Java
【Redis系列】那有序集合为什么要同时使用字典和跳跃表
面试官问:那有序集合为什么要同时使用字典和跳跃表来实现?我:这个设计主要是考虑了性能因素。1. 如果单纯使用字典,查询的效率很高是O(1),但执行类似ZRANGE、ZRNK时,排序性能低。每次排序需要在内存上对字典进行排序一次,同时消耗了额外的O(n)内存空间
【Redis系列】那有序集合为什么要同时使用字典和跳跃表
|
缓存 NoSQL 数据建模
【Redis源码】dict字典学习(三)
【Redis源码】dict字典学习(三)
79 0
|
6月前
|
存储 缓存 NoSQL
【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(字典)(一)
【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(字典)
111 0
|
缓存 NoSQL Redis
【Redis 系列】redis 学习十六,redis 字典(map) 及其核心编码结构
【Redis 系列】redis 学习十六,redis 字典(map) 及其核心编码结构
|
6月前
|
存储 NoSQL 算法
【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(字典)(二)
【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(字典)
115 0
|
4月前
|
存储 Java
Redis08命令-Hash类型,也叫散列,其中value是一个无序字典,类似于java的HashMap结构,Hash结构可以将对象中的每个字段独立存储,可以针对每字段做CRUD
Redis08命令-Hash类型,也叫散列,其中value是一个无序字典,类似于java的HashMap结构,Hash结构可以将对象中的每个字段独立存储,可以针对每字段做CRUD
|
存储 NoSQL Redis
Redis从入门到精通之底层数据结构字典/哈希表详解
Redis中的字典(Dictionary)是一种高效的数据结构,用于存储键值对,常用于实现哈希表(Hash Table)。在本文中,我们将深入了解Redis中的字典/哈希表,包括字典的结构和操作等。字典/哈希表适合存储大量的键值对,并需要快速地查找键对应的值的场景。在实际应用中,需要根据具体的业务场景选择合适的底层数据结构。例如,如果需要按照键的顺序进行访问,可以使用有序集合(Sorted Set)等其他数据结构。
249 10
Redis从入门到精通之底层数据结构字典/哈希表详解
|
NoSQL 安全 算法
redis6.0源码分析:字典扩容与渐进式rehash
redis6.0源码分析:字典扩容与渐进式rehash
198 0
|
存储 NoSQL Java
Redis源码剖析之字典(dict)
dict中的hashtable在出现hash冲突时采用的是开链方式,如果有多个entry落在同一个bucket中,那么他们就会串成一个单链表存储。
67 0
|
机器学习/深度学习 NoSQL 算法
Redis的设计与实现(3)-字典
Redis 的数据库使用字典实现, 对数据库的增, 删, 查, 改也是构建在对字典的操作之上的. 字典是哈希键的底层实现之一: 当一个哈希键包含的键值对比较多, 又或者键值对中的元素都是比较长的字符串时, Redis 将会使用字典作为哈希键的底层实现.
80 2