redis7.2.2|Dict

文章目录

  • Struct
    • redisDB
    • dict
    • dictType
    • dictEntry
  • 宏定义
  • 散列函数
  • 散列冲突
  • dictEntry pointer bit tricks[指针位技巧]
  • API implementation
    • _dictReset
    • _dictInit
    • dictCreate
    • dictGetHash
    • dictSetKey
    • dictSetVal
    • dictSetNext
    • dictGetNext
    • dictGetVal
    • dictGetKey
    • _dictClear
    • dictEmpty
    • dictRelease
  • 扩容
    • dictResize|dictExpand|_dictExpand|_dictNextExp
    • 扩容触发条件
      • **dictAdd**
      • **dictAddRaw**
      • **dictFindPositionForInsert**
      • **_dictExpandIfNeeded**
      • **dictTypeExpandAllowed**
      • **扩容策略**
      • **扩容触发条件**
      • **dictInsertAtPosition**
  • 缩容
      • dictDelete
      • htNeedsResize
      • 缩容条件
      • 缩容策略
  • 迁移
    • 迁移时机
    • 迁移步骤
      • _dictRehashStep
      • dictRehash
    • redis服务器定时任务----迁移数据
  • 旧版本中dict的结构
  • 总结

字典是Redis服务器中出现最为频繁的复合型数据结构,除了hash结构的数据会用到字典外,整个redis数据库的所有key和value也组成一个全局字典,还有带有过期时间的key集合也是一个字典。zet集合中存储value和score值的映射关系也是通过字典结构实现的。

先看一下redisDB的结构,它里面存储着数据字典和过期字典

Struct

redisDB

[redis-7.2.2\src\server.h]
redisDB

typedef struct redisDb {
	//全局数据字典
    dict *dict;                 /* The keyspace for this DB */
    //全局过期字典
    dict *expires;              /* Timeout of keys with a timeout set */
    dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP)*/
    dict *blocking_keys_unblock_on_nokey;   /* Keys with clients waiting for
                                             * data, and should be unblocked if key is deleted (XREADEDGROUP).
                                             * This is a subset of blocking_keys*/
    dict *ready_keys;           /* Blocked keys that received a PUSH */
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    //当前的数据库ID
    int id;                     /* Database ID */
    long long avg_ttl;          /* Average TTL, just for stats */
    unsigned long expires_cursor; /* Cursor of the active expire cycle. */
    list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
    clusterSlotToKeyMapping *slots_to_keys; /* Array of slots to keys. Only used in cluster mode (db 0). */
} redisDb;

dict

struct dict {
	//dictType是一个struct里面存储了各种与hashtable相关的function pointer,详细的见下面
    dictType *type;

	/*
	正常情况下,使用ht_table[0]存储数据;
	发生扩容或者缩容时,用ht_table[1]。
	*/
    dictEntry **ht_table[2];
    /*
    	对应着两个ht_table的元素个数;
    	ht_used[0]表示ht_table[0]的对象总数;
    	ht_used[1]表示ht_table[1]的对象总数.
    */
    unsigned long ht_used[2];

	/*
	标记当前的hashtable是否处于rehash状态,如果rehashidx==-1则当前没有处于rehash状态,如果rehashidx>=0则表明当前处于rehash状态并且按照rehashidx指示的顺序进行迁移。
	*/
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */

    /*
    该值大于0表示rehash终止,小于0表示编码错误
    */
    int16_t pauserehash; 
    
    /*
    通过ht_size_exp可以计算出两个表的一维长度也即是槽的个数。
    pow(2,ht_size_exp[0])表示ht_table[0]槽的个数;
    pow(2,ht_size_exp[1])表示ht_table[1]槽的个数;
    
    */
    signed char ht_size_exp[2]; 

	/*
	metadata是用于存储额外信息的字段,可以存储一些元数据
	
	*/
    void *metadata[];          
};

在这里插入图片描述

dictType

typedef struct dictType {
    uint64_t (*hashFunction)(const void *key);
    void *(*keyDup)(dict *d, const void *key);
    void *(*valDup)(dict *d, const void *obj);
    int (*keyCompare)(dict *d, const void *key1, const void *key2);
    void (*keyDestructor)(dict *d, void *key);
    void (*valDestructor)(dict *d, void *obj);
    int (*expandAllowed)(size_t moreMem, double usedRatio);
    /*
    如果设置了'no_value'标志,则表示没有使用值,即字典是一
    个集合。设置此标志时,无法访问dictEntry的值,也无法使用
    dicsetkey()。metadata也不能使用。
    */
    unsigned int no_value:1;
    /*
    如果no_value =1并且所有的键都是奇数(LSB=1),那么设置
    keys_are_odd =1可以实现另一个优化:在不分配dictEntry
    的情况下存储键。
    */
    unsigned int keys_are_odd:1;
 
    size_t (*dictEntryMetadataBytes)(dict *d);
    size_t (*dictMetadataBytes)(void);
    
    void (*afterReplaceEntry)(dict *d, dictEntry *entry);
} dictType;
  • set是一个集合,它的底层实现也是dict,但是对于集合来说只有key值没有value值;所以为了兼容set的实现,dictType中有一个标志性的字段"no_value",只要设置了该字段就表明这个dictEntry中只有key值没有value值,也就实现了set。

dictEntry

//dict中的entry
struct dictEntry {
    void *key;//指向key
    //union是指内存的同一个位置可以存储不同的数据类型,是为了兼容不同类型的value。
    //当value是uint64_t、int64_t、double的数据类型的时候,
    //可以直接内嵌在dictentry中,无需为此分配额外的内存,这样可以节省内存
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;     /* Next entry in the same hash bucket. 采用拉链法解决哈希冲突*/
    void *metadata[];           //存储额外的信息
    /*
    	一个任意数量的字节(从指针对齐的地址开始),大小由dictType
    	的dictEntryMetadataBytes()返回。
    */
};

当no_value=1时对应的结构是

//只有key没有value相当于set
typedef struct {
    void *key;
    dictEntry *next;
} dictEntryNoValue;

宏定义

[redis-7.2.2\src\dict.h]

