哈希字典Dict

目录

Dict的组成 

1.hash表的节点

 2.hash表结构

3.字典

4.Dict结构

hash算法

哈希函数

什么情况下使用

rehash

rehash主要的函数dictExpand

怎么判断要进行扩容还是收缩

什么时候会用到扩展和收缩

渐进式rehash

渐进式rehash主要的函数dictRehash

字典API

字典的创建dictCreate

增加键值对dictAdd

查找给定键的值dictFetchValue

 删除给定的键dictDelete

添加或覆盖键值对(更新键值对)dictReplace


Redis是一个键值型(Key - Value Pair)的数据库,我们可以根据键实现快速的增删改查。而键与值的映射关系正是通过Dict来实现的。

Dict的组成 

Dict由三部分组成,分别是:哈希表(DictHashTable)、哈希节点(DictEntry)、字典(Dict)

1.hash表的节点

//dict.h
typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;
  • hash表的节点, 主要是包含了key, val键值对和下一个节点的指针,。从该结构体我们可以看出, 这些节点会组成一个单向链表。
  • hash表的key是 Void * 指针类型,可以存储多种类型的数据, 值 v 是一个共用体, 也可以是uint64整数, 也可以是int64整数, 还可以是浮点数。

 2.hash表结构

typedef struct dictht {
    dictEntry **table;    //hash表数组,元素是 dictEntry*指针类型的
    unsigned long size;    //数组大小
    unsigned long sizemask;     //hash数组长度掩码, sizemask =  size - 1
    unsigned long used;    //hash表的已有的entry的个数
} dictht;
  • 由hash表结构, 可知redis是通过数组做hash散列的.
  • hash表数组的 size 长度一般为 2^n 的大小, 保证这样的大小可以通&操作来计算出hash槽的位置。因为如果数组容量不是2^n的大小, 那么就需要通过取模来获取hash表的槽索引, 取模操作相对&操作性能会差一些.
  • szimmask 表示数组掩码, 一般是 size -1, 如: 假如数组长度为8, 那么掩码就为7, 掩码转换成二进制就为 ob00000111 

3.字典

  • type 和 privdata 字段是针对不同类型的键值对,为创建多态字典而设计的。
  • type 字段是一个指向 dictType 结构的指针,每个 dictType 结构保存了一簇用于操作特定类型键值对的函数,Redis 会为用途不同的字典设置不同的类型特定函数。
  • privdata 字段则保存了需要传给那些类型特定函数的可选参数。
  • ht 字段是一个包含两个项的数组,数组中的每个项都是一个 dictht 哈希表。一般情况下,只使用ht[0],只有当哈希表的键值对数量超过负载(元素过多)时,才会将键值对迁移到ht[1],这一步迁移被称为 rehash (重哈希)。
  • rehashidx 字段也是和 rehash 有关,用于记录目前 rehash 的进度,如果没有 rehash,值就是 -1。
  • pauserehash代表rehash是否暂停
typedef struct dict {
    dictType *type;    //内置一些不同类型的hash函数
    void *privdata;    //携带的私有数据
    dictht ht[2];     //hash字典的数组, 长度为2, 主要是用于渐进式hash时使用
    long rehashidx;    //rehash的进度, -1表示未进行
    int16_t pauserehash;    //rehash是否暂停, 1则暂停,0则继续
} dict;

