目录

一、SDS

二、IntSet(整数集合)

三、双向链表

四、压缩列表

五、字典(哈希表)

七、跳表

八、QuickList

九、RedisObject


一、SDS

Redis 是用 C语言实现的,但是它没有直接使用C 语言的 char* 字符数组来实现字符串,而是自己封装了-个名为简单动态字符串(simple dynamic string,SDs) 的数据结构来表示字符串,也就是 Redis 的String 数据类型的底层数据结构是 SDS。
 

C 语言字符串的缺陷

  • 获取字符串长度的时间复杂度为O(N)
  • 非二进制安全(不能存二进制数据)
  • 不可修改
  • 字符串操作函数不高效且不安全,比如有缓冲区溢出的风险,有可能会造成程序运行终止
     

SDS 结构

 Redis 5.0 的 SDS 的数据结构:

  • len:记录了字符串长度。这样获取字符串长度的时候,只需要返回这个成员变量值就行,时间复杂度只需要O(1)。
  • alloc:分配给字符数组的空间长度。这样在修改字符串的时候,可以通过alloc - len计算出剩余的空间大小,可以用来判断空间是否满足修改需求,如果不满足的话,就会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用SDS既不需要手动修改SDS的空间大小,也不会出现前面所说的缓冲区溢出的问题。
  • flags:用来表示不同类型的SDS。一共设计了5种类型,分别是sdshdr5sdshdr8sdshdr16sdshdr32sdshdr64
  • buf[]:字节数组,用来保存实际数据。不仅可以保存字符串,也可以保存二进制数据。 总的来说,Redis的SDS结构在原本字符数组之上,增加了三个元数据:lenallocflags,用来解决C语言字符串的缺陷。

SDS的扩容原理:

  • 如果所需的sds长度小于1MB,那么最后的扩容是按照翻倍扩容来执行的,即2倍的newlen。
  • 如果所需的sds长度超过1MB,那么最后的扩容长度应该是newlen + 1MB。

在扩容SDS空间之前,SDS API会优先检查未使用空间是否足够,如果不够的话,API不仅会为SDS分配修改所必须要的空间,还会给SDS分配额外的「未使用空间」。

这样的好处是,下次在操作SDS时,如果SDS空间够的话,API就会直接使用「未使用空间」,而无须执行内存分配,有效的减少内存分配次数。

所以,使用SDS即不需要手动修改SDS的空间大小,也不会出现缓冲区溢出的问题。

二、IntSet(整数集合)

IntSet是Redis中set集合的一种实现方式,基于整数数组来实现,并且具备长度可变、有序等特征。 结构如下:

typedef struct intset {uint32_t encoding;  // 编码方式,决定每个元素的字节大小uint32_t length;    // 集合包含的元素数量int8_t contents[];  // 保存元素的柔性数组
} intset;

其中的encoding包含三种模式,表示存储的整数大小不同:

为了方便查找,Redis会将intset中所有的整数按照升序依次保存在contents数组中,结构如图:

现在,数组中每个数字都在int16_t的范围内,因此采用的编码方式是INTSET_ENC_INT16,每部分占用的字节大小为:

  • encoding:4字节
  • length:4字节
  • contents:2字节 * 3 = 6字节

我们向该其中添加一个数字:50000,这个数字超出了int16_t的范围,intset会自动升级编码方式到合适的大小。

以当前案例来说流程如下:

  • 升级编码为INTSET_ENC_INT32, 每个整数占4字节,并按照新的编码方式及元素个数扩容数组

  • 倒序依次将数组中的元素拷贝到扩容后的正确位置

  • 将待添加的元素放入数组末尾

  • 最后,将inset的encoding属性改为INTSET_ENC_INT32,将length属性改为4

Intset可以看做是特殊的整数数组,具备一些特点:

  • Redis会确保Intset中的元素唯一、有序

  • 具备类型升级机制,可以节省内存空间

  • 底层采用二分查找方式来查询

  • 不支持降级操作

三、双向链表

