Redis 底层数据结构,一文详解

news/2024/11/13 6:40:55/

Redis 底层用 C 语言实现,不同版本的数据类型使用的数据结构也不同,下面详细看

SDS

字符串在 Redis 中很常用,键值对的所有键都是字符串,值有时候也是字符串

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

Redis 不用 C 语言自带的字符串,而是自己又封装了一个结构,肯定是自带的字符串有一定缺陷

C 语言字符串的不足

C 语言的字符串就是一个字符数组,数组中的每个元素是字符串中的一个字符

最后一个字符 '\0' 表示字符串结束,C 语言标准库中的字符串操作函数就是通过判断字符是不是 '\0' 来决定是否停止操作

比如获取字符串长度的函数 strlen,就是通过遍历字符数组并进行计数,遇到 '\0' 字符后停止遍历,返回统计到的字符个数,这样的时间复杂度是 O(n),并且有缺陷,如果字符串中间有个 '\0' 字符,操作字符串时就会提前结束

这个缺点导致了字符串中不能含有 '\0' 字符,进而不能保存像图片、音频这样的二进制数据

另外,C 语言的字符串操作是不安全的,比如 strcat 函数作用是拼接字符串

//将 src 字符串拼接到 dest 字符串后⾯
char *strcat(char *dest, const char* src);

C 语言的字符串是不会记录本身的缓冲区大小的,所以使用这个函数时必须确保 dest 分配了足够的空间,可以容纳 src 字符串中的所有内容,如果空间不足,就会发生缓冲区溢出,可能会导致程序运行终止

总结一下,C 语言字符串的不足有:

  • 获取字符串⻓度的时间复杂度为 O(n)

  • 字符串⾥⾯不能包含有 '\0' 字符,因此不能保存⼆进制数据

  • 字符串操作函数不⾼效且不安全,⽐如有缓冲区溢出的⻛险,有可能会造成程序运⾏终⽌

Redis 实现的 SDS 结构就解决了这些问题

SDS 结构

SDS 结构属性为:

  • len,记录了字符串的长度,在获取字符串长度时只需要返回这个变量,时间复杂度为 O(1)

  • alloc,字符数组的空间,alloc - len 就可以算出剩余空间,使用时空间不够会自动扩容

  • flags,用来表示不同类型的 SDS,一共有 5 种类型,分别为 sdshdr5、sdshdr8、sdshdr16、 sdshdr32 和 sdshdr64

  • buf[],字符数组,不仅可以保存字符串,也可以保存二进制数据

flags 种的五种类型,区别在于对应的 len 和 alloc 属性数据类型不同,比如 sdshdr16,len 和 alloc 的数据类型都是 uint16_t,表示字符数组的长度和空间不能超过 2^16,sdshdr32 则都是 uint32_t,表示表示字符数组⻓度和空间大小不能超过 2^32

之所以这样设计,是为了能够灵活的保存不同大小的字符串,从而有效的节省空间

链表

Redis 的 List 类型底层实现之一就是链表,C 语言本身是没有链表结构的,所以 Redis 自己设计了一个

链表节点结构:

typedef struct listNode {//前置节点struct listNode *prev;//后置节点struct listNode *next;//节点的值void *value;
} listNode;

一个节点有前置节点和后置节点的指针,所以这是一个双向链表

Redis 还在 listNode 结构体的基础上封装了 list 结构:

typedef struct list {//链表头节点listNode *head;//链表尾节点listNode *tail;//节点值复制函数void *(*dup)(void *ptr);//节点值释放函数void (*free)(void *ptr);//节点值⽐较函数int (*match)(void *ptr, void *key);//链表节点数量unsigned long len;
} list;

list 结构有头节点指针 head、尾节点 tail、节点个数 len,以及可以自定义实现的 dup、free 和 match 函数

链表的优势在于,每个节点的空间都是不连续的,插入和删除一个节点很方便,list 结构还有格外的属性用于保存状态,比如获取头节点、尾节点、长度都只需要 O(1)