//字典类型    该结构体内部是一些函数指针
typedef struct dictType {
    uint64_t (*hashFunction)(const void *key);    //计算hash值的函数
    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.Dict结构

了解了上面的三个结构体,就可以知道Dict的结构了。

Dict使用了哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶。所以,我们常说,一个哈希表是由多个哈希桶组成的,每个哈希桶中保存了键值对数据。

hash表一般的实现都是数组+链表来实现。 数组来做hash散列, 链表用来解决hash冲突的问题

hash算法

哈希函数

类型处理函数中的第一个函数 hashFunction 就是计算某个键的哈希值的函数,对于不同类型的 key,哈希值的计算是不同的,所以在字典进行创建的时候,需要指定哈希函数。

redis默认使用的是dictGenHashFunction函数。dictGenHashFunction内部就只是调用了siphash函数。siphash是复杂的,但我们只需要知道有这么些函数就行了。

uint64_t dictGenHashFunction(const void *key, int len) {
    return siphash(key,len,dict_hash_function_seed);
}

uint64_t siphash(const uint8_t *in, const size_t inlen, const uint8_t *k) {
#ifndef UNALIGNED_LE_CPU
    uint64_t hash;
    uint8_t *out = (uint8_t*) &hash;
#endif
    uint64_t v0 = 0x736f6d6570736575ULL;
    uint64_t v1 = 0x646f72616e646f6dULL;
    uint64_t v2 = 0x6c7967656e657261ULL;
    uint64_t v3 = 0x7465646279746573ULL;
    uint64_t k0 = U8TO64_LE(k);
    uint64_t k1 = U8TO64_LE(k + 8);
    uint64_t m;
    const uint8_t *end = in + inlen - (inlen % sizeof(uint64_t));
    const int left = inlen & 7;
    uint64_t b = ((uint64_t)inlen) << 56;
    v3 ^= k1;
    v2 ^= k0;
    v1 ^= k1;
    v0 ^= k0;

    for (; in != end; in += 8) {
        m = U8TO64_LE(in);
        v3 ^= m;

        SIPROUND;

        v0 ^= m;
    }

    switch (left) {
    case 7: b |= ((uint64_t)in[6]) << 48; /* fall-thru */
    case 6: b |= ((uint64_t)in[5]) << 40; /* fall-thru */
    case 5: b |= ((uint64_t)in[4]) << 32; /* fall-thru */
    case 4: b |= ((uint64_t)in[3]) << 24; /* fall-thru */
    case 3: b |= ((uint64_t)in[2]) << 16; /* fall-thru */
    case 2: b |= ((uint64_t)in[1]) << 8; /* fall-thru */
    case 1: b |= ((uint64_t)in[0]); break;
    case 0: break;
    }

    v3 ^= b;

    SIPROUND;

    v0 ^= b;
    v2 ^= 0xff;

    SIPROUND;
    SIPROUND;

    b = v0 ^ v1 ^ v2 ^ v3;
#ifndef UNALIGNED_LE_CPU
    U64TO8_LE(out, b);
    return hash;
#else
    return b;
#endif
}

什么情况下使用

  • 在当要将一个新的键值对添加到字典里面 或者 通过键查找值的时候
  • 出现哈希冲突的时候使用

那么在插入或者查找dictEntry 时候,我们就需要找到其所在下标的索引。(哈希表是个数组)具体算法如下:

1.通过宏 dictHashKey 计算得到该键对应的哈希值

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

2.将哈希值和哈希表的 sizemask 属性做位与(相当于取模运算),得到索引值 index,其中 ht[x] 可以是 ht[0] 或者 ht[1]。

index = dictHashKey(d, key) & d->ht[x].sizemask;

3.哈希冲突

redis字典实现是使用链地址法。

哈希冲突那肯定是在键值对插入的时候。看看键值对插入函数dictAdd。

int dictAdd(dict *d, void *key, void *val)
{
    dictEntry *entry = dictAddRaw(d,key,NULL);

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

#define dictSetVal(d, entry, _val_) do { \
    if ((d)->type->valDup) \
        (entry)->v.val = (d)->type->valDup((d)->privdata, _val_); \
    else \
        (entry)->v.val = (_val_); \
} while(0)

dictAdd会调用dictAddRaw生成一个新的哈希节点,然后调用dictSetVal去设置这个哈希节点的val值,那么剩下key还没有设置,设置key在dictAddRaw函数内。

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

    if (dictIsRehashing(d)) _dictRehashStep(d);//若符合,则执行rehash

    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)//1.获取索引
        return NULL;

    //2.根据是否rehash状态获取ht[1]还是获取ht[0],如果正在refash,则用哈希表ht[1]进行插入
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    //3.分配内存生成新的hash节点,然后在链表头部插入这个hash节点,
    entry = zmalloc(sizeof(*entry));
    entry->next = ht->table[index];
    ht->table[index] = entry;
    ht->used++;

    //4.设置键
    dictSetKey(d, entry, key);
    return entry;
}

