【Redis 源码】3dict字典数据结构

1 数据结构说明

dictionary数据结构,也称为哈希表(hash table)。用于存储字典。哈希是一个键值对的集合,每个键都是唯一的并与一个值相关联。字典的设计旨在提供高效的查找、插入和删除操作。

2 核心数据结构

hash 的数据结构定义在头文件 dict.h中,核心数据结构为:

typedef struct dict {dictType *type;      /* 字典类型 */void *privdata;      /* 私有数据 */dictht ht[2];        /* 两个散列表 */long rehashidx;      /* 重新散列索引,如果不是正在进行重新散列,则为 -1 */int16_t pauserehash; /* 如果大于 0,则暂停重新散列,小于 0 表示编码错误.比如在dictScan时候就给 pauserehash值++,表示正在扫描。停止刷新*/
} 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;

在这里插入图片描述

  • dictt的ht属性是一个包含两个项的数组,数组中的每个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。
  • dictht 哈希表,table属性是一个数组,数组中的每个元素都是一个指向dictEntry结构的指针,每个dictEntry结构保存着一个键值对。
  • dictEntry 字典节点,dict解决冲突的办法是采用链表的链式解决方案,所以发生冲突的节点通过next指针链接在一起。哈希节点的键是void*类型的,可以表示任意类型的键。

其他结构说明

  • dictType
typedef struct dictType {//计算hash方法uint64_t (*hashFunction)(const void *key);//复制key方法void *(*keyDup)(void *privdata, const void *key);//复制value方法void *(*valDup)(void *privdata, const void *obj);// 判断两个key相等的方法int (*keyCompare)(void *privdata, const void *key1, const void *key2);//销毁key方法void (*keyDestructor)(void *privdata, void *key);//销毁value方法void (*valDestructor)(void *privdata, void *obj);
} dictType;

3 核心代码

dictAdd 添加

int dictAdd(dict *d, void *key, void *val)
{// 添加key,并创建一个新的dictEntry节点dictEntry *entry = dictAddRaw(d,key,NULL);//如果返回的entry为空说明添加失败if (!entry) return DICT_ERR//为当前节点设置我们需要的值dictSetVal(d, entry, val);//返回设置成功return DICT_OK;
}

在上面代码中参数有3个:dict数据结构、我们需要保存的key和value。

其中主要分为两个步骤:

  1. 根据我们输入的key在dict结构中插入节点,并将新创建的节点dictEntry返回。
  2. 将我们需要保存的value设置到新创建的dictEntry节点中。

dictSetVal 是一个宏定义,根据dictEntry.type的valDup函数来确定,当前值是否复制一个新的值还是直接进行引用

#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)
  • dictAddRaw 根据key在dict中添加新的dictEntry节点并返回,如果key存在,则返回-1.并将历史dictEntry赋值给existing
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{long index;dictEntry *entry;dictht *ht;//当前字典是否正在扩容,当标志位dict.rehashidx不为-1时,说明在扩容//当节点正在扩容,则调用dictRehash 进行刷新if (dictIsRehashing(d)) _dictRehashStep(d);/* 查找当前key,在table 的索引下标。并且检查当前key是否已经在dict中存在,如果存在则返回-1*/if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)return NULL;/*根据是否正在刷新,如果在刷新,则返回 ht1 ,否则ht0 */ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];//分配新的entry空间,并使用头插法,将插入链表中entry = zmalloc(sizeof(*entry));entry->next = ht->table[index];ht->table[index] = entry;ht->used++;/* 设置字典表的key逻辑类似 dictSetVal */dictSetKey(d, entry, key);return entry;
}
  • _dictKeyIndex
/**
dict *d:指向 dict 结构体的指针,即字典实例。
const void *key:指向键的指针。
uint64_t hash:键的散列值。
dictEntry **existing:如果键已经存在于字典中,则通过这个指针返回对应的 dictEntry。
*/
static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing)
{unsigned long idx, table;dictEntry *he;if (existing) *existing = NULL;/* 如果需要扩展散列表,调用 _dictExpandIfNeeded 函数进行扩展。如果扩展失败,则返回 -1。 */if (_dictExpandIfNeeded(d) == DICT_ERR)return -1;/*循环遍历两个散列表(如果正在进行重新散列),直到找到合适的索引位置或确认键不存在。*/for (table = 0; table <= 1; table++) {/*使用散列值和 sizemask 计算键在散列表中的索引位置。*/idx = hash & d->ht[table].sizemask;/*遍历指定索引位置上的链表,检查是否存在相同的键。 */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;}/*如果没有在刷新,就只需要查找ht0即可*/if (!dictIsRehashing(d)) break;}return idx;
}
  • _dictExpandIfNeeded 用于判断是否需要扩展散列表