//根据ht_size_exp计算dictht_size
#define DICTHT_SIZE(exp) ((exp) == -1 ? 0 : (unsigned long)1<<(exp))
//获取sizemask
#define DICTHT_SIZE_MASK(exp) ((exp) == -1 ? 0 : (DICTHT_SIZE(exp))-1)
//每一个hashtable的初始化大小
#define DICT_HT_INITIAL_EXP      2
#define DICT_HT_INITIAL_SIZE     (1<<(DICT_HT_INITIAL_EXP))
#define dictFreeVal(d, entry) do {                     \
    if ((d)->type->valDestructor)                      \
        (d)->type->valDestructor((d), dictGetVal(entry)); \
   } while(0)

#define dictFreeKey(d, entry) \
    if ((d)->type->keyDestructor) \
        (d)->type->keyDestructor((d), dictGetKey(entry))

#define dictCompareKeys(d, key1, key2) \
    (((d)->type->keyCompare) ? \
        (d)->type->keyCompare((d), key1, key2) : \
        (key1) == (key2))

#define dictEntryMetadataSize(d) ((d)->type->dictEntryMetadataBytes     \
                                  ? (d)->type->dictEntryMetadataBytes(d) : 0)
#define dictMetadataSize(d) ((d)->type->dictMetadataBytes               \
                             ? (d)->type->dictMetadataBytes() : 0)
//获取key的哈希值
#define dictHashKey(d, key) ((d)->type->hashFunction(key))
//获取两个dictht的总slot个数
#define dictSlots(d) (DICTHT_SIZE((d)->ht_size_exp[0])+DICTHT_SIZE((d)->ht_size_exp[1]))
//获取两个dictht的总对象个数
#define dictSize(d) ((d)->ht_used[0]+(d)->ht_used[1])
//判断当前是否处在rehash阶段
#define dictIsRehashing(d) ((d)->rehashidx != -1)
//终止rehash
#define dictPauseRehashing(d) ((d)->pauserehash++)
//重新开始rehash
#define dictResumeRehashing(d) ((d)->pauserehash--)

/* If our unsigned long type can store a 64 bit number, use a 64 bit PRNG. */
#if ULONG_MAX >= 0xffffffffffffffff
#define randomULong() ((unsigned long) genrand64_int64())
#else
#define randomULong() random()
#endif

散列函数

在这里插入图片描述

  • redis的字典默认的哈希函数是siphash,siphash算法即使在key很小的情况下,也可以产生随机性特别好的输出,性能非常突出。

散列冲突

  • 通过dictEntry的结构中包含"struct entry *next 指向同一个slot的下一个next entry"可以得出,dict采用"拉链法"解决"散列冲突"。

在这里插入图片描述

dictEntry pointer bit tricks[指针位技巧]

/* 
指向dictEntry的指针中的最低3位决定了该指针实际指向的是什么。如果最小的位被设置,它就是一个键值。否则,最少的3位有效位标记条目的类型。
*/

#define ENTRY_PTR_MASK     7 /* 111  与Mask相与获取数据的低三bit位*/
#define ENTRY_PTR_NORMAL   0 /* 000 已分配的entry并且带有value值*/
#define ENTRY_PTR_NO_VALUE 2 /* 010  已分配的entry但是不带有value值*/
/*
	返回1:entry pointer指向一个key值,而不是已经分配好的entry。
	其它情况返回0.
*/
static inline int entryIsKey(const dictEntry *de) {
//最低位设置了就是key
    return (uintptr_t)(void *)de & 1;
}
/*
	entry pointer指向的是一个dictentry struct返回1
	否则返回0
*/
static inline int entryIsNormal(const dictEntry *de) {
    return ((uintptr_t)(void *)de & ENTRY_PTR_MASK) == ENTRY_PTR_NORMAL;
}
/*
	entry 指向的是一个不带value值得 dictentry struct返回1
	否则返回0
 */
static inline int entryIsNoValue(const dictEntry *de) {
    return ((uintptr_t)(void *)de & ENTRY_PTR_MASK) == ENTRY_PTR_NO_VALUE;
}
/* Creates an entry without a value field. */
static inline dictEntry *createEntryNoValue(void *key, dictEntry *next) {
    dictEntryNoValue *entry = zmalloc(sizeof(*entry));
    entry->key = key;
    entry->next = next;
    //设置标记位ENTRY_PTR_NO_VALUE
    /*
    	void*是普通指针相互转换的桥梁
    	uintptr_t可以与void*相互转换
    	uintptr_t可以参与指针计算    	
    */
    return (dictEntry *)(void *)((uintptr_t)(void *)entry | ENTRY_PTR_NO_VALUE);
}
//编码,设置标志位
static inline dictEntry *encodeMaskedPtr(const void *ptr, unsigned int bits) {
    assert(((uintptr_t)ptr & ENTRY_PTR_MASK) == 0);
    return (dictEntry *)(void *)((uintptr_t)ptr | bits);
}
//解码,消除标志位
static inline void *decodeMaskedPtr(const dictEntry *de) {
	//非key才进行获取标志位操作
    assert(!entryIsKey(de));
    //"~"的优先级高于"&",先进行取反操作后进行按位与操作
    //&~操作消除左操作数中与右操作数对应位置的1
    return (void *)((uintptr_t)(void *)de & ~ENTRY_PTR_MASK);
}

//使用该函数之前确保dictEntry没有value,可以使用
//static inline int entryIsNoValue(const dictEntry *de) 进行判断
static inline dictEntryNoValue *decodeEntryNoValue(const dictEntry *de) {
    return decodeMaskedPtr(de);
}

//如果entry有一个value值,也就是正常的ENTRY_PTR_NORMAL,返回1
static inline int entryHasValue(const dictEntry *de) {
    return entryIsNormal(de);
}

API implementation

_dictReset

/* Reset hash table parameters already initialized with _dictInit()*/
static void _dictReset(dict *d, int htidx)
{
    d->ht_table[htidx] = NULL;
    d->ht_size_exp[htidx] = -1;
    d->ht_used[htidx] = 0;
}

_dictInit

/* Initialize the hash table */
int _dictInit(dict *d, dictType *type)
{
    _dictReset(d, 0);
    _dictReset(d, 1);
    d->type = type;
    d->rehashidx = -1;
    d->pauserehash = 0;
    return DICT_OK;
}

dictCreate

//创建一个新的hashtable
dict *dictCreate(dictType *type)
{
    size_t metasize = type->dictMetadataBytes ? type->dictMetadataBytes() : 0;
    dict *d = zmalloc(sizeof(*d) + metasize);
    if (metasize) {
        memset(dictMetadata(d), 0, metasize);
    }

    _dictInit(d,type);
    return d;
}