rehash

随着字典操作的不断执行,哈希表保存的键值对会不断增多或减少。当哈希表保存的键值对数量太多或者太少时,需要对哈希表大小进行扩容或者收缩。

而不管是扩容还是收缩,必定是会创建新的哈希表这会导致哈希表的size和sizemask发生变化,而key的查询是于sizemask相关的。所以必须对哈希表中的每一个key进行重新计算索引,然后插入新的哈希表,这个过程就是rehash。

过程如下:

  1. 计算出新hash表的realsize
  2. 按照新的realsize申请内存空间,创建dictht,并赋值给dicth.ht[1]
  3. 设置dict.rehashidx=0,表示开始rehash

rehash主要的函数dictExpand

int dictExpand(dict *d, unsigned long size) {
    return _dictExpand(d, size, NULL);
}

int _dictExpand(dict *d, unsigned long size, int* malloc_failed)
{
    if (malloc_failed) *malloc_failed = 0;

    /* 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;
    
    //步骤1,计算出新的realsize
    dictht n; /* the new hash table */
    unsigned long realsize = _dictNextPower(size);

    /* Detect overflows */
    if (realsize < size || realsize * sizeof(dictEntry*) < realsize)
        return DICT_ERR;

    /* Rehashing to the same table size is not useful. */
    if (realsize == d->ht[0].size) return DICT_ERR;
    
    //步骤2,申请内存空间,创建dictht
    /* Allocate the new hash table and initialize all pointers to NULL */
    n.size = realsize;
    n.sizemask = realsize-1;
    if (malloc_failed) {
        n.table = ztrycalloc(realsize*sizeof(dictEntry*));
        *malloc_failed = n.table == NULL;
        if (*malloc_failed)
            return DICT_ERR;
    } else
        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. */
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }
    
    //步骤3,设置rehashidx=0,标识开始rehash
    /* Prepare a second hash table for incremental rehashing */
    d->ht[1] = n;
    d->rehashidx = 0;
    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;

    if (size >= LONG_MAX) return LONG_MAX + 1LU;
    while(1) {
        if (i >= size)
            return i;
        i *= 2;
    }
}

怎么判断要进行扩容还是收缩

  • 触发扩容的条件,负载因子>5

负载因子 = 保存的节点数(used)/ 哈希表大小(size)。

即是load_factor = ht[0].used / ht[0].size。

static int dict_can_resize = 1;
static unsigned int dict_force_resize_ratio = 5;

static int _dictExpandIfNeeded(dict *d)
{
    /* Incremental rehashing already in progress. Return. */
    if (dictIsRehashing(d)) return DICT_OK;
    
    // 大小为0需要设置初始哈希表大小为4
    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) &&
        dictTypeExpandAllowed(d))    // 负载因子超过5,执行 dictExpand
    {
        return dictExpand(d, d->ht[0].used + 1);
    }
    return DICT_OK;
}
  • 触发收缩的条件:哈希表的负载因子小于 0.1 时
//dict.c
int dictResize(dict *d)
{
    unsigned long 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);
}


//dictResize函数的使用场景
//server.h
#define HASHTABLE_MIN_FILL        10      /* Minimal hash table fill 10% */

//server.c
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));
}
void tryResizeHashTables(int dbid) {
    if (htNeedsResize(server.db[dbid].dict))
        dictResize(server.db[dbid].dict);
    ....................
}

扩展或收缩哈希表都是用到了函数dictExpand,而新的哈希表最终大小都是 2 的 n 次幂