static int _dictExpandIfNeeded(dict *d)
{/* 如果字典正在进行重新散列(dictIsRehashing 返回真 */if (dictIsRehashing(d)) return DICT_OK;/* 如果散列表为空(即大小为零),则将其扩展到初始大小 DICT_HT_INITIAL_SIZE。 */if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);/* 1、如果散列表的使用率达到了 100% used >= size2、允许自动扩展(dict_can_resize 为真)3、即使不允许自动扩展,但负载因子超过了设定的安全阈值*/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)){return dictExpand(d, d->ht[0].used + 1);}return DICT_OK;
}

dictExpand 扩容

/*
dict *d:指向 dict 结构体的指针,即字典实例。
unsigned long size:目标散列表的大小。
int* malloc_failed:一个指向整型变量的指针,用于指示内存分配是否失败。
*/
int _dictExpand(dict *d, unsigned long size, int* malloc_failed)
{if (malloc_failed) *malloc_failed = 0;/* 检查目标大小是否有效。如果正在进行重新散列,或者目标大小小于当前散列表中已使用的条目数,则返回 DICT_ERR */if (dictIsRehashing(d) || d->ht[0].used > size)return DICT_ERR;dictht n; /* the new hash table *///根据size,计算大于或等于给定大小的最小的 2 的幂次方unsigned long realsize = _dictNextPower(size);/* 如果实际目标大小与当前散列表大小相同,则不需要扩展,返回 DICT_ERR。 */if (realsize == d->ht[0].size) return DICT_ERR;/* 创建新的数组,并对dictht进行初始化设置 */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;} elsen.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;}/* 将新创建的 dictht 赋值给ht1*/d->ht[1] = n;d->rehashidx = 0;return DICT_OK;
}

dictReplace 跟新数据

dictReplace 函数的作用是在 Redis 的字典中替换一个键对应的值。如果键已经存在,则更新其值;如果键不存在,则新增一个键值对。这个函数结合了添加新条目和更新现有条目的功能,使得字典能够在键存在时替换值,而在键不存在时添加新条目。

int dictReplace(dict *d, void *key, void *val)
{dictEntry *entry, *existing, auxentry;/* Try to add the element. If the key* does not exists dictAdd will succeed. */entry = dictAddRaw(d,key,&existing);if (entry) {dictSetVal(d, entry, val);return 1;}/* Set the new value and free the old one. Note that it is important* to do that in this order, as the value may just be exactly the same* as the previous one. In this context, think to reference counting,* you want to increment (set), and then decrement (free), and not the* reverse. */auxentry = *existing;dictSetVal(d, existing, val);dictFreeVal(d, &auxentry);return 0;
}

dictGenericDelete 删除数据

/*
dict *d:指向 dict 结构体的指针,即字典实例。
const void *key:指向要删除的键的指针。
int nofree:如果为非零值,则不释放键和值的内存。返回值:dictEntry *:指向被删除的 dictEntry,如果未找到对应的键,则返回 NULL。
*/
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {uint64_t h, idx;dictEntry *he, *prevHe;int table;//如果两个散列表都没有使用任何条目,则直接返回 NULLif (d->ht[0].used == 0 && d->ht[1].used == 0) return NULL;//如果正在进行重新散列,则调用 _dictRehashStep 函数进行一次重新散列步骤。if (dictIsRehashing(d)) _dictRehashStep(d);//计算给定键的散列值,并根据散列表的大小掩码计算索引。h = dictHashKey(d, key);//遍历指定索引位置上的链表,查找与给定键匹配的 dictEntry。//如果找到匹配的键,则将其从链表中删除,并根据 nofree 参数决定是否释放内存。for (table = 0; table <= 1; table++) {idx = h & d->ht[table].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;elsed->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 */
}

dictScan 字典扫描