Redis的链表实现优点如下:

  • listNode链表节点的结构里带有prev和next指针,获取某个节点的前置节点或后置节点的时间复杂度只需O(1),而且这两个指针都可以指向 NULL,所以链表是无环链表;
  • list结构因为提供了表头指针 head 和表尾节点tail,所以获取链表的表头节点和表尾节点的时间复杂度只需O(1);
  • list结构因为提供了链表节点数量len,所以获取链表中的节点数量的时间复杂度只需O(1);
  • listNode链表节使用void* 指针保存节点值,并且可以通过 list结构的 dup、free、match函数指针为节点设置该节点类型特定的函数,因此链表节点可以保存各种不同类型的值; 

链表的缺点:

  • 链表每个节点之间的内存都是不连续的,意味着无法很好利用CPU缓存。能很好利用CPU缓存的数据结构就是数组,因为数组的内存是连续的,这样就可以充分利用 CPU缓存来加速访问。
  • 还有一点,保存一个链表节点的值都需要一个链表节点结构头的分配,内存开销较大。

因此, Redis 3.0的List对象在数据量比较少的情况下,会采用「压缩列表」作为底层数据结构的实现,它的优势是节省内存空间,并且是内存紧凑型的数据结构。

不过,压缩列表存在性能问题(具体什么问题,下面会说),所以 Redis在3.2版本设计了新的数据结构quicklist,并将List对象的底层数据结构改由 quicklist 实现。

然后在 Redis 5.0 设计了新的数据结构 listpack,沿用了压缩列表紧凑型的内存布局,最终在最新的 Redis版本,将 Hash对象和 Zset对象的底层数据结构实现之一的压缩列表,替换成由 listpack实现。

四、压缩列表

ZipList 是一种特殊的“双端链表” ,由一系列特殊编码的连续内存块组成。可以在任意一端进行压入/弹出操作, 并且该操作的时间复杂度为 O(1)。

属性类型长度用途
zlbytesuint32_t4 字节记录整个压缩列表占用的内存字节数
zltailuint32_t4 字节记录压缩列表表尾节点距离压缩列表的起始地址有多少字节,通过这个偏移量,可以确定表尾节点的地址。
zllenuint16_t2 字节记录了压缩列表包含的节点数量。 最大值为UINT16_MAX (65534),如果超过这个值,此处会记录为65535,但节点的真实数量需要遍历整个压缩列表才能计算得出。
entry列表节点不定压缩列表包含的各个节点,节点的长度由节点保存的内容决定。
zlenduint8_t1 字节特殊值 0xFF (十进制 255 ),用于标记压缩列表的末端。

ZipListEntry

ZipList 中的Entry并不像普通链表那样记录前后节点的指针,因为记录两个指针要占用16个字节,浪费内存。而是采用了下面的结构:

  • previous_entry_length:前一节点的长度,占1个或5个字节。

    • 如果前一节点的长度小于254字节,则采用1个字节来保存这个长度值

    • 如果前一节点的长度大于254字节,则采用5个字节来保存这个长度值,第一个字节为0xfe,后四个字节才是真实长度数据

  • encoding:编码属性,记录content的数据类型(字符串还是整数)以及长度,占用1个、2个或5个字节

  • contents:负责保存节点的数据,可以是字符串或整数

ZipList中所有存储长度的数值均采用小端字节序,即低位字节在前,高位字节在后。例如:数值0x1234,采用小端字节序后实际存储值为:0x3412

Encoding编码

ZipListEntry中的encoding编码分为字符串和整数两种: 字符串:如果encoding是以“00”、“01”或者“10”开头,则证明content是字符串

  • 如果当前节点的数据是整数,encoding会使用1字节的空间进行编码,也就是encoding长度为1字节。通过encoding确认了整数类型,就可以确认整数数据的实际大小了,比如如果encoding编码确认了数据是int16整数,那么data的长度就是int16的大小。
  • 如果当前节点的数据是字符串,根据字符串的长度大小,encoding会使用1字节/2字节/5字节的空间进行编码,encoding编码的前两个bit表示数据的类型,后续的其他bit标识字符串数据的实际长度,即data的长度。