缺点在于,每个节点空间不连续意味着无法充分利用 CPU 缓存,因为缓存区原理是把一块连续的内存区域加载到缓存中。并且,在链表中保存一个值就需要一整个节点结构的空间,内存开销比较大

Redis 3.0 在数据量比较少的情况下,会采用 压缩列表 作为底层数据结构的实现,它比普通的链表内存更加紧凑,能节省内存空间

压缩列表

压缩列表占用一块连续的内存空间,是一种内存节凑型的数据结构,不仅可以利用 CPU 缓存,还会针对不同长度的数据进行编码

压缩列表的表头有三个字段,尾部有一个字段:

  • zlbytes,记录整个压缩列表占用的字节数

  • zltail,记录压缩列表的尾部距离起始地址有多少个字节,也就是尾部的偏移量

  • zllen,记录压缩列表包含的节点数量

  • zlend,标记压缩列表的结束点,固定值为 0xFF(十进制的 255)

在压缩列表中,定位第一个和最后一个元素可以通过表头的三个字段直接确定,时间复杂度是 O(1),但是查找其他元素时就必须逐个查找,时间复杂度是 O(n),所以 压缩列表不适合保存过多的元素

压缩列表的节点(entry)结构为:

  • prevlen,记录前一个节点的长度

  • encoding,记录当前节点的数据类型以及长度

  • data,记录当前节点实际数据

向压缩列表中插入数据时,会根据数据的类型和大小使用不同空间的 prevlen 和 encoding,从而节省内存

除了查找复杂度较高外,压缩列表还有一个问题,在新增或修改某个元素时,如果空间不足,压缩列表占用的空间就需要重新分配

这就会导致一种现象,当新插入的元素比较大时,会导致后续元素的 prevlen 空间位置都发生变化,从而引起 连锁更新,每个元素的空间都要重新分配

prevlen 的大小是根据上一个节点的长度来分配的:

  • 如果前一个节点的长度小于 254 字节,prevlen 需要用 1 字节的空间来保存这个长度

  • 如果前一个节点的长度大于等于 254 字节,prevlen 就需要用 5 字节的空间

看一个例子,假如现在一个压缩列表中有多个连续的、长度在 250~253 之间的节点:

因为这些节点⻓度值⼩于 254 字节,所以 prevlen 属性需要⽤ 1 字节的空间来保存这个⻓度值

这时,如果将⼀个⻓度⼤于等于 254 字节的新节点加⼊到压缩列表的表头节点,即新节点将成为 e1 的前置节点:

因为 e1 节点的 prevlen 属性只有 1 个字节大小,⽆法保存新节点的⻓度,此时就需要对压缩列表的空间重新分配,并将 e1 节点的 prevlen 属性从原来的 1 字节大小扩展为 5 字节大小,这时候就会有连锁反应:

因为前一个节点的 prevlen 属性扩展了,扩展后的长度大于等于了 254,所以后面的节点中 prevlen 属性也要扩充到 5 字节,这样就导致了连锁更新,非常影响性能

所以,压缩列表虽然能节省内存开销,但是如果保存的元素多了,或者元素变大了,就可以导致连锁更新,导致性能急剧下降

Redis 在 3.2 版本引入了 quicklist,在 5.0 引入了 listpack 两种数据结构,最新版本中,用 listpack 结构取代了压缩列表结构

quicklist

quicklist 的结构体和链表的结构体很类似,都有表头和表尾,区别在于 quicklist 的节点换成了 quicklistNode 结构

typedef struct quicklist {//quicklist的链表头quicklistNode *head;//quicklist的链表头quicklistNode *tail;//所有压缩列表中的总元素个数unsigned long count;//quicklistNodes的个数unsigned long len; ...
} quicklist;

quicklistNode 的结构:

typedef struct quicklistNode {//前⼀个quicklistNodestruct quicklistNode *prev;//下⼀个quicklistNodestruct quicklistNode *next;//quicklistNode指向的压缩列表unsigned char *zl; //压缩列表的的字节⼤⼩unsigned int sz; //压缩列表的元素个数unsigned int count : 16;....
} quicklistNode