dictScan 函数是 Redis 字典实现中的一个重要的遍历工具,用于扫描字典中的条目。这个函数允许用户在不锁定整个字典的情况下遍历其中的条目,这对于在高并发环境下安全地读取数据非常有用。dictScan 支持增量扫描,即可以从上次停止的地方继续扫描,这有助于减少内存消耗和提高效率。

/**
dict *d:指向 dict 结构体的指针,即字典实例。
unsigned long v:扫描的游标,表示从哪个位置开始扫描。
dictScanFunction *fn:回调函数指针,用于处理每一个遍历到的 dictEntry。
dictScanBucketFunction* bucketfn:可选的回调函数指针,用于处理每一个桶(bucket)。
void *privdata:传递给回调函数的私有数据指针。返回值:unsigned long:新的游标值,表示下次扫描应从哪个位置开始。
**/unsigned long dictScan(dict *d,unsigned long v,dictScanFunction *fn,dictScanBucketFunction* bucketfn,void *privdata)
{dictht *t0, *t1;const dictEntry *de, *next;unsigned long m0, m1;//如果字典没有条目,则直接返回 0。if (dictSize(d) == 0) return 0;/* 在扫描过程中暂停重新散列。 */dictPauseRehashing(d);/*如果没有正在进行的重新散列,只遍历第一个散列表*/if (!dictIsRehashing(d)) {t0 = &(d->ht[0]);m0 = t0->sizemask;/* Emit entries at cursor */if (bucketfn) bucketfn(privdata, &t0->table[v & m0]);de = t0->table[v & m0];while (de) {next = de->next;/*调用数据处理函数*/fn(privdata, de);de = next;}/* Set unmasked bits so incrementing the reversed cursor* operates on the masked bits */v |= ~m0;/* Increment the reverse cursor */v = rev(v);v++;v = rev(v);} else {t0 = &d->ht[0];t1 = &d->ht[1];/* Make sure t0 is the smaller and t1 is the bigger table */if (t0->size > t1->size) {t0 = &d->ht[1];t1 = &d->ht[0];}m0 = t0->sizemask;m1 = t1->sizemask;/* Emit entries at cursor */if (bucketfn) bucketfn(privdata, &t0->table[v & m0]);de = t0->table[v & m0];while (de) {next = de->next;fn(privdata, de);de = next;}/* Iterate over indices in larger table that are the expansion* of the index pointed to by the cursor in the smaller table */do {/* Emit entries at cursor */if (bucketfn) bucketfn(privdata, &t1->table[v & m1]);de = t1->table[v & m1];while (de) {next = de->next;fn(privdata, de);de = next;}/* Increment the reverse cursor not covered by the smaller mask.*/v |= ~m1;v = rev(v);v++;v = rev(v);/* Continue while bits covered by mask difference is non-zero */} while (v & (m0 ^ m1));}/*扫描完成后恢复重新散列。*/dictResumeRehashing(d);return v;
}

dictIterator 字典迭代器

  • 数据结构
typedef struct dictIterator {dict *d;              // 指向字典的指针long index;           // 当前迭代的索引位置int table, safe;      // 当前迭代的散列表编号和是否安全标志dictEntry *entry, *nextEntry; // 当前迭代的条目和下一个条目/* unsafe iterator fingerprint for misuse detection. */long long fingerprint; // 不安全迭代器的指纹,用于检测误用
} dictIterator;
  1. dict *d
    • 指向当前正在迭代的字典的指针。
  2. long index
    • 当前迭代器所在的索引位置。这个索引用于在散列表中定位当前的桶(bucket)。
  3. int table
    • 当前迭代的散列表编号。在字典进行重新散列时,字典会有两个散列表,table 变量指示当前迭代的是哪一个散列表。
  4. int safe
    • 安全标志,表示迭代器是否处于安全状态。当字典正在进行修改(如添加或删除条目)时,这个标志可以帮助迭代器决定是否继续迭代。
  5. dictEntry *entry, *nextEntry
    • 当前迭代的条目指针和下一个条目的指针。entry 指向当前正在处理的条目,而 nextEntry 指向下一个要处理的条目。
  6. long long fingerprint
    • 不安全迭代器的指纹,用于检测迭代器的误用。这个指纹在迭代器创建时初始化,并在字典发生更改时更新。如果迭代器检测到字典的指纹与自身的指纹不匹配,则认为字典已经发生了改变,迭代器可能不再安全。