ZipList的每个Entry都包含previous_entry_length来记录上一个节点的大小,长度是1个或5个字节: 如果前一节点的长度小于254字节,则采用1个字节来保存这个长度值 如果前一节点的长度大于等于254字节,则采用5个字节来保存这个长度值,第一个字节为0xfe,后四个字节才是真实长度数据 现在,假设我们有N个连续的、长度为250~253字节之间的entry,因此entry的previous_entry_length属性用1个字节即可表示,如图所示:

ZipList这种特殊情况下产生的连续多次空间扩展操作称之为连锁更新(Cascade Update)。新增、删除都可能导致连锁更新的发生。

优点:

  • 节省内存开销
  • 能更好地利用 CPU 缓存。

缺点:

  • 插入/删除效率低,修改可能触发连锁更新问题,会导致压缩列表占用的内存空间要多次重新分配,这就会直接影响到压缩列表的访问性能。
  • 仅适合元素少、元素小的小对象存储。

五、字典(哈希表)

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

Dict结构图:

Hash冲突

Redis 采用了「链式哈希」的方法来解决哈希冲突(拉链法),(头插法插入节点)

Dict的扩容:

Dict中的HashTable就是数组结合单向链表的实现,当集合中元素较多时,必然导致哈希冲突增多,链表过长,则查询效率会大大降低。

Dict在每次新增键值对时都会检查负载因子(LoadFactor = used/size) ,满足以下两种情况时会触发哈希表扩容(rehash):

  1. 哈希表的 LoadFactor >= 1,并且服务器没有执行 BGSAVE 或者 BGREWRITEAOF 等后台进程;
  2. 哈希表的 LoadFactor > 5 ;

Dict的rehash:

不管是扩容还是收缩,必定会创建新的哈希表,导致哈希表的size和sizemask变化,而key的查询与sizemask有关。因此必须对哈希表中的每一个key重新计算索引,插入新的哈希表,这个过程称为rehash。过程是这样的:

  • 计算新hash表的realeSize,值取决于当前要做的是扩容还是收缩:

    • 如果是扩容,则新size为第一个大于等于dict.ht[0].used + 1的2^n

    • 如果是收缩,则新size为第一个大于等于dict.ht[0].used的2^n (不得小于4)

  • 按照新的realeSize申请内存空间,创建dictht,并赋值给dict.ht[1]

  • 设置dict.rehashidx = 0,标示开始rehash

  • 将dict.ht[0]中的每一个dictEntry都rehash到dict.ht[1]

  • 将dict.ht[1]赋值给dict.ht[0],给dict.ht[1]初始化为空哈希表,释放原来的dict.ht[0]的内存

  • 将rehashidx赋值为-1,代表rehash结束

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

存在的问题:

如果「哈希表1」的数据量非常大,那么在迁移至「哈希表2」的时候,因为会涉及大量的数据拷贝,此时可能会对 Redis 造成阻塞,无法服务其他请求。

渐进式 rehash

为了避免 rehash 在数据迁移过程中,因拷贝数据的耗时,影响 Redis 性能的情况,所以 Redis 采用了 渐进式 rehash,也就是将数据的迁移的工作不再是一次性迁移完成,而是分多次迁移。

渐进式 rehash 步骤如下:

  • 给「哈希表 2」分配空间;

  • 在 rehash 进行期间,每次哈希表元素进行新增、删除、查找或者更新操作时,Redis 除了会执行对应的操作之外,还会顺序将「哈希表 1」中索引位置上的所有 key-value 迁移到「哈希表 2」上;

  • 随着处理客户端发起的哈希表操作请求数量越多,最终在某个时间点会把「哈希表 1」的所有 key-value 迁移到「哈希表 2」,从而完成 rehash 操作。

这样就巧妙地把一次性大量数据迁移工作的开销,分摊到了多次处理请求的过程中,避免了一次性 rehash 的耗时操作。