什么时候会用到扩展和收缩

  • 扩展是在每次执行添加键值对或者修改键值对时候进行判断的,即是每次添加键值对的时候都会执行_dictExpandIfNeeded
  • 判断是否需要收缩是可以认为是使用定时器,定时去对比判断的

dictExpand只是扩展了哈希表,还没有把哈希表内的键值对保存到ht[1]中,这个操作在其他地方进行的。这就需要到渐进式rehash

渐进式rehash

扩展和收缩哈希表都需要将 ht[0] 里面的所有键值对 rehash 到 ht[1] 里面。但是,rehash 动作并不是一次性、集中式完成的而是分多次、渐进式地完成的。因为一次性将所有键值对全部 rehash 到 ht[1] 的话,庞大的计算量可能会导致服务器在一段时间内停止服务。

渐进式rehash主要的函数dictRehash

  1. 每次进行新增、查询、修改、删除时,都会检查下dict.rehashidx是否大于-1,如果是则将dict.ht[0].table[rehashidx]的entry链表赋值给dict.ht[1]中,并将rehashidx++。直至dict.ht[0]的所有数据都rehash到dict.ht[1]中。
  2. 将dict.ht[1]赋值给dict.ht[0],将dict.ht[1]初始化为空哈希表,并释放原来的dict.ht[0]内存。
  3. 将rehashidx赋值为-1,代表rehash结束。

注意:在rehash过程中,新增操作的话,是直接写入到ht[1]而查询、修改和删除则是会在dict.ht[0]和dict.ht[1]依次查找并执行。这样可以确保ht[0]的数据只减不增,随着rehash最终变为空。

渐进式rehash做的优化:为了防止 ht[0] 是个稀疏表 (遍历很久遇到的都是NULL),从而导致函数阻塞时间太长,rehash做了个优化,引入“最大空格访问数”,也即代码中的 enmty_visits,初始值为 n*10。当遇到NULL的数量超过这个初始值直接返回

//dictRehash被_dictRehashStep调用
static void _dictRehashStep(dict *d) {
    if (d->pauserehash == 0) dictRehash(d,1);
}
//每次执行增加、删除等操作时候,都会判断是否要执行_dictRehashStep

int dictRehash(dict *d, int n) {
    //最多有10*n个空桶
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;

    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);
        //是空桶的话,且超过10*n个空桶,就跳过
        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) {
            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++;
    }

    //判断是否已经把ht[0]的所有数据都rehash到ht[1],若是则赋值ht[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;
    }

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

渐进式 rehash 的好处在于它采取分而治之的方式,将 rehash 键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式 rehash 而带来的庞大计算量。

字典API

字典的创建dictCreate

 主要是分配空间,初始化一些数据。

static void _dictReset(dict *ht) {
    ht->table = NULL;
    ht->size = 0;
    ht->sizemask = 0;
    ht->used = 0;
}

/* Create a new hash table */
static dict *dictCreate(dictType *type, void *privDataPtr) {
    dict *ht = malloc(sizeof(*ht));
    _dictInit(ht,type,privDataPtr);
    return ht;
}

/* Initialize the hash table */
static int _dictInit(dict *ht, dictType *type, void *privDataPtr) {
    _dictReset(ht);
    ht->type = type;
    ht->privdata = privDataPtr;
    return DICT_OK;
}

增加键值对dictAdd

  1. 判断是否需要进行rehash的。
  2. 通过哈希函数找到要插入的key的槽位置(通过函数_dictKeyIndex)
  3. 判断要插入哪个哈希表,并插入,最终设置value。
