[redis 源码走读] 字典(dict)

本文深入探讨了Redis中字典(dict)数据结构的内部机制,包括哈希表的实现细节,如O(1)时间复杂度的高效读取,渐进式数据迁移策略,以及动态扩容和缩容功能。此外,还分析了字典在Redis数据库中的关键作用,如数据保存、查找和更新操作的流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

字典


redis 是 key-value 的 NoSQL 数据库,dict 是基本数据结构,dict 总体来说是一个哈希表,哈希表 O ( 1 ) O(1) O(1) 的时间复杂度,能高效进行数据读取。dict 还有动态扩容/缩容的功能,能灵活有效地使用机器内存。因为 redis 是单进程服务,所以当数据量很大的时候,扩容/缩容这些内存操作,涉及到新内存重新分配,数据拷贝。当数据量大的时候,会导致系统卡顿,必然会影响服务质量,redis 作者采用了渐进式的方式,将一次性操作,分散到 dict 对应的各个增删改查操作中。每个操作触发有限制数量的数据进行迁移。所以 dict 会有两个哈希表(dictht ht[2];),相应的 rehashidx 迁移位置,方便数据迁移操作。

更精彩内容,可以关注我的博客:wenfh2020.com


数据结构

结构

//字典
typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx;/* rehashing not in progress if rehashidx == -1 */
    int 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;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

// 数据类型,不同应用实现是不同的,所以用指针函数抽象出通用的接口,方便调用。
typedef struct dictType {
    unsigned int (*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);
} dictType;

时间复杂度(读数据)

查找数据,哈希表 O ( 1 ) O(1) O(1) 时间复杂度,但是哈希表也会存在碰撞问题,所以哈希索引指向的列表长度也会影响效率。

#define dictHashKey(d, key) (d)->type->hashFunction(key)

dictEntry *dictFind(dict *d, const void *key) {
    dictEntry *he;
    uint64_t h, idx, table;

    if (d->ht[0].used + d->ht[1].used == 0) return NULL; /* dict is empty */
    if (dictIsRehashing(d)) _dictRehashStep(d);
    h = dictHashKey(d, key);
    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;
        he = d->ht[table].table[idx];
        while(he) {
            // 如果 key 已经存在则返回错误。
            if (key==he->key || dictCompareKeys(d, key, he->key))
                return he;
            he = he->next;
        }

        // 如果数据正在迁移,从第二张表上查找。
        if (!dictIsRehashing(d)) return NULL;
    }
    return NULL;
}

工作流程

  • 堆栈调用流程,下面会通过这个堆栈函数调用时序,看以下写操作的源码流程:

调试方法,可以参考视频

#0  dictAdd (d=0x100529310, key=0x1018000b1, val=0x101800090) at dict.c:324
#1  0x000000010002bb9c in dbAdd (db=0x101005800, key=0x101800070, val=0x101800090) at db.c:159
#2  0x000000010002bd5c in setKey (db=0x101005800, key=0x101800070, val=0x101800090) at db.c:186
#3  0x000000010003abad in setGenericCommand (c=0x101015400, flags=0, key=0x101800070, val=0x101800090, expire=0x0, unit=0, ok_reply=0x0, abort_reply=0x0) at t_string.c:86
#4  0x000000010003afdd in setCommand (c=0x101015400) at t_string.c:139
#5  0x000000010001052d in call (c=0x101015400, flags=15) at server.c:2252
#6  0x00000001000112ac in processCommand (c=0x101015400) at server.c:2531
#7  0x0000000100025619 in processInputBuffer (c=0x101015400) at networking.c:1299
#8  0x0000000100021cb8 in readQueryFromClient (el=0x100528ba0, fd=5, privdata=0x101015400, mask=1) at networking.c:1363
#9  0x000000010000583c in aeProcessEvents (eventLoop=0x100528ba0, flags=3) at ae.c:412
#10 0x0000000100005ede in aeMain (eventLoop=0x100528ba0) at ae.c:455
#11 0x00000001000159d7 in main (argc=2, argv=0x7ffeefbff8c8) at server.c:4114