在进行渐进式 rehash 的过程中,会有两个哈希表,所以在渐进式 rehash 进行期间,哈希表元素的删除、查找、更新等操作都会在这两个哈希表进行。

比如,查找一个 key 的值的话,先会在「哈希表 1」里面进行查找,如果没找到,就会继续到哈希表 2 里面进行找到。

另外,在渐进式 rehash 进行期间,新增一个 key-value 时,会被保存到「哈希表 2」里面,而「哈希表 1」则不再进行任何添加操作,这样保证了「哈希表 1」的 key-value 数量只会减少,随着 rehash 操作的完成,最终「哈希表 1」就会变成空表。

七、跳表

跳表结构设计:

链表在查找元素的时候,因为需要逐一查找,所以查询效率非常低,时间复杂度是O(N),于是就出现了跳表。跳表是在链表基础上改进过来的,实现了一种「多层」的有序链表,这样的好处是能快速定位数据。

下图展示了一个层级为3的跳表

图中头节点有 L0~L2 三个头指针,分别指向了不同层级的节点,然后每个层级的节点都通过指针连接起来:

  • L0 层级共有 5 个节点,分别是节点1、2、3、4、5;
  • L1 层级共有 3 个节点,分别是节点 2、3、5;
  • L2 层级只有 1 个节点,也就是节点 3。

如果我们要在链表中查找节点 4 这个元素,只能从头开始遍历链表,需要查找 4 次,而使用了跳表后,只需要查找 2 次就能定位到节点 4,因为可以在头节点直接从 L2 层级跳到节点 3,然后再往前遍历找到节点 4。

可以看到,这个查找过程就是在多个层级上跳来跳去,最后定位到元素。当数据量很大时,跳表的查找复杂度就是 O(logN)。

那跳表节点是怎么实现多层级的呢?这就需要看「跳表节点」的数据结构了,如下:

typedef struct zskiplistNode {//Zset 对象的元素值sds ele;//元素权重值double score;//后向指针struct zskiplistNode *backward;//节点的 level 数组,保存每层上的前向指针和跨度struct zskiplistLevel {struct zskiplistNode *forward;unsigned long span;} level[];
} zskiplistNode;

Zset 对象要同时保存「元素」和「元素的权重」,对应到跳表节点结构里就是 sds 类型的 ele 变量和 double 类型的 score 变量。每个跳表节点都有一个后向指针(struct zskiplistNode *backward),指向前一个节点,目的是为了方便从跳表的尾节点开始访问节点,这样倒序查找时很方便。

跳表是一个带有层级关系的链表,而且每一层级可以包含多个节点,每一个节点通过指针连接起来,实现这一特性就是靠跳表节点结构体中的zskiplistLevel 结构体类型的 level 数组。

level 数组中的每一个元素代表跳表的一层,也就是由 zskiplistLevel 结构体表示,比如 leve 就表示第一层,leve 就表示第二层。zskiplistLevel 结构体里定义了「指向下一个跳表节点的指针」和「跨度」,跨度时用来记录两个节点之间的距离。

比如,下面这张图,展示了各个节点的跨度。

跨度实际上是为了计算这个节点在跳表中的排位。

具体怎么做的呢?

因为跳表中的节点都是按序排列的,那么计算某个节点排位的时候,从头节点到该结点的查询路径上,将沿途访问过的所有层的跨度累加起来,得到的结果就是目标节点在跳表中的排位。

举个例子,查找图中节点3在跳表中的排位,从头节点开始查找节点3,查找的过程只经过了一个层(L2),并且层的跨度是3,所以节点3在跳表中的排位是3。

另外,图中的头节点其实也是zskiplistNode跳表节点,只不过头节点的后向指针、权重、元素值都没有用到,所以图中省略了这部分。

问题来了,由谁定义哪个跳表节点是头节点呢?这就介绍「跳表」结构体了,如下所示:

typedef struct zskiplist {struct zskiplistNode *header, *tail;unsigned long length;int level;
} zskiplist;