1 获取迭代器
dictIterator *dictGetIterator(dict *d)
{dictIterator *iter = zmalloc(sizeof(*iter));iter->d = d;iter->table = 0;iter->index = -1;iter->safe = 0;iter->entry = NULL;iter->nextEntry = NULL;return iter;
}dictIterator *dictGetSafeIterator(dict *d) {dictIterator *i = dictGetIterator(d);i->safe = 1;return i;
}
2 迭代数据
/*
dictEntry *:返回下一个条目的指针,如果没有更多的条目则返回 NULL
*/
dictEntry *dictNext(dictIterator *iter)
{while (1) {/*如果 iter->entry 为空,需要找到下一个非空的条目。*/if (iter->entry == NULL) {/*根据iter的table 找到正在遍历ht的那个散列表*/dictht *ht = &iter->d->ht[iter->table];/*如果是第一次遍历*/if (iter->index == -1 && iter->table == 0) {//如果是安全遍历,则加计数器。否则计算当前迭代器的hashif (iter->safe)dictPauseRehashing(iter->d);elseiter->fingerprint = dictFingerprint(iter->d);}/*索引++*/iter->index++;/*当前的table遍历完了,如果是正在刷新,并且正在遍历的是ht0,则遍历ht1*/if (iter->index >= (long) ht->size) {if (dictIsRehashing(iter->d) && iter->table == 0) {iter->table++;iter->index = 0;ht = &iter->d->ht[1];} else {break;}}iter->entry = ht->table[iter->index];} else {/*如果 iter->entry 不为空,则将其作为返回值,并保存下一个条目的指针*/iter->entry = iter->nextEntry;}if (iter->entry) {/* We need to save the 'next' here, the iterator user* may delete the entry we are returning. */iter->nextEntry = iter->entry->next;return iter->entry;}}return NULL;
}

dictFind 字典查找

dictFind 函数用于在 Redis 字典中查找指定键对应的条目。这个函数会根据给定的键,利用哈希算法计算出一个哈希值,并在字典中寻找与该键匹配的条目。

/*
dict *d:指向字典的指针。
const void *key:指向要查找的键的指针dictEntry *:返回指向找到的条目的指针,如果找不到,则返回 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);h = dictHashKey(d, key);for (table = 0; table <= 1; table++) {idx = h & d->ht[table].sizemask;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;
}

dictRehash

函数用于重新散列 Redis 字典。重新散列是指将字典中的条目从一个散列表移动到另一个散列表的过程。这个过程通常发生在字典的增长或收缩过程中,以保持散列表的有效负载因子在合理的范围内,从而维持良好的性能。

/**
dict *d:指向字典的指针。
int n:指定本次重新散列的步数,即处理的桶(buckets)数量。**/
int dictRehash(dict *d, int n) {int empty_visits = n*10; /* Max number of empty buckets to visit. *//** 如果字典当前不在重新散列状态,则直接返回 0 **/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);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++;}/* 检查是否完成重新散列 */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;
}

4 总结

- Redis 字典为什么使用双散列表?

Redis 使用双散列表机制来支持自动扩容。当散列表的负载因子(已使用的槽位数除以总的槽位数)超过一定阈值时,Redis 会创建一个新的更大的散列表,并逐步将旧散列表中的条目迁移到新的散列表中。这样做有两个好处:首先,它可以避免在高负载下一次性重新散列整个表所带来的性能影响;其次,它确保了散列表的负载因子保持在一个合理的水平,从而保持了良好的性能。

- Redis 如何实现渐进式的重新散列?

Redis 的重新散列是一个渐进式的过程,通过 dictRehash 函数来实现。每次调用 dictRehash 函数时,它只会处理一部分条目,而不是一次性处理所有的条目。这样可以将重新散列的压力分散到多次操作中,避免对系统性能造成大的冲击。

- Redis 中的迭代器是如何设计的?

Redis 的迭代器设计考虑到了安全性,特别是在遍历过程中可能发生的字典结构变化。例如,dictNext 函数会保存当前条目的下一个条目的指针,这样即使当前条目在遍历过程中被删除,迭代器仍然可以正确地继续遍历。此外,迭代器还提供了安全模式选项,可以在遍历时暂停重新散列过程,以确保遍历的一致性。

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

腾讯云SDK基本概念

本文旨在介绍您在使用音视频终端 SDK&#xff08;腾讯云视立方&#xff09;产品过程中可能会涉及到的基本概念。 音视频终端 SDK&#xff08;腾讯云视立方&#xff09; 应用 音视频终端 SDK&#xff08;腾讯云视立方&#xff09;通过应用的形式来管理您的项目&#xff08;Ap…

LabVIEW提高开发效率技巧----合理管理程序架构

在LabVIEW开发中&#xff0c;合理管理程序架构是保持项目可维护性和扩展性的关键。随着项目复杂度的增加&#xff0c;良好的架构设计可以避免代码混乱&#xff0c;并且便于后期的修改和扩展。以下是两种常见且有效的架构管理方式&#xff1a; 1. 面向对象编程&#xff08;OOP&a…

初识Tomcat

Tomcat是一款可以运行javaWebAPP的服务器软件。 一个服务器想要执行java代码&#xff0c;则需要JRE&#xff08;jvm、java运行环境等&#xff09;&#xff0c;但是需要执行javaWEB项目则还需要服务器软件&#xff0c;Tomacat就是其中很流行的一款。因为一个javaWEB项目会有很多…

USB2.0主机设备检测过程以及信号分析

一&#xff0c;USB协议发展 USB接口自1994年推出以来&#xff0c;经过30年的发展&#xff0c;从USB1.0发展到了现在的USB4.0&#xff0c;传输速率也从最开始的1.5Mbps&#xff0c;大幅提高到了最新的40Gbps。 USB协议按照速度等级和连接方式分可分为7个版本&#xff0c;但是从…

JAVAEE如何实现网页(jsp)间的数据传输?一文总结

刚刚接触到JAVAEE的WEB开发&#xff0c;解释不周的地方希望感谢指正&#xff01;&#xff01;&#xff01; 情景如下&#xff1a; 我的使用是21版的IDEA&#xff0c;9.03版本的tomcat&#xff0c;来做一个示范。 构建项目 点击下一步 -> 完成&#xff0c;等待项目构建结束…

Trie树之字符串统计问题

这是C算法基础-数据结构专栏的第二十七篇文章&#xff0c;专栏详情请见此处。 引入 Trie树&#xff0c;即字典树&#xff0c;顾名思义&#xff0c;就是用类似字典的方式存储数据&#xff0c;而Trie树最经典也是最简单的一个应用就是字符串统计问题。 字符串统计问题就是维护一个…

华为玄玑感知系统震撼发布:智能穿戴新品引领情绪健康新纪元

在科技日新月异的今天&#xff0c;华为再次以其卓越的创新能力&#xff0c;为智能穿戴领域带来了一场革命性的变革。 8月28日&#xff0c;华为玄玑感知系统暨穿戴创新技术发布会圆满落幕&#xff0c;会上正式揭晓了这款名为“玄玑”的神秘感知系统&#xff0c;预示着穿戴设备将…

element 中 v-loading 更改icon颜色、字体颜色

文章目录 问题分析 问题 如下图&#xff0c;由于背景的原因&#xff0c;可以看出展示的文字不是很清楚&#xff0c;因此需要自定义一下文字字体大小和文字颜色。像图二一样 分析 找到对应的class。然后直接修改即可 话不多说&#xff0c;直接上代码 ::v-deep {.el-loading…

Linux开源网络:高性能数据平面

数据平面的性能在很大程度上取决于网络 I/O 的性能&#xff0c;而网络数据包从网卡到用户空间的应用程序需要经历多个阶段&#xff0c;本文从数据平面基础到NFV&#xff0c;NFC基础设施再到OVS-DPDK VPP进行概论上的描述。 部分内容来源于《Linux开源网络全栈详解&#xff1a;从…

008——树

目录 树&#xff1a; 相关概念&#xff1a; 1.结点&#xff1a; 结点和结点之间的关系 2.度 3.n叉树 4.高度/深度 5.有序树和无序树 6.空树&#xff1a; 树的存储结构/表示方法&#xff1a; 树中都需要存储什么&#xff1f; 1.双亲表示法 2.孩子表示法 可以将上面…

MySQL之基础篇

数据库操作 1.查看当前的数据库版本 select version(); 2.显示所有数据库 show databases; 3.创建数据库 create [if not exists] database 数据库名 character set 字符编码集 collate 排序规则&#xff1b; 我们这里提前说一下 被方括号括起来的代码 表示可写可不写 示例…

Linux 安装redis主从模式+哨兵模式3台节点

下载 https://download.redis.io/releases/ 解压 tar -zxvf redis-7.2.4.tar.gz -C /opt chmod 777 -R /opt/redis-7.2.4/安装 # 编译 make # 安装&#xff0c; 一定是大写PREFIX make PREFIX/opt/redis-7.2.4/redis/ install配置为系统服务 cd /etc/systemd/system/主服务…

spring揭秘24-springmvc02-5个重要组件

文章目录 【README】【1】HanderMapping-处理器映射容器【1.1】HanderMapping实现类【1.1.1】SimpleUrlHandlerMapping 【2】Controller&#xff08;二级控制器&#xff09;【2.1】AbstractController抽象控制器&#xff08;控制器基类&#xff09; 【3】ModelAndView(模型与视…

从零开始搭建UVM平台(三)-加入objection机制

书接上回&#xff1a; 从零开始搭建UVM平台&#xff08;一&#xff09;-只有uvm_driver的验证平台 从零开始搭建UVM平台&#xff08;二&#xff09;-加入factory机制 加入objection机制 需要在第一个消耗仿真时间语句前raise_objection&#xff0c;最后再drop_objection&…

【微服务即时通讯系统】——etcd一致性键值存储系统、etcd的介绍、etcd的安装、etcd使用和功能测试

文章目录 etcd1. etcd的介绍1.1 etcd的概念 2. etcd的安装2.1 安装etcd2.2 安装etcd客户端C/C开发库 3. etcd使用3.1 etcd接口介绍 4. etcd使用测试4.1 原生接口使用测试4.2 封装etcd使用测试 etcd 1. etcd的介绍 1.1 etcd的概念 Etcd 是一个基于GO实现的 分布式、高可用、一致…

2024年7月大众点评武汉餐饮美食店铺基础信息

在做一些城市分析、学术研究分析、商业选址、商业布局分析等数据分析挖掘时&#xff0c;大众点评的数据参考价值非常大&#xff0c;截至2024年7月&#xff0c;大众点评美食店铺剔除了暂停营业、停止营业后的最新数据情况分析如下。 武汉餐饮美食店铺约9.6万家&#xff0c;有均…

实验1.2 熟悉VRP基本操作

实验1.2 熟悉VRP基本操作 原理概述 VRP&#xff08;Versatile Routing Platform&#xff0c;通用路由平台&#xff09;是华为公司数据通信产品的通用网络操作系统平台&#xff0c;拥有一致的网络界面、用户界面和管理界面。在VRP操作系统中&#xff0c;用户通过命令行对设备下…

Kettle9连接mysql8.0.36失败处理

一、问题描述 kettle作为数据转换同步的工具&#xff0c;使用java开发&#xff0c;连接数据库使用jar的驱动包&#xff0c;比如oracle连接使用ojdbc8.jar&#xff0c;mysql连接使用mysql-connect-java-8.0.*,但是截止目前mysql8.0.33到8.0.36在官网是没有mysql驱动包的&#x…

Vue之axios请求

Vue之axios请求 axios请求, 是Vue前端框架非常重要的一部分, 今天我们就讲解axios请求, 到底有什么作用, 以及会告诉大家axios的常见用法。 axios请求, 是网页向后端发起请求, 后端吧数据给我们网页, 这是一个前后端交互的过程。当我们学会了axios, 我们可以实现前端和后端练…

PIKACHU | PIKACHU 靶场初识

关注这个靶场的其他相关笔记&#xff1a;PIKACHU —— 靶场笔记合集-CSDN博客 0x01&#xff1a;PIKACHU 靶场简介 PIKACHU 是一款开源的练习 Web 漏洞的综合靶场&#xff0c;使用 PHP 代码编写而成&#xff0c;它包含了多种常见的 Web 安全漏洞&#xff0c;适合不同水平的用户…