/* Add an element to the target hash table */
int dictAdd(dict *d, void *key, void *val)
{
    dictEntry *entry = dictAddRaw(d,key,NULL);

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

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

    if (dictIsRehashing(d)) _dictRehashStep(d);//判断是否需要进行rehash

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

static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing)
{
    unsigned long idx, table;
    dictEntry *he;
    if (existing) *existing = NULL;

    /* Expand the hash table if needed */
    if (_dictExpandIfNeeded(d) == DICT_ERR)
        return -1;
    for (table = 0; table <= 1; table++) {
        idx = hash & d->ht[table].sizemask;
        /* Search if this slot does not already contain the given key */
        he = d->ht[table].table[idx];
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key)) {
                if (existing) *existing = he;
                return -1;
            }
            he = he->next;
        }
        if (!dictIsRehashing(d)) break;
    }
    return idx;
}

查找给定键的值dictFetchValue

利用 dictFind 找到给定键的 dictEntry,然后获得值,值的类型不确定,所以返回一个万能指针。

其主要是调用了dictFind。该操作内部有执行rehash工作,说明了渐进式rehash是分摊到查找、删除、更新等操作里面,从而避免了集中式 rehash 而带来的庞大计算量。

步骤:

  1. 计算指定key的哈希值
  2. 遍历hash表,根据hash值和sizemask计算出table的索引(是遍历Dict两个hash表,因为渐进式rehash中,不能确定该key是在哪个哈希表的)
  3. 取得hash节点链表信息并进行遍历
void *dictFetchValue(dict *d, const void *key) {
    dictEntry *he;

    he = dictFind(d,key);
    return he ? dictGetVal(he) : NULL;
}

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

    if (dictSize(d) == 0) return NULL; /* dict is empty */
    if (dictIsRehashing(d)) _dictRehashStep(d);    //符合就执行rehash
    h = dictHashKey(d, key);    //通过给定的哈希函数和去计算key的哈希值
    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;    //hash值和sizemask计算出table的索引
        he = d->ht[table].table[idx];        //所在的哈希表节点
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key))
                return he;
            he = he->next;
        }
        if (!dictIsRehashing(d)) return NULL;
    }
    return NULL;
}

 删除给定的键dictDelete

通过哈希算法找到对应的键,从对应链表移除

int dictDelete(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,0) ? DICT_OK : DICT_ERR;
}

static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
    uint64_t h, idx;
    dictEntry *he, *prevHe;
    int table;

    if (d->ht[0].used == 0 && d->ht[1].used == 0) return NULL;

    if (dictIsRehashing(d)) _dictRehashStep(d);
    h = dictHashKey(d, key);    //计算出key的哈希值

    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;    //通过哈希值和sizemask找到该索引
        he = d->ht[table].table[idx];       //找到该链表结点
        prevHe = NULL;
        //通过链表里寻找
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key)) {
                /* Unlink the element from the list */
                if (prevHe)
                    prevHe->next = he->next;
                else
                    d->ht[table].table[idx] = he->next;
                if (!nofree) {
                    dictFreeKey(d, he);
                    dictFreeVal(d, he);
                    zfree(he);
                }
                d->ht[table].used--;
                return he;
            }
            prevHe = he;
            he = he->next;
        }
        if (!dictIsRehashing(d)) break;
    }
    return NULL; /* not found */
}

添加或覆盖键值对(更新键值对)dictReplace

添加一个元素,如果键已经存在,则丢弃旧值。

该函数也主要是调用dictAddRaw。

如果键是从头开始添加的,则返回1;如果已经有一个元素具有这样的键,并且dictReplace()刚刚执行了值更新操作,则返回0

int dictReplace(dict *d, void *key, void *val)
{
    dictEntry *entry, *existing, auxentry;

    entry = dictAddRaw(d,key,&existing);
    if (entry) {
        dictSetVal(d, entry, val);
        return 1;
    }

    auxentry = *existing;
    dictSetVal(d, existing, val);
    dictFreeVal(d, &auxentry);
    return 0;
}

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

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

相关文章

shopee虾皮运营技巧,学会这几招能有效提高销量

在电商的运营过程中&#xff0c;新手总会踩上不少的坑&#xff0c;运气好的累计成经验值&#xff0c;应用于日后的店铺运营中&#xff0c;运气不好的&#xff0c;进坑了就再也爬不上来了&#xff0c;特别是跨境电商&#xff0c;本身与国内电商存在很大的信息差&#xff0c;不可…