quicklistNode 也是一个双向链表结构,但是节点的元素不再单纯保存元素值,而是保存一个压缩列表,即结构中的 *zl 属性

向 quicklist 中添加元素时,不再像普通的链表一样,直接新建一个链表节点,而是检查插入位置的压缩列表是否能够容纳该元素,如果能容纳就直接放到压缩列表中,不能容纳再新建一个 quicklistNode 结构

quicklist 通过控制 quicklistNode 结构中的压缩列表的大小或者元素个数来规避潜在的连锁更新,但是并没有完全解决,在某些情况下依然会发生连锁更新

listpack

quicklistNode 结构中有压缩列表,注定无法彻底规避掉连锁更新,想要彻底解决这个问题,就需要设计新的数据结构

Redis 在 5.0 版本新设计了 listpack 结构,用于代替压缩列表,最大特点是 listpack 中的每个节点不再包含上一个节点的长度,从而彻底避免连锁更新

listpack 也用了一块连续的空间来紧凑的保存数据,并且为了节省内存开销,listpack 节点采用不同的编码用于保存不同大小的数据

listpack 的结构为:

头部包括两个属性,用于记录总字节数和元素数量,末尾用于标识结束,listpack entry 就是节点

entry 的结构为:

  • encoding,定义保存元素的编码类型,会根据不同长度的数据进行不同的编码

  • data,实际存放的数据

  • len,encoding + data 的总长度

listpack 只记录当前节点的长度,插入新元素时,不会导致其他节点发生变化,这样就避免了连锁更新问题

哈希表

哈希表是一种保存键值对的数据结构,哈希表中的每一个 key 都是唯一的,可以根据 key 找到对应的 value

Redis 的 Hash 对象底层实现之一是压缩列表(最新版本的 Redis 已经替换为了 listpack),另一个实现就是哈希表

哈希表的优点在于,可以在 O(1) 的复杂度下查找数据,把 key 进行 Hash 函数运算,就能定位数据在哈希表中的位置,因为哈希表实际上就是一个数组,可以通过索引值快速查询到数据

哈希表的结构为:

typedef struct dictht {//哈希表数组dictEntry **table;//哈希表⼤⼩unsigned long size; //哈希表⼤⼩掩码,⽤于计算索引值unsigned long sizemask;//该哈希表已有的节点数量unsigned long used;
} dictht

可以看出来,哈希表实际上就是一个数组(dictEntry **table),数组的每一个元素都是指向哈希节点(dictEntry)的指针

哈希节点 dictEntry 的结构为:

typedef struct dictEntry {//键值对中的键void *key;//键值对中的值union {void *val;uint64_t u64;int64_t s64;double d;} v;//指向下⼀个哈希表节点,形成链表struct dictEntry *next;
} dictEntry;
​

dictEntry 结构中不仅有指向键和值的指针,还有指向下一个哈希节点的指针,这个指针用于将多个哈希值相同的键值对链接起来,以此来解决哈希冲突

哈希冲突

在计算一个键值对的位置时,首先要把键经过 Hash 函数计算得到哈希值,再通过(哈希值 % 哈希表大小)进行取模,得到的结果就是哈希表中对应数组元素的位置,也就是第几个哈希桶

而在计算的过程中,不同的键计算的下标结果相同,就称作发生了哈希冲突,Redis 采用了链式哈希来解决哈希冲突,实现方式就是给每个哈希节点分配一个 next 指针,如果发生了哈希冲突,就可以通过这个 next 指针不断连接哈希节点,形成一个单向链表

这样虽然可以解决哈希冲突,但是随着链表的长度增加,查询这一位置的耗时就会增加,因为查询单向链表的时间复杂度为 O(n)

想要解决这一问题,就需要对哈希表的大小进行扩展,进行 rehash

rehash

Redis 在使用哈希表时,会定义一个 dict 结构体,这个结构体中包含两个哈希表

typedef struct dict {…//两个Hash表,交替使⽤,⽤于rehash操作dictht ht[2];…
} dict;

之所以定义两个哈希表,就是为了进行 rehash 操作