写数据

保存数据

数据库保存数据时,先检查这个键是否已经存在,从而分开添加,删除逻辑。

/* High level Set operation. This function can be used in order to set
 * a key, whatever it was existing or not, to a new object.
 *
 * 1) The ref count of the value object is incremented.
 * 2) clients WATCHing for the destination key notified.
 * 3) The expire time of the key is reset (the key is made persistent). */
void setKey(redisDb *db, robj *key, robj *val) {
    if (lookupKeyWrite(db,key) == NULL) {
        dbAdd(db,key,val);
    } else {
        dbOverwrite(db,key,val);
    }
    incrRefCount(val);
    removeExpire(db,key);
    signalModifiedKey(db,key);
}

添加数据

要添加一个元素,首先需要申请一个空间,申请空间涉及到是否需要扩容,key 是否已经存在了。

/* Add an element to the target hash table */
int dictAdd(dict *d, void *key, void *val) {
    dictEntry *entry = dictAddRaw(d,key);

    if (!entry) return DICT_ERR;
    dictSetVal(d, entry, val);
    return DICT_OK;
}

增加数据结点

/* Low level add. This function adds the entry but instead of setting
 * a value returns the dictEntry structure to the user, that will make
 * sure to fill the value field as he wishes.
 *
 * This function is also directly exposed to the user API to be called
 * mainly in order to store non-pointers inside the hash value, example:
 *
 * entry = dictAddRaw(dict,mykey);
 * if (entry != NULL) dictSetSignedIntegerVal(entry,1000);
 *
 * Return values:
 *
 * If key already exists NULL is returned.
 * If key was added, the hash entry is returned to be manipulated by the caller.
 */
dictEntry *dictAddRaw(dict *d, void *key) {
    int index;
    dictEntry *entry;
    dictht *ht;

    if (dictIsRehashing(d)) _dictRehashStep(d);

    /* Get the index of the new element, or -1 if
     * the element already exists. */
    // 检查 key 是否存在,避免重复添加。
    if ((index = _dictKeyIndex(d, key)) == -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. */
    // 如果哈希表正在迁移数据,操作哈希表2.
    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;
}

哈希索引

/* Returns the index of a free slot that can be populated with
 * a hash entry for the given 'key'.
 * If the key already exists, -1 is returned.
 *
 * Note that if we are in the process of rehashing the hash table, the
 * index is always returned in the context of the second (new) hash table. */
static int _dictKeyIndex(dict *d, const void *key) {
    unsigned int h, idx, table;
    dictEntry *he;

    /* Expand the hash table if needed */
    if (_dictExpandIfNeeded(d) == DICT_ERR)
        return -1;
    /* Compute the key hash value */
    h = dictHashKey(d, key);
    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;
        /* Search if this slot does not already contain the given key */
        he = d->ht[table].table[idx];
        while(he) {
            // 如果 key 已经存在则返回错误。
            if (key==he->key || dictCompareKeys(d, key, he->key))
                return -1;
            he = he->next;
        }

        // 如果哈希表处在数据迁移状态,从第二张表上查找。
        if (!dictIsRehashing(d)) break;
    }
    return idx;
}

数据迁移

哈希表数据迁移

避免数据量大,一次性迁移需要耗费大量资源。每次只迁移部分数据。

/* This function performs just a step of rehashing, and only if there are
 * no safe iterators bound to our hash table. When we have iterators in the
 * middle of a rehashing we can't mess with the two hash tables otherwise
 * some element can be missed or duplicated.
 *
 * This function is called by common lookup or update operations in the
 * dictionary so that the hash table automatically migrates from H1 to H2
 * while it is actively used. */
static void _dictRehashStep(dict *d) {
    if (d->iterators == 0) dictRehash(d,1);
}