dictGetHash

uint64_t dictGetHash(dict *d, const void *key) {
    return dictHashKey(d, key);
}

dictSetKey

void dictSetKey(dict *d, dictEntry* de, void *key) {
	//如果dictEntry设置了no_value标识,则不能使用该函数
    assert(!d->type->no_value);
    if (d->type->keyDup)
        de->key = d->type->keyDup(d, key);
    else
        de->key = key;
}

dictSetVal

void dictSetVal(dict *d, dictEntry *de, void *val) {
	//entry有value值才可以设置
    assert(entryHasValue(de));
    //前面分析过,de->v是一个union
    de->v.val = d->type->valDup ? d->type->valDup(d, val) : val;
}

dictSetNext

static void dictSetNext(dictEntry *de, dictEntry *next) {
	//分配好的entry struct才可以设置next
    assert(!entryIsKey(de));
    if (entryIsNoValue(de)) {
        dictEntryNoValue *entry = decodeEntryNoValue(de);
        entry->next = next;
    } else {
        de->next = next;
    }
}

dictGetNext

/* Returns the 'next' field of the entry or 
NULL if the entry doesn't have a'next' field. 
*/
static dictEntry *dictGetNext(const dictEntry *de) {
    if (entryIsKey(de)) return NULL; /* there's no next */
    if (entryIsNoValue(de)) return decodeEntryNoValue(de)->next;
    return de->next;
}

dictGetVal

void *dictGetVal(const dictEntry *de) {
    assert(entryHasValue(de));
    return de->v.val;
}

dictGetKey

void *dictGetKey(const dictEntry *de) {
    if (entryIsKey(de)) return (void*)de;
    if (entryIsNoValue(de)) return decodeEntryNoValue(de)->key;
    return de->key;
}

_dictClear

/* Destroy an entire dictionary */
//销毁一个完整的字典
//callback回调函数,传入参数是dict*传出参数是void
int _dictClear(dict *d, int htidx, void(callback)(dict*)) {
    unsigned long i;

    //释放所有的元素
    for (i = 0; i < DICTHT_SIZE(d->ht_size_exp[htidx]) && d->ht_used[htidx] > 0; i++) {
        dictEntry *he, *nextHe;

        if (callback && (i & 65535) == 0) callback(d);

        if ((he = d->ht_table[htidx][i]) == NULL) continue;
        while(he) {
            nextHe = dictGetNext(he);
            //释放key
            dictFreeKey(d, he);
            //释放val
            dictFreeVal(d, he);
            //如果entry he不是一个key,释放entry struct
            if (!entryIsKey(he)) zfree(decodeMaskedPtr(he));
            //元素个数减一
            d->ht_used[htidx]--;
            he = nextHe;
        }
    }
    /* Free the table and the allocated cache structure */
    //释放整个表
    zfree(d->ht_table[htidx]);
    /* Re-initialize the table */
    //初始化d[htidx]
    _dictReset(d, htidx);
    return DICT_OK; /* never fails */
}

dictEmpty

void dictEmpty(dict *d, void(callback)(dict*)) {
//清除第一个表dictht[0]
    _dictClear(d,0,callback);
    //清除第二个表dictht[1]
    _dictClear(d,1,callback);
    //重置rehashidx和pauserehash
    d->rehashidx = -1;
    d->pauserehash = 0;
}

dictRelease

/* Clear & Release the hash table */
void dictRelease(dict *d)
{
//清除第一个表dictht[0]
    _dictClear(d,0,NULL);
    //清除第二个表dictht[1]
    _dictClear(d,1,NULL);
    //释放字典
    zfree(d);
}

扩容

dictResize|dictExpand|_dictExpand|_dictNextExp

这个函数是用来重新设置dictht的大小。在达到扩容条件或者缩容条件的时候,进行内存重新分配的时候就会用到此函数计算出正确的内存大小。

typedef enum {
    DICT_RESIZE_ENABLE,
    DICT_RESIZE_AVOID,
    DICT_RESIZE_FORBID,
} dictResizeEnable;
static dictResizeEnable dict_can_resize = DICT_RESIZE_ENABLE;
/*
将表的大小调整为包含所有元素的最小大小,并且还要保证负载因子"used(表中的元素个数)/size(表的长度)"<=1;
*/
int dictResize(dict *d)
{
    unsigned long minimal;
	//如果当前的hashtable处于不能resize或者正在rehash的状态,则不能进行扩容或者缩容
    if (dict_can_resize != DICT_RESIZE_ENABLE || dictIsRehashing(d)) return DICT_ERR;
    //获取旧表的元素个数
    minimal = d->ht_used[0];
    //#define DICT_HT_INITIAL_EXP      2
	//#define DICT_HT_INITIAL_SIZE     (1<<(DICT_HT_INITIAL_EXP))
    if (minimal < DICT_HT_INITIAL_SIZE)
        minimal = DICT_HT_INITIAL_SIZE;
    return dictExpand(d, minimal);
}
  • 第一步先矫正minimal。
  • 之后调用dictExpand,先根据_dictNextExp计算新表的容量之后重新分配内存。
/* return DICT_ERR if expand was not performed */
int dictExpand(dict *d, unsigned long size) {
    return _dictExpand(d, size, NULL);
}

辅助函数dictNextExp

//返回大于等于size的第一个pow(2,exp)中的exp
static signed char _dictNextExp(unsigned long size)
{
//#define DICT_HT_INITIAL_EXP      2
    unsigned char e = DICT_HT_INITIAL_EXP;//2

    if (size >= LONG_MAX) return (8*sizeof(long)-1);
    while(1) {
    
        if (((unsigned long)1<<e) >= size)
            return e;
        e++;
    }
}
/* 
 成功分配返回DICT_OK,否则返回DICT_ERR
 */