在正常情况下,插入的数据都会被写入到 哈希表1 中,此时的 哈希表2 并没有分配空间,随着数据逐步增多,单向链表越来越长,就会触发 rehash 操作,这个操作分为三步:

  • 给 哈希表2 分配空间,一般会比 哈希表1 大 2 倍

  • 将 哈希表1 的数据迁移到 哈希表2 中

  • 迁移完成后,释放 哈希表1 的空间,把 哈希表2 设置为 哈希表1,然后创建一个新的哈希表 2,为下次 rehash 做准备

这个过程看起来很简单直观,但是第二步其实很有问题,如果 哈希表1 的数据量非常大,那么在迁移到 哈希表2 的时候,会涉及到大量的数据拷贝,可能会对 Redis 造成阻塞,无法响应正常请求

渐进式 rehash

为了避免上面提到的问题,Redis 采用了渐进式 rehash,也就是把数据的迁移工作分成多次

渐进式 rehash 步骤为:

  • 给 哈希表2 分配空间

  • 在 rehash 期间,每次哈希表元素进行增删改查的时候,除了对这些操作响应外,还会按顺序把 哈希表1 的键值对迁移到 哈希表2 上

  • 随着元素操作次数越来越多,最终会在某个时间点完成键值对的迁移,从而完成 rehash 操作

这样把 rehash 操作分摊到多次处理请求的过程中,避免了一次性 rehash 带来的耗时问题

在 rehash 期间,对哈希表元素的删改查操作都会在两个哈希表中进行,一个哈希表中没有找到对应键值对就去找另外一个

而增加元素,会直接保存到 哈希表2 中,哈希表1 不做任何操作,这样随着 rehash 操作的进行,哈希表 1 会主键变为空表

rehash 触发条件

rehash 的触发条件和负载因子(load factor)有关

负载因子 = 哈希表已保存节点数量 / 哈希表大小

rehash 操作触发条件主要有两个:

  • 当负载因子大于等于 1,并且 Redis 在没有执行 RDB 快照和 AOF 重写的时候,就会进行 rehash 操作

  • 当负载因子大于等于 5,此时说明哈希冲突已经非常严重了,不管有没有在执行 RDB 快照或者 AOF 重写,都会强制进行 rehash 操作

intset

intset,即整数集合,是 Set 结构的底层实现之一,当一个 Set 对象只包含整数元素,并且元素数量较少时,就会使用整数集合作为底层实现

整数集合实际上是一块连续的内存空间,结构为:

typedef struct intset {//编码⽅式uint32_t encoding;//集合包含的元素数量uint32_t length;//保存元素的数组int8_t contents[];
} intset

保存元素的容器是一个 contents 数组,虽然 contents 被声明为 int8_t 类型的数组,但是实际上存放的并不是 int8_t,真正的取值类型取决于 encoding 属性的值:

  • 如果 encoding 属性值为 INTSET_ENC_INT16,那么 contents 就是⼀个 int16_t 类型的数组,数组中每⼀个元素的类型都是 int16_t

  • 如果 encoding 属性值为 INTSET_ENC_INT32,那么 contents 就是⼀个 int32_t 类型的数组,数组中每⼀个元素的类型都是 int32_t

  • 如果 encoding 属性值为 INTSET_ENC_INT64,那么 contents 就是⼀个 int64_t 类型的数组,数组中每⼀个元素的类型都是 int64_t

不同类型的 contents 数组大小也不同

升级操作

整数集合有一个升级规则,加入新元素时,如果新元素的类型比整数集合现有的所有元素的类型都要大时,整数集合就会先升级,按照新元素的类型扩展 contents 数组,然后才能把新元素加到整数集合里

升级的过程不会重新分配一个新类型的数组,而是在原来的数组上扩展空间,然后在每个元素之间按照类型大小分割,比如 encoding 属性值为 INTSET_ENC_INT16,那每个元素之间就是 16 位

整数集合升级的好处是,可以按需求存放 int16_t、int32_t、int64_t 类型的元素,如果没有升级,想保存这三种类型的值就只能用 int64_t 类型的数组,但是如果所有的元素都是 int16_t,就会白白浪费很多空间