跳表结构里包含了:

  • 跳表的头尾节点,便于在O(1)时间复杂度内访问跳表的头节点和尾节点;
  • 跳表的长度,便于在O(1)时间复杂度获取跳表节点的数量;
  • 跳表的最大层数,便于在O(1)时间复杂度获取跳表中层高最大的那个节点的层数量;

跳表节点查询过程

查找一个跳表节点的过程时,跳表会从头节点的最高层开始,逐一遍历每一层。在遍历某一层的跳表节点时,会用跳表节点中的 SDS 类型的元素和元素的权重来进行判断,共有两个判断条件:

  • 如果当前节点的权重『小于』要查找的权重时,跳表就会访问该层上的下一个节点。
  • 如果当前节点的权重『等于』要查找的权重时,并且当前节点的 SDS 类型数据『小于』要查找的数据时,跳表就会访问该层上的下一个节点。

如果上面两个条件都不满足,或者下一个节点为空时,跳表就会使用目前遍历到的节点的 level 数组里的下一层指针,然后沿着下一层指针继续查找,这就相当于跳到了下一层接着查找。

举个例子,下图有个 3 层级的跳表。

如果要查找『元素:abcd,权重:4』的节点,查找的过程是这样的:

  • 先从头节点的最高层开始,L2 指向了『元素:abc,权重:3』节点,这个节点的权重比要查找节点的小,所以要访问该层上的下一个节点;
  • 但是该层的下一个节点是空节点(leve指向的是空节点),于是就会跳到『元素:abc,权重:3』节点的下一层去找,也就是 leve;
  • 『元素:abc,权重:3』节点的 leve 的下一个指针指向了『元素:abcde,权重:4』的节点,然后将其和要查找的节点比较。虽然『元素:abcde,权重:4』的节点的权重和要查找的权重相同,但是当前节点的 SDS 类型数据『大于』要查找的数据,所以会继续跳到『元素:abc,权重:3』节点的下一层去找,也就是 leve;
  • 『元素:abc,权重:3』节点的 leve 的下一个指针指向了『元素:abcd,权重:4』的节点,该节点正是要查找的节点,查询结束。

跳表节点层数设置


跳表的相邻两层的节点数量的比例会影响跳表的查询性能、举个例子,下图的跳表,第二层的节点数量只有1个,而第一层的节点数量有6个

这时,如果想要查询节点 6,那基本就跟链表的查询复杂度一样,就需要在第一层的节点中依次顺序查找,复杂度就是 O(N) 了。所以,为了降低查询复杂度,我们就需要维持相邻层结点数间的关系。

跳表的相邻两层的节点数量最理想的比例是 2:1,查找复杂度可以降低到 O(logN)。

下图的跳表就是,相邻两层的节点数量的比例是 2:1。

那怎样才能维持相邻两层的节点数量的比例为 2 : 1 呢?

如果采用新增节点或者删除节点时,来调整跳表节点以维持比例的方法的话,会带来额外的开销。

Redis 则采用一种巧妙的方法是,跳表在创建节点的时候,随机生成每个节点的层数,并没有严格维持相邻两层的节点数量比例为 2 : 1 的情况。

具体的做法是,跳表在创建节点时候,会生成范围为[0-1]的一个随机数,如果这个随机数小于 0.25(相当于概率 25%),那么层数就增加 1 层,然后继续生成下一个随机数,直到随机数的结果大于 0.25 结束,最终确定该节点的层数。

这样的做法,相当于每增加一层的概率不超过 25%,层数越高,概率越低,层高最大限制是 64。

虽然我前面讲解跳表的时候,图中的跳表的「头节点」都是 3 层高,但是其实如果层高最大限制是 64,那么在创建跳表「头节点」的时候,就会直接创建 64 层高的头节点。

八、QuickList

其实 quicklist 就是「双向链表+压缩列表」组合,因为一个 quicklist 就是一个链表,而链表中的每个元素又是一个压缩列表。


虽然压缩列表是通过紧凑型的内存布局节省了内存开销,但是因为它的结构设计,如果保存的元素数量增加,或者元素变大了,压缩列表会有「连锁更新」的风险,一旦发生,会造成性能下降。