【EI会议】2024年机械制造、航天工程技术与材料应用国际学术会议 (MMAETMA 2024)

2024 International Academic Conference on Mechanical Manufacturing, Aerospace Engineering Technology and Materials Application (MMAETMA 2024) 会议地点&#xff1a;长沙&#xff0c;中国 截稿时间&#xff1a;2024.4.30 网址&#xff1a;http://www.mmaetma.com/ …

Linux环境下安装redis踩坑zmalloc.h:50:31收录(附解决方法)

老将回归第一篇问题收录&#xff08;苦笑&#xff09; 报错收录 解决&#xff1a;zmalloc.h:50:31: 致命错误&#xff1a;jemalloc/jemalloc.h&#xff1a;没有文件或目录 解法一&#xff1a; # 执行如下命令 MALLOC环境变量去建立Redis的libcmake MALLOClibc解决 解法二&a…

FJSP:霸王龙优化算法(Tyrannosaurus optimization,TROA)求解柔性作业车间调度问题(FJSP),提供MATLAB代码

一、柔性作业车间调度问题 柔性作业车间调度问题&#xff08;Flexible Job Shop Scheduling Problem&#xff0c;FJSP&#xff09;&#xff0c;是一种经典的组合优化问题。在FJSP问题中&#xff0c;有多个作业需要在多个机器上进行加工&#xff0c;每个作业由一系列工序组成&a…

企业IT运维事中故障定位方法及工具

企业IT故障定位指诊断故障直接原因或根因&#xff0c;故障定位有助于故障恢复动作更加有效。故障定位通常是整个故障过程中耗时最长的环节&#xff0c;定位的目标围绕在快速恢复的基础上&#xff0c;而非寻找问题根因&#xff0c;后者由问题管理负责。通常大部分可用性故障&…

电脑桌面监控软件

电脑桌面监控软件 有一项触目精心的统计&#xff0c;2022年上半年&#xff0c;有33.2%的网民在单位上网&#xff0c;上网行为包括聊天、游戏、看小说、炒股等等&#xff0c;可以说五花八门。这些行为给企业带来四个问题&#xff1a; &#xff08;1&#xff09;宽带资源被滥用。…

Myelsa的Python函数之旅(高铁直达)

一、函数的定义&#xff1a; 函数(Function)是一段可重复使用的代码块&#xff0c;用于执行特定的任务或计算&#xff0c;并可以接受输入参数和返回输出结果。函数可以将复杂的问题分解为更小的子问题&#xff0c;提高代码的可读性和可维护性。 二、函数的组成&#xff1a; 在…

面试:HashMap

目录 1、底层数据结构&#xff0c;1.7 与1.8有何不同? 2、为何要用红黑树&#xff0c;为何一上来不树化&#xff0c;树化阈值为何是8&#xff0c;何时会树化&#xff0c;何时会退化为链表? 3、索引如何计算? hashCode都有了&#xff0c;为何还要提供hash()方法?数组容量为…

java实现运行脚本文件

在最近的项目中&#xff0c;有一个需求是前端传给我一个脚本文件&#xff0c;然后我需要运行脚本文件后将结果进行返回&#xff0c;那接下来就让我们看看是怎么做的吧&#xff01; public R runScripts(Integer id) {ScriptsInfo scriptsInfo this.baseMapper.selectById(id);…

【数据结构】红黑树详解

目录 前言&#xff1a; 红黑树的概念&#xff1a; 红黑树的性质: 红黑树节点的定义&#xff1a; 红黑树的插入&#xff1a; 情况1&#xff1a;cur为红&#xff0c;p为红&#xff0c;g为黑&#xff0c;u存在且为红 情况2&#xff1a;cur为红&#xff0c;p为红&#xff0c…

CY5.5-NH2生物分子荧光标记Cy5.5 星戈瑞