/* Performs N steps of incremental rehashing. Returns 1 if there are still
 * keys to move from the old to the new hash table, otherwise 0 is returned.
 *
 * Note that a rehashing step consists in moving a bucket (that may have more
 * than one key as we use chaining) from the old to the new hash table, however
 * since part of the hash table may be composed of empty spaces, it is not
 * guaranteed that this function will rehash even a single bucket, since it
 * will visit at max N*10 empty buckets in total, otherwise the amount of
 * work it does would be unbound and the function may block for a long time. */
int dictRehash(dict *d, int n) {
    // empty_visits 记录哈希表最大遍历空桶个数。
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;

    // 从 ht[0] rehashidx 位置开始遍历 n 个桶进行数据迁移。
    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;

        /* Note that rehashidx can't overflow as we are sure there are more
         * elements because ht[0].used != 0 */
        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];
        /* Move all the keys in this bucket from the old to the new hash HT */
        while(de) {
            unsigned int 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++;
    }

    // 数据迁移完毕,重置哈希表两个 table。
    /* Check if we already rehashed the whole table... */
    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;
    }

    /* More to rehash... */
    return 1;
}

定时执行任务

/* Rehash for an amount of time between ms milliseconds and ms+1 milliseconds */
int dictRehashMilliseconds(dict *d, int ms) {
    long long start = timeInMilliseconds();
    int rehashes = 0;

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

扩容缩容

dict 是 redis 使用对基础数据之一,该数据结构有动态扩容和缩容功能。

是否需要扩容

/* Expand the hash table if needed */
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. */
    // 当使用的数据大于哈希表大小就可以扩展了。`dict_can_resize` 不允许扩展,那么数据的使用与哈希表的大小对比,要超出一个比率才能扩展内存。
    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;
}

扩容容量大小

/* Our hash table capability is a power of two */
static unsigned long _dictNextPower(unsigned long size) {
    unsigned long i = DICT_HT_INITIAL_SIZE;

    // 新容量大小是 2 的 n 次方,并且这个数值是第一个大于 2 * 原长度 的值。
    if (size >= LONG_MAX) return LONG_MAX;
    while(1) {
        if (i >= size)
            return i;
        i *= 2;
    }
}

扩容

/* Expand or create the hash table */
int dictExpand(dict *d, unsigned long size) {
    dictht n; /* the new hash table */
    unsigned long realsize = _dictNextPower(size);

    /* the size is invalid if it is smaller than the number of
     * elements already inside the hash table */
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    /* Rehashing to the same table size is not useful. */
    if (realsize == d->ht[0].size) return DICT_ERR;

    /* Allocate the new hash table and initialize all pointers to NULL */
    n.size = realsize;
    n.sizemask = realsize-1;
    n.table = zcalloc(realsize*sizeof(dictEntry*));
    n.used = 0;

    /* Is this the first initialization? If so it's not really a rehashing
     * we just set the first hash table so that it can accept keys. */
    // 如果哈希表还是空的,给表1分配空间,否则空间分配给表2
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }

    /* Prepare a second hash table for incremental rehashing */
    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;
}

缩容

  • 缩容,部分删除操作,会触发重新重新分配内存进行存储。
#define HASHTABLE_MIN_FILL        10      /* Minimal hash table fill 10% */

int zsetDel(robj *zobj, sds ele) {
    ...
    if (htNeedsResize(zs->dict)) dictResize(zs->dict);
    ...
}

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));
}

/* Resize the table to the minimal size that contains all the elements,
 * but with the invariant of a USED/BUCKETS ratio near to <= 1 */
int dictResize(dict *d) {
    int minimal;

    if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
    minimal = d->ht[0].used;
    if (minimal < DICT_HT_INITIAL_SIZE)
        minimal = DICT_HT_INITIAL_SIZE;
    return dictExpand(d, minimal);
}

参考


问题

  1. iterator 作用是啥。
  2. scan 的用法