整数集合升级就能避免这种情况,如果一直往整数集合添加 int16_t 类型的元素,contents 就一直是 int16_t,只有要添加 int32_t 或者 int64_t 类型的元素时,才会对数组升级

要注意,数组一旦进行升级,就会一直保持升级后的状态,不支持再降级

跳表

Redis 只在 ZSet 结构的底层使用了跳表,跳表的优势是能支持平均复杂度为 O(logn) 的查找操作

  • ZSet 结构在使用压缩列表作为底层数据结构时,ZSet 结构的指针会指向压缩列表

  • ZSet 结构在使用跳表作为底层数据结构时,ZSet 结构的指针会指向 ZSet 结构(哈希表+跳表)

ZSet 在使用跳表作为底层数据结构时,并不是指向跳表结构,而是指向 ZSet 结构:

typedef struct zset {dict *dict;zskiplist *zsl;
} zset;

它包含了哈希表和跳表两个结构,这样的好处是既能进行高效的范围查询,也能进行高效的单点查询

下面来详细看跳表的结构

跳表结构

在链表上查找元素时,需要逐一查找,时间复杂度是 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;

节点内同时保存了元素的值和权重;每个跳表节点都有一个指针,指向前一个节点,这样方便从跳表的尾部开始访问节点;权重值决定了元素在集合中的排序,权重值较小的排在前面

level 数组保存了当前节点的层级关系,包括当前在哪一层、前向指针 forward 和跨度 span,level本身取值的下标表示具体层级,level[0] 代表第 0层、level[1] 代表第 1 层,以此类推

其他属性都好理解,跨度需要再说明一下

跨度是为了计算这个节点在跳表中的排位,因为跳表中的节点都是有顺序的,计算某个节点的位置时,从头节点出发到该节点的查询路径上,把沿途访问过的所有层的节点数累加起来,得到的结果就是该节点在跳表中的位置

比如要查找上图中的节点 3 在跳表中的位置,从头节点开始,查询过程中先找节点 1、再找节点 2,最后到达节点 3,一共需要 3 个跨度,所以节点 3 在跳表中的位置是 3,通过节点 3 的跨度值,就可以快速知道当前节点前面的节点数

跳表结构为:

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

跳表结构有:

  • 头尾节点,便于在 O(1) 的时间复杂度访问跳表的头尾节点

  • 跳表长度,便于在 O(1) 的时间复杂度获取跳表的长度

  • 跳表的最⼤层数,便于在 O(1) 时间复杂度获取跳表中层⾼最⼤的那个节点的层数量

可以看出,头尾节点也是普通的跳表节点,需要注意,头尾节点不存储元素,头尾节点的层数就是跳表的最大层数

查询过程

查找一个跳表节点是根据权重值 score 来查,根据值做唯一性判断,查询时从头节点的最高层开始,逐一遍历每一层:

  • 如果当前节点的权重小于待查找权重,继续在本层上向后搜索

  • 如果当前节点的权重等于待查找权重,并且当前节点的值小于要查找的值时,也继续在本层上向后搜索

如果这两个条件都不满足,或者下一个节点为空时,就需要根据 level 数组找下一层的指针,沿着下一层指针继续查找,即跳到下一层接着找

比如,现在有个 3 层的跳表,要查找元素为 abcd、权重为 4 的节点

  • 先从头节点的最高层找起,L2 指向了值为 abc、权重为 3 的节点,这个节点比要查找节点的权重要小,所以要访问该层的下一个节点

  • 但是该层的下一个节点为空,所以要跳到下一层,即从 L2 层跳到 L1 层

  • L1 层的 abc 节点,下一个节点是 abcde,权重是 4,虽然权重和待查找的节点相同,但是 abcde 的值要比 abcd 大,所以要继续往下跳,在 L0 层找

  • L0 层的 abc 节点,下一个节点是 abcd,权重为 4,就是要查找的节点,查询结束

层数设置

跳表的相邻两层的节点数量的比例会影响跳表的查询性能,最理想的比例是 2 :1,查找复杂度可以降到 O(logn)