int _dictExpand(dict *d, unsigned long size, int* malloc_failed)
{
	//清空指针
    if (malloc_failed) *malloc_failed = 0;

    //正处于rehashing阶段或者申请内存小于当前元素个数[发生这种可能是刚刚迁移完数据,新表替代旧表]
    if (dictIsRehashing(d) || d->ht_used[0] > size)
        return DICT_ERR;

    // new hashtable
    dictEntry **new_ht_table;
    // the use of the new hashtable
    unsigned long new_ht_used;
    //根据size[旧表容量]计算出新表的容量
    //作用是返回第一次大于等于size的pow(2,exp)的exp
    signed char new_ht_size_exp = _dictNextExp(size);

    
    //检测是否溢出
    size_t newsize = 1ul<<new_ht_size_exp;
    if (newsize < size || newsize * sizeof(dictEntry*) < newsize)
        return DICT_ERR;

    //新表的大小等于旧表的大小
    if (new_ht_size_exp == d->ht_size_exp[0]) return DICT_ERR;

    /* Allocate the new hash table and initialize all pointers to NULL */
    //malloc_failed记录是否分配成功
    if (malloc_failed) {
        new_ht_table = ztrycalloc(newsize*sizeof(dictEntry*));
        *malloc_failed = new_ht_table == NULL;
        if (*malloc_failed)
            return DICT_ERR;
    } else
        new_ht_table = zcalloc(newsize*sizeof(dictEntry*));

    new_ht_used = 0;//重置新表的元素个数

    //d->ht_table[0]==NULL表明是第一次插入数据,此时d->ht_table被初始化默认的大小4
    if (d->ht_table[0] == NULL) {
        d->ht_size_exp[0] = new_ht_size_exp;
        d->ht_used[0] = new_ht_used;
        d->ht_table[0] = new_ht_table;
        return DICT_OK;
    }

    /* Prepare a second hash table for incremental rehashing */
    //为rehashing做准备,设置ht_table[1]
    d->ht_size_exp[1] = new_ht_size_exp;
    d->ht_used[1] = new_ht_used;
    d->ht_table[1] = new_ht_table;
    d->rehashidx = 0;//从0号索引开始迁移
    //此时只是为rehashing做了准备阶段,并没有真正的开启迁移数据
    return DICT_OK;
}

扩容触发条件

  • 随着数据量的不断增大,哈希碰撞产生的可能性越来越大,同一个槽内的链表越来越长,为了保持查找,删除,添加的效率,需要进行扩容。所以我们从dictAdd函数下手。

dictAdd

/* Add an element to the target hash table */
int dictAdd(dict *d, void *key, void *val)
{
	//dictAddRaw找出key合适的插入位置并插入,如果已经存在返回NULL
    dictEntry *entry = dictAddRaw(d,key,NULL);
    //key已经存在
    if (!entry) return DICT_ERR;
    //没有设置no_value标识,设置相应的value值
    if (!d->type->no_value) dictSetVal(d, entry, val);
    return DICT_OK;
}

dictAddRaw

/*

*/
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    //寻找key的插入位置,如果key已经存在返回NULL
    void *position = dictFindPositionForInsert(d, key, existing);
    //key已经存在
    if (!position) return NULL;

    /* Dup the key if necessary. */
    if (d->type->keyDup) key = d->type->keyDup(d, key);
    //在合适的位置插入key,此时不设置对应的key,是为了满足用户根据自己的意愿设置或者不设置value值。
    return dictInsertAtPosition(d, key, position);
}

dictFindPositionForInsert

//寻找目标key的插入位置,如果key已经存在返回NULL
void *dictFindPositionForInsert(dict *d, const void *key, dictEntry **existing) {
    unsigned long idx, table;
    dictEntry *he;
    //获取key对应的hash值
    uint64_t hash = dictHashKey(d, key);
    if (existing) *existing = NULL;
    /*
    如果当前处于rehash的状态[rehashidx!=-1],进行数据的迁移操作。
    这也是数据迁移触发的条件之一
    针对迁移的详细分析在下面。
    */
    if (dictIsRehashing(d)) _dictRehashStep(d);

    /* Expand the hash table if needed */
    if (_dictExpandIfNeeded(d) == DICT_ERR)
        return NULL;
    //开始查找
    for (table = 0; table <= 1; table++) {
    //哈希值与MASK相与,得出寻找的目标key在dictht的位置下标
        idx = hash & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
        //获取下标所在位置的第一个dictEntry
        he = d->ht_table[table][idx];
        //像遍历链表那样,依次寻找目标key
        while(he) {
        //获取dictEntry的key值
            void *he_key = dictGetKey(he);
            if (key == he_key || dictCompareKeys(d, key, he_key)) {
            //key存在,返回NULL;并将目标key存储在existing中
                if (existing) *existing = he;
                return NULL;
            }
            //继续寻找下一个entry
            he = dictGetNext(he);
        }
        //没有rehash,说明ht[1]没有,故不需要在ht[1]中寻找,直接退出外层循环。
        if (!dictIsRehashing(d)) break;
    }
    //走到这一步说明在ht中没有找到目标key,返回插入位置为插入操作做准备
    //如果正处于rehash,插入位置在ht[1]中,否则插入在ht[0]中。[头插法]
    dictEntry **bucket = &d->ht_table[dictIsRehashing(d) ? 1 : 0][idx];
    return bucket;
}

_dictExpandIfNeeded

/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
    //如果正在rehashing就直接返回
    //迁移数据的阶段不需要扩容
    if (dictIsRehashing(d)) return DICT_OK;

    
    //第一插入数据,将ht[0]扩大为初始的大小
    if (DICTHT_SIZE(d->ht_size_exp[0]) == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

     /*
     检验是否允许expand,详细代码如下
     不允许的话,直接返回。
     */
    if (!dictTypeExpandAllowed(d))
        return DICT_OK;
    //以下是扩容触发的条件
    if ((dict_can_resize == DICT_RESIZE_ENABLE &&
         d->ht_used[0] >= DICTHT_SIZE(d->ht_size_exp[0])) ||
        (dict_can_resize != DICT_RESIZE_FORBID &&
         d->ht_used[0] / DICTHT_SIZE(d->ht_size_exp[0]) > dict_force_resize_ratio))
    {

        return dictExpand(d, d->ht_used[0] + 1);
    }
    return DICT_OK;
}
/* Using dictEnableResize() / dictDisableResize() we make possible to
 * enable/disable resizing of the hash table as needed. This is very important
 * for Redis, as we use copy-on-write and don't want to move too much memory
 * around when there is a child performing saving operations.
 *  * Note that even when dict_can_resize is set to 0, not all resizes are
 * prevented: a hash table is still allowed to grow if the ratio between
 * the number of elements and the buckets > dict_force_resize_ratio. */