quicklist 解决办法,通过控制每个链表节点中的压缩列表的大小或者元素个数,来规避连锁更新的问题,因为压缩列表元素越少或越小,连锁更新带来的影响就越小,从而提供了更好的访问性能。
 

quicklist 结构设计

在向 quicklist 添加一个元素的时候,不会像普通的链表那样,直接新建一个链表节点。而是会检查插入位置的压缩列表是否能容纳该元素,如果能容纳就直接保存到 quicklistNode 结构里的压缩列表,如果不能容纳,才会新建一个新的 quicklistNode 结构。

quicklist 会控制 quicklistNode 结构里的压缩列表的大小或者元素个数,来规避潜在的连锁更新的风险,但是这并没有完全解决连锁更新的问题。

QuickList的特点:

  • 是一个节点为ZipList的双端链表

  • 节点采用ZipList,解决了传统链表的内存占用问题

  • 控制了ZipList大小,解决连续内存空间申请效率问题

  • 中间节点可以压缩,进一步节省了内存

九、RedisObject

Redis中的任意数据类型的键和值都会被封装为一个RedisObject,也叫做Redis对象

从Redis的使用者的角度来看,⼀个Redis节点包含多个database(非cluster模式下默认是16个,cluster模式下只能是1个),而一个database维护了从key space到object space的映射关系。这个映射关系的key是string类型,⽽value可以是多种数据类型,比如: string, list, hash、set、sorted set等。

我们可以看到,key的类型固定是string,而value可能的类型是多个。

⽽从Redis内部实现的⾓度来看,database内的这个映射关系是用⼀个dict来维护的。dict的key固定用⼀种数据结构来表达就够了,这就是动态字符串sds。而value则比较复杂,为了在同⼀个dict内能够存储不同类型的value,这就需要⼀个通⽤的数据结构,这个通用的数据结构就是robj,全名是redisObject。

Redis的编码方式

Redis中会根据存储的数据类型不同,选择不同的编码方式,共包含11种不同类型:

编号编码方式说明
0OBJ_ENCODING_RAWraw编码动态字符串
1OBJ_ENCODING_INTlong类型的整数的字符串
2OBJ_ENCODING_HThash表(字典dict)
3OBJ_ENCODING_ZIPMAP已废弃
4OBJ_ENCODING_LINKEDLIST双端链表
5OBJ_ENCODING_ZIPLIST压缩列表
6OBJ_ENCODING_INTSET整数集合
7OBJ_ENCODING_SKIPLIST跳表
8OBJ_ENCODING_EMBSTRembstr的动态字符串
9OBJ_ENCODING_QUICKLIST快速列表
10OBJ_ENCODING_STREAMStream流

五种数据结构

Redis中会根据存储的数据类型不同,选择不同的编码方式。每种数据类型的使用的编码方式如下:

数据类型编码方式
OBJ_STRINGint、embstr、raw
OBJ_LISTLinkedList和ZipList(3.2以前)、QuickList(3.2以后)
OBJ_SETintset、HT
OBJ_ZSETZipList、HT、SkipList
OBJ_HASHZipList、HT

十、Listpack

quicklist 虽然通过控制 quicklistNode 结构里的压缩列表的大小或者元素个数,来减少连锁更新带来的性能影响,但是并没有完全解决连锁更新的问题。

因为 quicklistNode 还是用了压缩列表来保存元素,压缩列表连锁更新的问题,来源于它的结构设计,所以要想彻底解决这个问题,需要设计一个新的数据结构。

于是,Redis 在 5.0 新设计一个数据结构叫 listpack,目的是替代压缩列表,它最大特点是 listpack 中每个节点不再包含前一个节点的长度了,压缩列表每个节点正因为需要保存前一个节点的长度字段,就会有连锁更新的隐患。

Listpack结构设计

listpack 采用了压缩列表的很多优秀的设计,比如还是用一块连续的内存空间来紧凑地保存数据,并且为了节省内存开销,listpack 节点会采用不同的编码方式保存不同大小的数据。