Redis 并没有严格维持比例为 2:1,跳表在创建节点时,会生成 [0, 1] 范围内的随机数,如果这个数小于 0.25,那么层数就增加一层,然后继续生成下一个随机数,直到随机数的结果大于 0.25 才结束

所以,跳表中的元素有几层是随机决定的,但是只要元素出现在了第 k 层,那么 k 层以下的链表也会出现这个元素

不同版本限制最高层数不同,Redis 7.0 为 32,5.0 为 64,3.0 定义为 32

常见数据类型所用结构

Redis 常见的数据类型有 string、list、hash、set、zset,后续增加的 bitmap、hyperLogLog 等暂不讨论

  • string,最早期 string 是 SDS,从 4.0 开始,对于大型字符串(超过 512 MB),使用 Redis Object(字节流形式)

  • list,早期时元素较少使用压缩列表,元素较多使用双向链表,从 4.0 开始使用 quicklist

  • hash,小的哈希表使用压缩列表,大的使用哈希表

  • set,小集合使用压缩列表,大集合使用哈希表

  • zset,小型 zset 使用压缩列表,大型 zset 使用跳表 skiplist


http://www.ppmy.cn/news/1527548.html

相关文章

功能测试干了三年,快要废了。。。

8年前刚进入到IT行业,到现在学习软件测试的人越来越多,所以在这我想结合自己的一些看法给大家提一些建议。 最近聊到软件测试的行业内卷,越来越多的转行和大学生进入测试行业,导致软件测试已经饱和了,想要获得更好的待…

基于鸿蒙API10的RTSP播放器(八:音量和亮度调节功能的整合)

一、前言: 笔者在前面第六、七节文章当中,分别指出了音量和屏幕亮度的前置知识,在本节当中,我们将一并实现这两个功能,从而接续第五节内容。本文的逻辑分三大部分,先说用到的变量,再说界面&…

【TypeScript】 ts控制语句

文章目录 ts控制语句1. 条件语句1.1 if 语句1.2 if...else 语句1.3 if...else if...else 语句1.4 switch...case 语句 2. 循环2.1 for 循环2.2 for...in 循环2.3 for...of、forEach、every 和 some 循环2.4 while 循环2.5 do...while 循环2.6 break 语句2.7 continue 语句 3. 函…

Golang | Leetcode Golang题解之第406题根据身高重建队列

题目&#xff1a; 题解&#xff1a; func reconstructQueue(people [][]int) (ans [][]int) {sort.Slice(people, func(i, j int) bool {a, b : people[i], people[j]return a[0] > b[0] || a[0] b[0] && a[1] < b[1]})for _, person : range people {idx : pe…

C编程控制PC蜂鸣器方法2

在《C编程控制PC蜂鸣器》一文中,我们了解并使用了通过IO端口控制的方式操作硬件,而有些时候这对于一些朋友来说太模糊了,很容易让人迷糊,这次采用最基本的write系统调用来写入input_event数据实现相同功能。这里涉及到的input_event可参考《C编程实现键盘LED闪烁方法2》一文…

[数据集][目标检测]红外微小目标无人机直升机飞机飞鸟检测数据集VOC+YOLO格式7559张4类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;7559 标注数量(xml文件个数)&#xff1a;7559 标注数量(txt文件个数)&#xff1a;7559 标注…

Dify 中的讯飞星火平台工具源码分析

本文主要对 Dify 中的讯飞星火平台工具 spark 进行了源码分析&#xff0c;该工具可根据用户的输入生成图片&#xff0c;由讯飞星火提供图片生成 API。通过本文学习可自行实现将第三方 API 封装为 Dify 中工具的能力。 源码位置&#xff1a;dify-0.6.14\api\core\tools\provide…

机器学习实战—天猫用户重复购买预测

目录 背景 数据集 用户画像数据 用户行为日志数据 训练数据 测试数据 提交数据 其它数据 数据探索 导入依赖库 读取数据 查看数据信息 缺失值分析 数据分布 复购因素分析 特征工程 模型训练 模型验证 背景 商家有时会在特定日期,例如节礼日(Boxing-day),黑…