static unsigned int dict_force_resize_ratio = 5;
  • 翻译一下上述的那段英文。在进行copy-on-write[写时复制,执行bgsave或者aofrewrite的时候会发生cop],为了避免过多的内存从child中分离,此时不希望进行扩容操作。
  • 如果装载因子超过安全值,不论当前有没有发生写时复制都会执行扩容操作。

dictTypeExpandAllowed

/*
扩容的时候,因为要一次性分配足够大的内存,所以需要根据成员函数"expandAlloewd"判断是否能够完成本次分配。
*/
static int dictTypeExpandAllowed(dict *d) {
    if (d->type->expandAllowed == NULL) return 1;
    return d->type->expandAllowed(
                    DICTHT_SIZE(_dictNextExp(d->ht_used[0] + 1)) * sizeof(dictEntry*),
                    (double)d->ht_used[0] / DICTHT_SIZE(d->ht_size_exp[0]));
}

  • expandAllowed的第一个参数是计算扩容之后新的容量,也即是第一次大于等于(d->ht_used[0]+1)的pow(2,exp)[这里的d->ht_used[0]加1是因为,需要先判断是否需要扩容,接着才会把新的key加入;所以计算的时候,新key还没有加入表中但是要计算的时候要将其包括在内。]
  • expandAllowed的第二个参数是,哈希表的装载因子

扩容策略

在这里插入图片描述

  • 插入一个新的key之前,先判断是否需要扩容,如果需要扩容的话,先申请新的更大的空间,之后再将新的key插入新的表中。
  • 扩容的原则就是"在满足能够包含所有元素[包括新插入的key]的基础上,保证新表的大小是最小的pow(2,exp)"。

扩容触发条件

在这里插入图片描述

  • 扩容的触发条件1。装载因子大于等于1并且当前没有进行写时复制[bgsave,aofrewrite]
  • 扩容的触发条件2。装载因子大于5.

dictInsertAtPosition

通过"dictFindPositionForInsert"找到目标key的插入位置,[key已经存在返回NULL此时不需要插入]。通过"dictInsertAtPosition"插入目标key。


dictEntry *dictInsertAtPosition(dict *d, void *key, void *position) {
    dictEntry **bucket = position; /* It's a bucket, but the API hides that. */
    dictEntry *entry;
   //判断插入位置position是否适当合理
    int htidx = dictIsRehashing(d) ? 1 : 0;
    assert(bucket >= &d->ht_table[htidx][0] &&
           bucket <= &d->ht_table[htidx][DICTHT_SIZE_MASK(d->ht_size_exp[htidx])]);
    size_t metasize = dictEntryMetadataSize(d);
    //如果设置了no_value标识
    if (d->type->no_value) {
    //不能使用metadata
        assert(!metasize); /* Entry metadata + no value not supported. */
        if (d->type->keys_are_odd && !*bucket) {
            /* We can store the key directly in the destination bucket without the
             * allocated entry.
             *
             * TODO: Add a flag 'keys_are_even' and if set, we can use this
             * optimization for these dicts too. We can set the LSB bit when
             * stored as a dict entry and clear it again when we need the key
             * back. */
            entry = key;
            assert(entryIsKey(entry));
        } else {
            /* Allocate an entry without value. */
            entry = createEntryNoValue(key, *bucket);
        }
    } else {
       
        entry = zmalloc(sizeof(*entry) + metasize);
        assert(entryIsNormal(entry)); /* Check alignment of allocation */
        if (metasize > 0) {
            memset(dictEntryMetadata(entry), 0, metasize);
        }
        //link newentry
        entry->key = key;
        entry->next = *bucket;
    }
    *bucket = entry;
    d->ht_used[htidx]++;

    return entry;
}
  • 通过代码可以发现,插入的操作很简单就是头插法

缩容

  • 随着删除操作的不断执行,表中的数据越来越少,为了节约内存空间,需要把当前的表在满足装下所有元素的基础上进行缩小。所以从"Delete操作"入手。
    [redis-7.2.2\src\t_hash.c]
    在这里插入图片描述

dictDelete

//成功删除返回DICT_OK,否则返回DICT_ERR
int dictDelete(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,0) ? DICT_OK : DICT_ERR;
}
//找到目标key并删除
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
    uint64_t h, idx;
    dictEntry *he, *prevHe;
    int table;

    /* dict is empty */
    if (dictSize(d) == 0) return NULL;
    //此时正处于rehash阶段,先进行数据的迁移工作
    if (dictIsRehashing(d)) _dictRehashStep(d);
    
    //获取目标key的哈希值
    h = dictHashKey(d, key);

    for (table = 0; table <= 1; table++) {
    //获取目标key在ht中的位置下标
        idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
        //获取下标所在的bucket
        he = d->ht_table[table][idx];
        prevHe = NULL;
        //遍历链表
        while(he) {
        //获取entry指向的key
            void *he_key = dictGetKey(he);
            //找到目标key
            if (key == he_key || dictCompareKeys(d, key, he_key)) {
            	//目标key处在中间位置
                if (prevHe)
                    dictSetNext(prevHe, dictGetNext(he));
                else
                //当前目标节点是bucket中的第一个节点
                    d->ht_table[table][idx] = dictGetNext(he);
                if (!nofree) {
                //释放节点
                    dictFreeUnlinkedEntry(d, he);
                }
                //元素个数减一
                d->ht_used[table]--;
                return he;
            }
            //保存prevhe,继续检测下一个entry
            prevHe = he;
            he = dictGetNext(he);
        }
        //没有rehash无需寻找ht[1]
        if (!dictIsRehashing(d)) break;
    }
    return NULL; /* not found */
}

htNeedsResize

int htNeedsResize(dict *dict) {
    long long size, used;
	//获取dict中两个ht的buckets长度之和
    size = dictSlots(dict);
    //获取dict中两个ht的元素总个数
    used = dictSize(dict);
    //#define HASHTABLE_MIN_FILL        10      /* Minimal hash table fill 10% */
    return (size > DICT_HT_INITIAL_SIZE &&
            (used*100/size < HASHTABLE_MIN_FILL));
}

缩容条件

在这里插入图片描述

  • 缩容条件:装载因子小于0.1

缩容策略