CY5.5-NH2是一种常用的生物分子荧光标记染料&#xff0c;属于Cy5.5系列染料的一种。它具有强烈的荧光信号和较高的光稳定性&#xff0c;因此在生物分子标记和成像领域得到应用。 CY5.5-NH2染料具有一个氨基(-NH2)官能团&#xff0c;可以与生物分子中的羧基(-COOH)或其他活性基…

linux网络预备

网络预备 网络协议初识 协议分层 打电话例子 在这个例子中, 我们的协议只有两层; 但是实际的网络通信会更加复杂, 需要分更多的层次。 分层最大的好处在于 “封装” 。 OSI七层模型 OSI&#xff08;Open System Interconnection&#xff0c;开放系统互连&#xff09;七层网…

用Python做一个4399游戏脚本原来这么简单 !(内含完整思路)

说明 简述&#xff1a;本文将以4399小游戏《宠物连连看经典版2》作为测试案例&#xff0c;通过识别小图标&#xff0c;模拟鼠标点击&#xff0c;快速完成配对。对于有兴趣学习游戏脚本的同学有一定的帮助。 运行环境&#xff1a;Win10/Python3.5。 主要模块&#xff1a;win3…

汇编语言:寻址方式在结构化数据访问中的应用——计算人均收入

有一年多没有在CSDN上发博文了。人的工作重心总是有转移的&#xff0c;庆幸一直在做着有意义的事。   今天的内容&#xff0c;是为汇编语言课程更新一个实验项目。      本方案修改自王爽编《汇编语言》第&#xff14;版P172“实验7寻址方式在结构化数据访问中的应用” …

nodejs应用程序以守护进程daemon的方式启动,容器化部署的时候一直部署出错,导致无法成功启动程序。

一、背景 nodejs应用程序使用Egg.js 框架脚本命令&#xff0c;见package.json&#xff1a; "scripts": {"debug": "egg-bin debug","clean": "easy clean","build": "easy build prod","start&…

数字逻辑分析仪初体验

为啥会用到这玩意儿&#xff0c;要从一个荒诞的需求开始。想在市面上找一款特别低空飞行的监控&#xff0c;而且不想它一直开着监控&#xff0c;最好是我在外面远程指挥它起飞&#xff0c;飞去厨房&#xff0c;飞去洗手间&#xff0c;甚至飞去阳台&#xff0c;查看水龙头情况啊…

Redis性能瓶颈与安全隐患排查验证纪实

在写《Redis怎样保证数据安全&#xff1f;》这篇文章&#xff0c;我是有对redis设置密码需要哪些步骤&#xff0c;设置密码的性能损耗有验证的。这就涉及到要对redis的配置做修改。 开始时我是打算采用直接使用redis配置文件的方式。所以我从redis官网下载了一个默认的配置文件…

C++搭建深度学习的推理框架

我们的目的是:借助C++搭建一个类似于pytorch,tensorflow的深度学习框架,对标pytorch,tensorflow实现对应的功能。由于本人能力有限,下面本人将借助C++搭建一个简单的全连接神经网络,并且尝试解释里面的算子定义和计算图构建。 算子定义 回顾pytorch里面搭建的全连接神经网…

where 函数

Pandas 中的 where 函数 在 Pandas 中&#xff0c;where 函数用于替换不满足条件的值。具体来说&#xff0c;它返回一个与原始 DataFrame 或 Series 形状相同的新对象&#xff0c;但所有不满足条件的值都被替换为指定的值&#xff08;默认为 NaN&#xff09;。 对于 DataFram…

数据结构——二叉树链式结构的实现

大家好我是小锋&#xff0c;今天我们来学习的是二叉树链式结构的实现 首先我们来学习一下二叉树的基本操作 在看二叉树基本操作前我们来回顾下二叉树的概念&#xff0c; 二叉树是&#xff1a; 1. 空树 2. 非空&#xff1a;根节点&#xff0c;根节点的左子树、根节点的右…