我们先看看 listpack 结构:

 listpack 头包含两个属性,分别记录了 listpack 总字节数和元素数量,然后 listpack 未尾也有个结尾标识。图中的 listpack entry 就是 listpack 的节点了。

主要包含三个方面内容:

  • encoding,定义该元素的编码类型,会对不同长度的整数和字符串进行编码;
  • data,实际存放的数据;
  • len,encoding+data的总长度;

可以看到,listpack 没有压缩列表中记录前一个节点长度的字段了,listpack 只记录当前节点的长度,当我们向 listpack 加入一个新元素的时候,不会影响其他节点的长度字段的变化,从而避免了压缩列表的连锁更新问题。

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

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C#.NET HttpClient 使用教程

简介 HttpClient 是 .NET 中用于发送 HTTP 请求和接收 HTTP 响应的现代化 API,它取代了过时的 WebClient 和 HttpWebRequest 类。 HttpClient 是 .NET Framework 4.5 和 .NET Core/.NET 5 中提供的、基于消息处理管道(message handler pipeline&#…

Nginx常用安全配置指南

Nginx是一个轻量级的,高性能的Web服务器以及反向代理和邮箱代理服务器。它运行在UNIX、GNU、linux、BSD、Mac OS X、Solaris和Windows各种版本。根据调查统计数据显示,当前全球超过6%的网站使用Nginx Web服务器来管理Web网站应用。 为了保证基于Nginx的…

【UniApp 日期选择器实现与样式优化实践】

UniApp 日期选择器实现与样式优化实践 发布时间:2025/6/26 前言 在移动端应用开发中,日期选择器是一个常见且重要的交互组件。本文将分享我们在 UniApp 项目中实现自定义日期选择器的经验,特别是在样式优化过程中遇到的问题及解决方案。通过…

推荐系统的视频特征-视频关键帧特征提取与向量生成

📌 总体流程概览 视频文件 (.mp4)↓ 关键帧抽取(FFmpeg / SceneDetect)↓ 帧图像(.jpg)↓ 图像模型提取特征(CLIP / CNN / ViT)↓ 多帧聚合成视频向量(均值池化等)↓ 向…

Apache SeaTunnel Flink引擎执行流程源码分析

目录 1. 任务启动入口 2. 任务执行命令类:FlinkTaskExecuteCommand 3. FlinkExecution的创建与初始化 3.1 核心组件初始化 3.2 关键对象说明 4. 任务执行:FlinkExecution.execute() 5. Source处理流程 5.1 插件初始化 5.2 数据流生成 6. Transform处理流程 6.1 插…

Vue 3 + Element Plus 实现「动态表单组件」详解教程

✅ Vue 3 Element Plus 实现「动态表单组件」详解教程 📌 适用场景:表单字段根据配置动态生成,支持校验、提交、自定义组件、复杂布局等。 🧩 技术栈:Vue 3 TypeScript Element Plus 🔧 核心特性&#x…

本地部署开源时间跟踪工具 Kimai 并实现外部访问( Windows 版本)

Kimai 是一款开源的时间跟踪工具,它易于使用,并提供了强大的报告功能,在个人和团队记录工作时间、项目时间和活动时间等之后可以帮助用户了解他们是如何花费时间的,从而提高生产力和效率。本文将详细介绍如何在 Windows 系统本地部…

系统分析师案例知识点

目录 1 必做题1.1 状态机图1.2 活动图1.3 统一软件开发过程RUP 2 需求分析2.1 数据流图DFD2.2 ER图2.3 状态转换图STD2.4 数据字典2.5 流程图2.6 需求评审2.7 设计类2.8 FAST分析2.9 常见的关系类 3 嵌入式3.1 容器技术3.2 虚拟机技术3.3 虚拟机和容器的不同点 4 数据库4.1 NoS…

多相机人脸扫描设备如何助力高效打造数字教育孪生体?

在教育数字化转型浪潮中,数字孪生体作为现实教育场景的虚拟映射,正成为智慧教育发展的关键技术支点。传统教育模式面临师资资源分布不均、个性化教学难以覆盖、跨时空教学场景受限等痛点,而数字孪生体通过构建高仿真虚拟教育主体(…

用 EXCEL/WPS 实现聚类分析:赋能智能客服场景的最佳实践

聚类分析作为无监督学习的核心技术,能在客服数据中发现隐藏的用户群体或问题模式。尽管 Excel/WPS 并非专业统计软件,但巧妙利用其内置功能,也能实现基础的聚类分析,为中小型客服团队提供快速洞察。以下介绍具体方法及智能客服场景…

基于定制开发开源AI智能名片S2B2C商城小程序源码的H5游戏开发模式创新研究

摘要 本文以定制开发开源AI智能名片S2B2C商城小程序源码为技术底座,探讨其在H5游戏开发中的创新应用。通过分析原生开发与第三方工具两种传统开发模式的局限性,提出将AI智能名片的多模态内容生成能力、S2B2C商城的生态协同机制与H5游戏开发深度融合的解…

vue3+ELInput无法输入的问题

vue3ElInput无法输入的问题 开篇 写业务的时候发现,因为想偷懒嘛,直接就在想在外部去定义一个变量,然后写个弹窗里(tsx)的el-input,而不是又去写个vue页面,但发现就输入不了了,而且…

SQL Server:如何检测和修复 FILESTREAM 数据库损坏?

SQL Server 中的 FILESTREAM 功能可以将二进制大型对象 (BLOB) 存储到文件系统上,而不是将它们存储在数据库中。但是,默认情况下不启用此功能。用户需要使用 SQL Server Management Studio (SSMS) 和 SQL S…

FORCE 开发者论坛 | 火山引擎发布多款 Agent 开发工具

资料来源:火山引擎-开发者社区 6 月 12 日,2025 火山引擎 FORCE 原动力大会开发者论坛成功举办。大会聚焦 Agent 开发新范式,升级发布了 PromptPilot、MCP Servers、TRAE、扣子开发平台等产品,以及多款开源项目,构建起…

【Qt-windows】如何使用perfmon 具体分析windows serverR2的Qt程序CPU问题

可以使用 Windows 自带的 PerfMon(Performance Monitor) 工具对运行在 Windows Server R2 上的 Qt 程序进行详细的性能分析,尤其是 CPU 使用情况。以下是具体的操作步骤和建议: 一、打开 PerfMon 工具 按下 Win R 打开运行窗口。…

【软考高级系统架构论文】论NoSQL数据库技术及其应用

论文真题 随着互联网web2.0网站的兴起,传统关系数据库在应对web2.0 网站,特别是超大规模和高并发的web2.0纯动态 SNS 网站上已经显得力不从心,暴露了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。 NoSQL(Not only SQL )的产生就是为了解…

bash的配置文件,source

一.按生效范围分类 二.按shell登录的方式分类 这里的执行顺序存疑,因为会互相调用,不需要记忆 source执行脚本 source不创建子进程,bash创建子进程 普通脚本:用bash 配置文件脚本:用source 三.按功能分类

30道C语言高频题整理(附答案背诵版)

1.请描述一下C语言的基本数据类型有哪些? C语言提供了一系列的基本数据类型,它们是构建更复杂数据结构的基础。这些基本数据类型主要包括: 整型(Integer Types):用于存储整数值。根据存储大小和符号性&…

使用Tailwind CSS和i18n的react实践

首先在 src 下设置 i18n.js 文件 // src/i18n.js import i18n from i18next; import { initReactI18next } from react-i18next;import en from ./locales/en/public; import zh from ./locales/zh/public;i18n.use(initReactI18next) .init({resources: {en: { translation:…

生信自学路线|R语言的数据变量类型与对应运算

R 是一种动态类型语言,使用灵活,变量无需预先声明类型。掌握 R 的数据类型和变量机制,是后续进行数据处理和建模分析的基础。本章节主要介绍 R 语言中的常量、变量、基本数据类型及常用数据结构,并结合示例进行说明。 文章目录 一…