if (htNeedsResize(o->ptr)) dictResize(o->ptr);
  • dictResize前面分析过:获取并修正旧表ht[0]的元素个数,以此为基础进行扩容,扩大[缩小为]为
    大于等于used的第一个pow(2,exp)

迁移

迁移时机

扩容只是第一步,接下来需要逐步,以增量的方式迁移数据,将旧表中的数据迁移到新表,然后用新表替代旧表,将新表重置待下次扩容[缩容]时使用。

通过以上"扩容,缩容的分析",我们可以发现,迁移触发的时机有以下两个
在这里插入图片描述

  • 在hashtable中寻找新key的目标位置的时候,如果当前正处在rehash阶段,会先rehash进行数据的迁移,[如果不处于rehash阶段,会先判断是否需要扩容,先扩容],然后会遍历key所在的槽判断key是否已经存在,不存在返回应该插入的位置,已经存在返回NULL。

在这里插入图片描述

  • 删除一个key的时候,如果当前处于rehash阶段,会先进行rehash数据的迁移,之后遍历寻找目标key,找到之后删除key。

总结一下,就是在执行写操作的时候会触发数据的迁移rehash。可是也存在一个问题,如果线上客户端都不在执行删除,修改,插入这些写操作,那永远都不会触发迁移,那旧表中的数据该如何安置。其实redis服务器会有定时任务进行数据的迁移,即使线上没有写操作的执行也会进行数据的迁移。定时任务在后面介绍

接下俩看看,迁移的流程

迁移步骤

_dictRehashStep

/*
该函数由字典中的常见查找或更新操作调用,以便哈希表在活跃使用时自动从H1迁移到H2。也就是所查找和更新操作会触发rehash而且每次只rehash一个位置处[rehashidx指向的位置]的key链表
*/
static void _dictRehashStep(dict *d) {
//如果rehash没有被终止,就进行数据的迁移
    if (d->pauserehash == 0) dictRehash(d,1);
}

dictRehash