<think>我们正在讨论Redis中serverCron的实现原理及其在过期键删除中的作用。根据引用[1]和[2],我们知道Redis使用惰性删除和定期删除两种策略,而定期删除就是由serverCron定时任务触发的。 首先,我们需要了解serverCron是什么。它是Redis服务器中的一个周期性操作函数,负责管理服务器的常规任务,如键过期、持久化、集群同步等。这个函数默认每秒执行10次(可通过配置hz参数调整)。 在serverCron中,过期键删除的具体流程如下: 1. 遍历所有数据库(默认16个)。 2. 对每个数据库,随机抽取一部分设置了过期时间的键(由ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP配置,默认20个)。 3. 检查这些键是否过期,如果过期则删除。 4. 如果本轮删除的过期键数量超过抽取样本的25%,则重复步骤2-3(即继续抽样删除),直到比例低于25%或达到时间限制(避免阻塞主线程)。 这种设计使得Redis在过期键清理上实现了时间与空间的平衡:既不会因为频繁扫描所有键而消耗过多CPU,也不会因清理不及时导致内存占用过高。 接下来,我们结合源码分析serverCron的实现(以Redis 6.0为例): ```c // server.c int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { // ... 其他任务(如更新时钟、处理客户端超时等) // 执行过期键清理 databasesCron(); // ... } void databasesCron(void) { // 如果开启了过期键清理功能 if (server.active_expire_enabled) { // 如果服务器是主节点,或者没有配置延迟监控,则执行主动过期 if (server.masterhost == NULL) { activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW); } else { expireSlaveKeys(); } } // ... 其他数据库维护任务(如resize哈希表) } ``` 其中,`activeExpireCycle`函数实现了定期删除的核心逻辑: ```c void activeExpireCycle(int type) { // ... do { // 随机抽取数据库中的过期键 // 如果当前数据库没有过期键,则跳过 if ((num = dictSize(db->expires)) == 0) { db->avg_ttl = 0; break; } slots = dictSlots(db->expires); now = mstime(); // 当样本数量不足且未超时,继续抽样 while (sampled < num && checked < slots) { // 随机选择一个槽位 de = dictGetRandomKey(db->expires); ttl = dictGetSignedIntegerVal(de)-now; if (ttl < 0) { // 键已过期,删除 deleteExpiredKeyAndPropagate(db,keyptr); expired++; } sampled++; } // 计算过期键比例 if (sampled) { percent = (expired*100)/sampled; } else { percent = 0; } // 如果过期比例超过25%,则继续在当前数据库清理 } while (percent > 25 && !timelimit_exceeded); } ``` 从源码可见,定期删除通过随机抽样和比例控制(25%阈值)实现了自适应清理:过期键越多时清理力度越大,反之则快速退出以节省CPU[^2]。 最后,serverCron的执行频率由`hz`参数控制(默认10,即每秒10次)。提高`hz`会增加CPU使用但提升过期键清理的及时性,需根据业务场景权衡[^1]。 --- §§相关问题§§ 1. 如何配置Redis的`hz`参数以平衡性能与过期键清理效率? 2. Redis的定期删除在集群模式下与主从复制如何协同? 3. 为什么Redis选择25%作为重复清理的阈值? 4. 如何监控Redis过期键的积累情况? [^1]: Redis 键的过期删除策略及缓存淘汰策略 [^2]: Redis(五):关于过期键(2)过期键的删除 [^3]: [redis 源码走读] redis 过期策略 [^4]: [redis 源码走读] - redis 过期策略 [^5]: Redis-数据库、键过期的实现</think>### Redis 中 `serverCron` 的实现原理与过期键删除机制 #### 一、`serverCron` 的核心作用 `serverCron` 是 Redis 的核心定时任务函数,默认**每秒执行 10 次**(通过配置文件 `hz` 参数调整)。它负责维护服务器状态,包括: 1. **过期键清理**(核心功能) 2. 持久化(RDB/AOF)管理 3. 主从复制协调 4. 集群状态同步 5. 客户端超时检测 6. 内存碎片整理[^1] #### 二、`serverCron` 的实现原理 1. **事件驱动架构** Redis 基于事件循环(`aeEventLoop`)运行,`serverCron` 作为**时间事件**注册到事件循环中: ```c // server.c aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL); ``` 其中 `1` 表示初始执行间隔(毫秒),实际间隔由 `hz` 动态调整[^3]。 2. **自适应执行机制** 每次执行时动态计算下次触发时间: ```c int serverCron(struct aeEventLoop *eventLoop, ...) { // 计算耗时并调整下次执行间隔 double elapsed_ms = calc_elapsed_time(); double next_delay = 1000/server.hz - elapsed_ms; return next_delay > 0 ? next_delay : 1; } ``` 这种设计确保**即使单次任务超时,也不会阻塞事件循环**。 3. **分阶段任务处理** 通过 `cronloops` 计数器实现任务分段执行: ```c static int cronloops = 0; // 全局计数器 cronloops++; if (cronloops % 10 == 0) { // 每10次循环执行低频任务 clusterCron(); // 集群维护 resizeReplicationBacklog(); // 复制缓冲区调整 } ``` #### 三、过期键删除在 `serverCron` 中的实现 在 `serverCron` 中调用 `databasesCron()` 处理过期键,核心流程如下: 1. **数据结构基础** 过期键存储在专属字典 `redisDb->expires` 中: ```c typedef struct redisDb { dict *dict; // 键空间(所有键值对) dict *expires; // 过期字典:key→指针, value→毫秒时间戳 } redisDb; // [^5] ``` 2. **定期删除算法** 每次随机抽取部分键检查(源码摘要): ```c void activeExpireCycle(int type) { for (j = 0; j < dbs_per_call; j++) { // 1. 随机抽取 keys_per_loop 个键(默认20) expired = 0; for (i = 0; i < keys_per_loop; i++) { de = dictGetRandomKey(db->expires); // 2. 检查是否过期(当前时间 > expires[de]) if (now > expire_time) { deleteExpiredKey(de); // 删除键 expired++; } } // 3. 自适应重试:若过期比例 >25% 则继续处理 do { if ((expired*100/sampled) <= 25) break; // 再次抽样删除... } while (expired > 0); } } ``` - **随机抽样**:避免全表扫描的 $O(n)$ 开销 - **25%阈值**:平衡 CPU 与内存效率(高过期率时加大清理力度) - **时间限制**:单次最长执行 25ms(防止阻塞主线程)[^2] 3. **协同删除机制** `serverCron` 的定期删除与**惰性删除**互补: - 定期删除:周期性减少内存占用 - 惰性删除:访问键时实时检查(`expireIfNeeded()` 函数) ```mermaid graph LR A[客户端访问键] --> B{检查 expires 字典} B -->|已过期| C[删除键并返回空] B -->|未过期| D[返回数据] ``` #### 四、性能优化设计 1. **渐进式 rehash** 当 `expires` 字典扩容/缩容时,结合定时任务逐步迁移数据: ```c if (dictIsRehashing(db->expires)) { dictRehashMilliseconds(db->expires,1); // 每次迁移1ms } ``` 2. **从库特殊处理** 从库的过期键由主库同步 DEL 命令删除,自身不主动清理(保持数据一致性)[^4]。 3. **内存淘汰联动** 当内存超限时,`serverCron` 会触发 `freeMemoryIfNeeded()` 执行 LRU/LFU 淘汰策略,与过期键清理协同释放内存。 #### 五、配置参数影响 | 参数 | 默认值 | 作用 | 对过期键清理的影响 | |------------------|--------|-------------------------------|----------------------------------| | `hz` | 10 | `serverCron` 每秒执行次数 | 值越大清理越及时,CPU 消耗越高 | | `maxmemory` | 0 | 最大内存限制 | 超限时触发额外淘汰机制 | | `active-expire-effort` | 1 | 清理力度(1-10) | 值越大抽样数量越多,清理更彻底 | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值