/*
执行N步增量散列。如果仍有键需要从old-hashtable迁移到new-hashtable则返回1,否则返回0;
一个rehash步骤在于移动一个bucket[包括产生hash冲突的key即一个槽内所有的key]从old-hashtable到new-hashtable;
由于hashtable的一部分可能由空白空间组成,因此不能保证此函数会rehash整个单个bucket,因为最多可以访问N*10个empty bucket,超过将会停止当前轮的rehash,该函数会阻塞很长时间
*/
int dictRehash(dict *d, int n) {
    int empty_visits = n*10; //最多能够访问N*10个empty bucket
    //hashatable[0]的size
    unsigned long s0 = DICTHT_SIZE(d->ht_size_exp[0]);
    
    //hashatable[1]的size
    unsigned long s1 = DICTHT_SIZE(d->ht_size_exp[1]);
    
    //如果当前的状态处于禁止rehash或者,没有设置rehashidx直接返回
    if (dict_can_resize == DICT_RESIZE_FORBID || !dictIsRehashing(d)) return 0;
    
    
    if (dict_can_resize == DICT_RESIZE_AVOID && 
        ((s1 > s0 && s1 / s0 < dict_force_resize_ratio) ||
         (s1 < s0 && s0 / s1 < dict_force_resize_ratio)))
    {
        return 0;
    }
    
	//开始进行迁移,n表示迁移的步数
	//如果过hashtable[0]没有元素则表示迁移结束
    while(n-- && d->ht_used[0] != 0) {
        dictEntry *de, *nextde;

        /*
        确保rehashidx没有超出范围 
        */
        assert(DICTHT_SIZE(d->ht_size_exp[0]) > (unsigned long)d->rehashidx);

		//从rehashidx开始,寻找一个非空的bucket,最多只能有n*10个empty bucket
        while(d->ht_table[0][d->rehashidx] == NULL) {
            d->rehashidx++;//遍历下一个bucket
            if (--empty_visits == 0) return 1;
        }
        //获取rehashidx指向槽的第一个Entry
        de = d->ht_table[0][d->rehashidx];
        //相当于遍历单链表
        while(de) {
            uint64_t h;
			//获取下一个dictentry
            nextde = dictGetNext(de);
            //从de中找出key
            void *key = dictGetKey(de);
            
            //扩容状态,获取entry在新表中的位置
            if (d->ht_size_exp[1] > d->ht_size_exp[0]) {
            //重新计算哈希值
                h = dictHashKey(d, key) & DICTHT_SIZE_MASK(d->ht_size_exp[1]);
            } else {
                //处在缩容阶段
                h = d->rehashidx & DICTHT_SIZE_MASK(d->ht_size_exp[1]);
            }
            //设置了no_value标识
            if (d->type->no_value) {
                if (d->type->keys_are_odd && !d->ht_table[1][h]) {
                    
                    assert(entryIsKey(key));
                    if (!entryIsKey(de)) zfree(decodeMaskedPtr(de));
                    de = key;
                } else if (entryIsKey(de)) {
                    /* We don't have an allocated entry but we need one. */
                    de = createEntryNoValue(key, d->ht_table[1][h]);
                } else {
                    /* Just move the existing entry to the destination table and
                     * update the 'next' field. */
                    assert(entryIsNoValue(de));
                    dictSetNext(de, d->ht_table[1][h]);
                }
            } else {
            //将de插入newhashtable对应的位置处
                dictSetNext(de, d->ht_table[1][h]);
            }
            //修改槽的位置
            d->ht_table[1][h] = de;
            //ht[0]的元素个数减1
            d->ht_used[0]--;
            //ht[1]的元素个数加1
            d->ht_used[1]++;
            //继续迁移槽内的下一个entry
            de = nextde;
        }
        //ht[0]rehashidx指向的bucket已经全部迁移到ht[1]中,将其置为空
        d->ht_table[0][d->rehashidx] = NULL;
        d->rehashidx++;//为下一次rehash做准备
    }

    /* Check if we already rehashed the whole table... */
    //rehash完成,释放hashtable[0];
    //hashatable[1]成为hashtable[0]
    if (d->ht_used[0] == 0) {
    //释放ht[0]
        zfree(d->ht_table[0]);
        /* Copy the new ht onto the old one */
        d->ht_table[0] = d->ht_table[1];
        d->ht_used[0] = d->ht_used[1];
        d->ht_size_exp[0] = d->ht_size_exp[1];
        
        //重置ht1
        _dictReset(d, 1);
        //关闭rehash标识
        d->rehashidx = -1;
        //迁移全部完成返回0
        return 0;
    }

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

迁移的顺序

  • 通过源码分析,可以得出迁移的顺序是按照"rehashidx"进行的,并且每次只迁移一个非空的bucket,寻找非空的bucket时最多只能扫描到n*10个 empty bucket。[这里的n表示每次进行几次迁移操作,每次只迁移一次]

迁移何时停止,或者说如何判断迁移是否完成

  • 分析源码可以发现,ht_used[0]==0表示数据迁移全部完成。即是否迁移完成是根据旧表是否还有元素来进行判断的。

redis服务器定时任务----迁移数据

为了防止,线上的客户端不再执行写操作(hset,hdel等指令),导致迁移操作无法继续进行。redis会在定时任务对字典进行主动搬迁。
[redis-7.2.2\src\server.c]


void databasesCron(void) {
    //通过随机采样删除过期key,从节点不需要主动删除
    //通过主节点同步del语句即可
    if (server.active_expire_enabled) {
        if (iAmMaster()) {
            activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
        } else {
            expireSlaveKeys();
        }
    }

    /* Defrag keys gradually. */
    activeDefragCycle();

    /*
	如果需要,执行散列表重新散列,但只有在没有其他进程在磁盘上保存DB时才执行。
	否则,重新散列是不好的,因为会导致大量的写时复制内存页。
    */
    if (!hasActiveChildProcess()) {
        
        static unsigned int resize_db = 0;
        static unsigned int rehash_db = 0;
        //#define CRON_DBS_PER_CALL 16
        int dbs_per_call = CRON_DBS_PER_CALL;
        int j;

        /* Don't test more DBs than we have. */
        if (dbs_per_call > server.dbnum) dbs_per_call = server.dbnum;

        /* Resize */
        for (j = 0; j < dbs_per_call; j++) {
            tryResizeHashTables(resize_db % server.dbnum);
            resize_db++;
        }

        /* 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;
                }
            }
        }
    }
}
/* If the percentage of used slots in the HT reaches HASHTABLE_MIN_FILL
 * we resize the hash table to save memory */
void tryResizeHashTables(int dbid) {
    if (htNeedsResize(server.db[dbid].dict))
        dictResize(server.db[dbid].dict);
    if (htNeedsResize(server.db[dbid].expires))
        dictResize(server.db[dbid].expires);
}
/*
我们的哈希表实现在我们从哈希表中写入/读取时执行增量的重新哈希。但是,如果服务器空闲,哈希表将长时间使用两个表。因此,我们尝试在每次调用该函数时使用1毫秒的CPU时间来执行一些重新散列。
如果执行了一些散列,该函数返回1,否则返回0。
*/
int incrementallyRehash(int dbid) {
    /* Keys dictionary */
    if (dictIsRehashing(server.db[dbid].dict)) {
        dictRehashMilliseconds(server.db[dbid].dict,1);
        return 1; /* already used our millisecond for this loop... */
    }
    /* Expires */
    if (dictIsRehashing(server.db[dbid].expires)) {
        dictRehashMilliseconds(server.db[dbid].expires,1);
        return 1; /* already used our millisecond for this loop... */
    }
    return 0;
}

旧版本中dict的结构

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

/* 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;



typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

//Union允许在同一内存位置存储不同的数据类型,而Enum用于定义一组相关常量并赋予它们有意义的名称。

在这里插入图片描述

总结

负载因子ratio=used/buckets[表中的元素数量/哈希表一维数组长度]

扩容时机

  • ratio>=1并且不发生aofwrite,bgsave,这样做是为了防止更多的内存分离
  • ratio>5,强制进行扩容

缩容时机

  • ratio<0.1强制缩容,为了节省内存空间

扩容策略

  • 新表的大小为第一次大于原表uesd+1的2的次幂[redis3.2.100版本是原表used*2]

缩容策略

  • 容纳所有元素的最小大小;新表的大小为第一次大于原表uesd的2的次幂[redis3.2.100版本是原表used]

迁移的实现

  • 迁移的时机。在"dictFindPositionForInsert"和"dictGenericDelete"这两个函数中会发生数据的迁移dictRehash,也就是线上执行写操作的时候会触发rehash。为了防止客户端下线不再执行写操作,dict长时间占用着两个表,redis后台线程会专门维护一个定时任务用来进行数据的迁移。
  • 迁移的顺序。按照rehashidx指向的顺序进行数据的迁移,而且每次只迁移一个bucket。
  • 迁移完成。如果旧表中的元素个数变为0则表明迁移完成。(通过源码可以发现,最外层循环是通过判断旧表中的元素个数是否为0,不为0的话进入循环内会继续判断rehashidx是否超过旧表数组的一维长度,所以我认为判断迁移是否完成是通过used和rehashidx共同完成的。)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/425134.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

五、西瓜书——集成学习

1.个体与集成 集成学习通过将多个学习器进行结合,常可获得比单一学习器显著优越的泛化性能&#xff0c;这对“弱学习器”(weak learner)尤为明显因此集成学习的很多理论研究都是针对弱学习器进行的而基学习器有时也被直接称为弱学习器。 要获得好的集成个体学习器应“好而不同”…

详解JavaScript的函数

详解 JavaScript 的函数 函数的语法格式 创建函数/函数声明/函数定义 function 函数名(形参列表) { 函数体 return 返回值; // return 语句可省略 } 函数调用 函数名(实参列表) // 不考虑返回值 返回值 函数名(实参列表) // 考虑返回值 示例代码 //定义的没有参数列表&am…

5个好玩神奇还免费的工具网站收藏不后悔-搜嗖工具箱

生命倒计时 http://www.thismuchlonger.com 这是一个相哇塞的网站&#xff0c;可以让我们静下心来好好想想我们来这个世界究竟为了什么&#xff0c;因为当我们作为命运的主宰者。敲打键盘设定好自己一生长度的时候&#xff0c;我们的剩余寿命已经成绝对值&#xff0c;一旦生命…

mysql5.7配置主从

原理&#xff1a; MySQL主从复制的工作原理如下:1. 主服务器产生Binlog日志当主服务器的数据库发生数据修改操作时,如INSERT、UPDATE、DELETE语句执行,主服务器会记录这些操作的日志信息到二进制日志文件中。2. 从服务器读取Binlog日志 从服务器会向主服务器发送请求,主服务器把…

Linux网络编程——socket 通信基础

Linux网络编程——socket 通信基础 1. socket 介绍2. 字节序2.1 简介2.2 字节序举例2.3 字节序转换函数 3. socket 地址3.1 通用 socket 地址3.2 专用 socket 地址 4. IP地址转换&#xff08;字符串ip -> 整数&#xff0c;主机、网络字节序的转换 &#xff09;5. TCP 通信流…

智能驾驶规划控制理论学习05-车辆运动学规划案例分析

目录 案例一——Hybrid A*&#xff08;基于正向运动学&#xff09; 1、基本思想 2、 实现流程 3、启发函数设计 4、分析扩张&#xff08;Analytic Expansions&#xff09; 5、分级规划&#xff08;Hierarchical planning&#xff09; 案例二——State Lattice Planning&…

Vue3快速上手(十六)Vue3路由传参大全

Vue3路由传参 一、传参的多种方式 1.1 拼接方式 这种方式适合传递单个参数的情况&#xff0c;比如点击查看详情&#xff0c;传个id这样的场景 传参&#xff1a; <RouterLink to"/person?id1" active-class"active">person</RouterLink> …

RabbitMQ相关问题

RabbitMQ相关问题 一、RabbitMQ的核心组件和工作原理&#xff1f;二、如何保证消息可靠投递不丢失的&#xff1f;三、RabbitMQ如何保证消息的幂等性&#xff1f;四、什么是死信队列&#xff1f;死信队列是如何导致的&#xff1f;五、RabbitMQ死信队列是如何导致的&#xff1f;六…

PDF 解析问题调研

说点真实的感受 &#xff1a;网上看啥组件都好&#xff0c;实际测&#xff0c;啥组件都不行。效果好的不开源收费&#xff0c;开源的效果不好。测试下来&#xff0c;发现把组件融合起来&#xff0c;还是能不花钱解决问题的&#xff0c;都是麻烦折腾一些。 这里分享了目前网上能…

数据结构 第3章 栈、队列和数组(一轮习题总结)

第3章 栈、队列和数组 3.1 栈3.2 队列3.3 栈与队列的应用3.4 数组和特殊矩阵 3.1 栈&#xff08;1 10 11 20&#xff09; 3.2 队列&#xff08;6 12 14 17&#xff09; 3.3 栈与队列的应用&#xff08;6 11&#xff09; 3.4 数组和特殊矩阵 3.1 栈 T1 栈和队列具有相同的逻辑…

一周学会Django5 Python Web开发-Django5详细视图DetailView

锋哥原创的Python Web开发 Django5视频教程&#xff1a; 2024版 Django5 Python web开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili2024版 Django5 Python web开发 视频教程(无废话版) 玩命更新中~共计28条视频&#xff0c;包括&#xff1a;2024版 Django5 Python we…

Linux-信号2

文章目录 前言一、信号是如何保存的&#xff1f;int sigemptyset(sigset_t *set);int sigfillset(sigset_t *set);int sigaddset (sigset_t *set, int signo);int sigdelset(sigset_t *set, int signo);int sigismember&#xff08;const sigset_t *set, int signo);int sigpen…

leetcode 长度最小的子数组

在本题中&#xff0c;我们可以知道&#xff0c;是要求数组中组成和为target的最小子数组的长度。所以&#xff0c;我们肯定可以想到用两层for循环进行遍历&#xff0c;然后枚举所有的结果进行挑选&#xff0c;但这样时间复杂度过高。 我们可以采用滑动窗口&#xff0c;其实就是…

NoSQL--1.虚拟机网络配置

目录 1.初识NoSQL 1.1 NoSQL之虚拟机网络配置 1.1.1 首先&#xff0c;导入预先配置好的NoSQL版本到VMware Workstation中 1.1.2 开启虚拟机操作&#xff1a; 1.1.2.1 点击开启虚拟机&#xff1a; 1.1.2.2 默认选择回车CentOS Linux&#xff08;3.10.0-1127.e17.x86_64) 7 …

同样是证书,NPDP和PMP有什么区别?

PMP和NPDP的区别是啥&#xff1f; PMP、NPDP证书考哪个更有用&#xff1f;还是两个都考&#xff1f; PMP和NPDP哪个更适合现在及以后发展&#xff1f; PMP和NPDP这两哪个含金量更高&#xff1f; 一&#xff0c;关于PMP和NPDP PMP和NPDP都是美国PMI/PDMA的专业考试&#xf…

C语言中的分支和循环语句:从入门到精通

分支和循环语句 1. 前言2. 预备知识2.1 getchar函数2.2 putchar函数2.3 计算数组的元素个数2.4 清屏2.5 程序的暂停2.6 字符串的比较 3. 结构化3.1 顺序结构3.2 分支结构3.3 循环结构 4. 真假性5. 分支语句&#xff08;选择结构&#xff09;5.1 if语句5.1.1 语法形式5.1.2 else…

Stable Cascade又升级了,现在只需要两个模型

Stable Cascade这个模型&#xff0c;大家如果还有印象的话&#xff0c;是需要下载三个模型的&#xff0c;分别是Stage_a,Stage_b和Stage_c,如果全都下载下来&#xff0c;需要20多个G&#xff0c;但是最近使用ComfyUI做尝试的时候&#xff0c;发现官方的案例中已经没有用到单独的…

数据审计 -本福德定律 Benford‘s law (sample database classicmodels _No.6)

数据审计 -本福德定律 Benford’s law 准备工作&#xff0c;可以去下载 classicmodels 数据库资源如下 [ 点击&#xff1a;classicmodels] 也可以去我的博客资源下载 文章目录 数据审计 -本福德定律 Benfords law 前言一、什么是 本福德定律&#xff1f;二、数学公式三、应用…

单细胞Seurat - 降维与细胞标记(4)

本系列持续更新Seurat单细胞分析教程&#xff0c;欢迎关注&#xff01; 非线形降维 Seurat 提供了几种非线性降维技术&#xff0c;例如 tSNE 和 UMAP&#xff0c;来可视化和探索这些数据集。这些算法的目标是学习数据集中的底层结构&#xff0c;以便将相似的细胞放在低维空间中…