Redis常用的5种数据类型底层结构是怎样构成的

2023-11-18

前言

Redis是一个基于内存中的数据结构存储系统,可以用作数据库、缓存和消息中间件。Redis支持五种常见对象类型:字符串(String)、哈希(Hash)、列表(List)、集合(Set)以及有序集合(Zset),我们在日常工作中也会经常使用它们。知其然,更要知其所以然,本文将会带你读懂这五种常见对象类型的底层数据结构。

1. 对象类型和编码

Redis使用对象来存储键和值的,在Redis中,每个对象都由redisObject结构表示。redisObject结构主要包含三个属性:type、encoding和 ptr。

typedef struct redisObject {
    // 类型
    unsigned type:4;
    // 编码
    unsigned encoding:4;
    // 底层数据结构的指针
    void *ptr;
} robj;

其中type属性记录了对象的类型。对于Redis来说,键对象总是字符串类型,值对象可以是任意支持的类型。因此,当我们说Redis键采用哪种对象类型的时候,指的是对应的值采用哪种对象类型。

Redis常用的5种数据类型底层结构是怎样构成的_Redis

*ptr属性指向了对象的底层数据结构,而这些数据结构由 encoding 属性决定。

Redis常用的5种数据类型底层结构是怎样构成的_数据类型_02

之所以由encoding属性来决定对象的底层数据结构,是为了实现同一对象类型,支持不同的底层实现。这样就能在不同场景下,使用不同的底层数据结构,进而极大提升Redis的灵活性和效率。

底层数据结构后面会详细讲解,这里简单看一下即可。

2. 字符串对象

字符串是我们日常工作中用得最多的对象类型,它对应的编码可以是int、raw 和 embstr。字符串对象相关命令可参考:Redis命令-Strings。

如果一个字符串对象保存的是不超过long类型的整数值,此时编码类型即为 int,其底层数据结构直接就是long类型。例如执行set number 10086,就会创建int编码的字符串对象作为number键的值。

Redis常用的5种数据类型底层结构是怎样构成的_数据类型_03

如果字符串对象保存的是一个长度大于39字节的字符串,此时编码类型即为 raw,其底层数据结构是简单动态字符串(SDS);如果长度小于等于 39个字节,编码类型则为embstr,底层数据结构就是embstr编码SDS。下面,我们详细理解下什么是简单动态字符串。

2.1 简单动态字符串

SDS 定义

在Redis中,使用sdshdr数据结构表示SDS:

struct sdshdr {
    // 字符串长度
    int len;
    // buf数组中未使用的字节数
    int free;
    // 字节数组,用于保存字符串
    char buf[];
};

SDS遵循了C字符串以空字符结尾的惯例,保存空字符的1字节不会计算在len属性里面。例如,Redis这个字符串在SDS里面的数据可能是如下形式:

Redis常用的5种数据类型底层结构是怎样构成的_数据类型_04

SDS与C字符串的区别

C语言使用长度为N+1的字符数组来表示长度为N的字符串,并且字符串的最后一个元素是空字符\0。Redis采用SDS相对于C字符串有如下几个优势:

1.常数复杂度获取字符串长度;

2.杜绝缓冲区溢出;

3.减少修改字符串时带来的内存重分配次数;

4.二进制安全。

常数复杂度获取字符串长度

因为C字符串并不记录自身的长度信息,所以为了获取字符串的长度,必须遍历整个字符串,时间复杂度是 O(N)。而SDS使用len属性记录了字符串的长度,因此获取SDS字符串长度的时间复杂度是O(1)。

杜绝缓冲区溢出

C字符串不记录自身长度带来的另一个问题是,很容易造成缓存区溢出。比如使用字符串拼接函数(stract)的时候,很容易覆盖掉字符数组原有的数据。

与C字符串不同,SDS的空间分配策略完全杜绝了发生缓存区溢出的可能性。当SDS进行字符串扩充时,首先会检查当前的字节数组的长度是否足够。如果不够的话,会先进行自动扩容,然后再进行字符串操作。

减少修改字符串时带来的内存重分配次数

因为C字符串的长度和底层数据是紧密关联的,所以每次增长或者缩短一个字符串,程序都要对这个数组进行一次内存重分配:

1.如果是增长字符串操作,需要先通过内存重分配来扩展底层数组空间大小,不这么做就导致缓存区溢出;

2.如果是缩短字符串操作,需要先通过内存重分配来来回收不再使用的空间,不这么做就导致内存泄漏。

因为内存重分配涉及复杂的算法,并且可能需要执行系统调用,所以通常是个比较耗时的操作。对于Redis来说,字符串修改是一个十分频繁的操作。如果每次都像C字符串那样进行内存重分配,对性能影响太大了,显然是无法接受的。

SDS通过空闲空间解除了字符串长度和底层数据之间的关联。在SDS中,数组中可以包含未使用的字节,这些字节数量由free属性记录。通过空闲空间,SDS实现了空间预分配和惰性空间释放两种优化策略。

1. 空间预分配

空间预分配是用于优化SDS字符串增长操作的,简单来说就是当字节数组空间不足触发重分配的时候,总是会预留一部分空闲空间。这样的话,就能减少连续执行字符串增长操作时的内存重分配次数。

有两种预分配的策略:

len小于1MB时:每次重分配时会多分配同样大小的空闲空间; len大于等于1MB 时:每次重分配时会多分配1MB大小的空闲空间。

2.惰性空间释放

惰性空间释放是用于优化SDS字符串缩短操作的。简单来说就是当字符串缩短时,并不立即使用内存重分配来回收多出来的字节,而是用free属性记录,等待将来使用。SDS也提供直接释放未使用空间的API,在需要的时候,也能真正的释放掉多余的空间。

二进制安全

C字符串中的字符必须符合某种编码,并且除了字符串末尾之外,其它位置不允许出现空字符。这些限制使得C字符串只能保存文本数据。

但是对于Redis来说,不仅仅需要保存文本,还要支持保存二进制数据。为了实现这一目标,SDS的API全部做到了二进制安全(binary-safe)。

2.2 raw和embstr编码的SDS区别

我们在前面讲过,长度大于 39 字节的字符串,编码类型为raw,底层数据结构是简单动态字符串(SDS)。这个很好理解,比如当我们执行set story "Long, long, long ago there lived a king ..."(长度大于39)之后,Redis就会创建一个raw编码的String对象。

数据结构如下:

Redis常用的5种数据类型底层结构是怎样构成的_Redis_05

长度小于等于 39 个字节的字符串,编码类型为embstr,底层数据结构则是embstr编码SDS。embstr编码是专门用来保存短字符串的,它和raw编码最大的不同在于:raw编码会调用两次内存分配分别创建 redisObject结构和sdshdr结构;而embstr编码则是只调用一次内存分配,在一块连续的空间上同时包含redisObject 结构和sdshdr结构。

Redis常用的5种数据类型底层结构是怎样构成的_Redis_06

2.3 编码转换

int编码和embstr编码的字符串对象在条件满足的情况下会自动转换为raw 编码的字符串对象。

对于int编码来说,当我们修改这个字符串为不再是整数值的时候,此时字符串对象的编码就会从int变为raw。

对于embstr编码来说,只要我们修改了字符串的值,此时字符串对象的编码就会从embstr变为raw。

embstr编码的字符串对象可以认为是只读的,因为Redis为其编写任何修改程序。当我们要修改embstr编码字符串时,都是先将转换为raw编码,然后再进行修改。

3. 列表对象

列表对象的编码可以是linkedlist或者ziplist,对应的底层数据结构是链表和压缩列表。列表对象相关命令可参考:Redis命令-List。

默认情况下,当列表对象保存的所有字符串元素的长度都小于64字节,且元素个数小于512个时,列表对象采用的是ziplist编码,否则使用 linkedlist编码。

可以通过配置文件修改该上限值。

3.1 链表

链表是一种非常常见的数据结构,提供了高效的节点重排能力以及顺序性的节点访问方式。在Redis中,每个链表节点使用listNode结构表示:

typedef struct listNode {
    // 前置节点
    struct listNode *prev;
    // 后置节点
    struct listNode *next;
    // 节点值
    void *value;
} listNode复制代码

多个 listNode 通过 prev 和 next 指针组成双端链表,如下图所示:

Redis常用的5种数据类型底层结构是怎样构成的_数据类型_07

为了操作起来比较方便,Redis 使用了list结构持有链表。

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

list结构为链表提供了表头指针head、表尾指针tail,以及链表长度计数器len,而dup、free和match成员则是实现多态链表所需类型的特定函数。

Redis常用的5种数据类型底层结构是怎样构成的_Redis_08

Redis链表实现的特征总结如下:

1.双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(n);

2.无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL为终点;

3.带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1);

4.带链表长度计数器:程序使用list结构的len属性来对list持有的节点进行计数,程序获取链表中节点数量的复杂度为O(1);

5.多态:链表节点使用void* 指针来保存节点值,可以保存各种不同类型的值。

3.2 压缩列表

压缩列表(ziplist)是列表键和哈希键的底层实现之一。压缩列表主要目的是为了节约内存,是由一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值。

Redis常用的5种数据类型底层结构是怎样构成的_Redis_09

如上图所示,压缩列表记录了各组成部分的类型、长度以及用途。

Redis常用的5种数据类型底层结构是怎样构成的_Redis_10

4. 哈希对象

哈希对象的编码可以是ziplist或者hashtable。

4.1 hash-ziplist

ziplist底层使用的是压缩列表实现,上文已经详细介绍了压缩列表的实现原理。每当有新的键值对要加入哈希对象时,先把保存了键的节点推入压缩列表表尾,然后再将保存了值的节点推入压缩列表表尾。比如,我们执行如下三条 HSET命令:

HSET profile name "tom"
HSET profile age 25
HSET profile career "Programmer"

如果此时使用 ziplist编码,那么该Hash对象在内存中的结构如下:

Redis常用的5种数据类型底层结构是怎样构成的_Redis_11

3.2 hash-hashtable

hashtable编码的哈希对象使用字典作为底层实现。字典是一种用于保存键值对的数据结构,Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,每个哈希表节点保存的就是一个键值对。

3.3 哈希表

Redis使用的哈希表由dictht结构定义:

typedef struct dictht{
    // 哈希表数组
    dictEntry **table;

    // 哈希表大小
    unsigned long size;

    // 哈希表大小掩码,用于计算索引值
    // 总是等于 size-1
    unsigned long sizemask;

    // 该哈希表已有节点数量
    unsigned long used;
} dictht

table属性是一个数组,数组中的每个元素都是一个指向 dictEntry 结构的指针,每个dictEntry结构保存着一个键值对。

size属性记录了哈希表的大小,即table数组的大小。used属性记录了哈希表目前已有节点数量。sizemask总是等于size-1,这个值主要用于数组索引。

比如下图展示了一个大小为4的空哈希表。

Redis常用的5种数据类型底层结构是怎样构成的_数据类型_12

哈希表节点

哈希表节点使用dictEntry结构表示,每个dictEntry结构都保存着一个键值对:

typedef struct dictEntry {
    // 键
    void *key;

    // 值
    union {
        void *val;
        unit64_t u64;
        nit64_t s64;
    } v;

    // 指向下一个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;

key属性保存着键值对中的键,而v属性则保存了键值对中的值。值可以是一个指针,一个uint64_t整数或者是int64_t整数。next 属性指向了另一个dictEntry节点,在数组桶位相同的情况下,将多个dictEntry节点串联成一个链表,以此来解决键冲突问题(链地址法)。

3.4 字典

Redis字典由dict结构表示:

typedef struct dict {
    // 类型特定函数
    dictType *type;

    // 私有数据
    void *privdata;

    // 哈希表
    dictht ht[2];

    //rehash索引
    // 当rehash不在进行时,值为-1
    int rehashidx;
}

ht是大小为 2,且每个元素都指向 dictht 哈希表。一般情况下,字典只会使用 ht[0] 哈希表,ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用。rehashidx 记录了 rehash 的进度,如果目前没有进行 rehash,值为 -1。

rehash

为了使hash表的负载因子(ht[0]).used/ht[0]).size)维持在一个合理范围,当哈希表保存的元素过多或者过少时,程序需要对hash表进行相应的扩展和收缩。

rehash(重新散列)操作就是用来完成hash表的扩展和收缩的。

rehash的步骤如下:

  1. 为 ht [1] 哈希表分配空间;

1.如果是扩展操作,那么ht[1] 的大小为第一个大于 ht[0].used*2的2n。比如ht[0].used=5,那么此时 ht[1] 的大小就为 16(大于 10 的第一个 2n 的值是 16);

2.如果是收缩操作,那么 ht[1] 的大小为第一个大于 ht[0].used 的 2n。比如ht[0].used=5,那么此时 ht[1] 的大小就为 8(大于 5 的第一个 2n 的值是 8)。

  1. 将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 中;

  2. 迁移完成之后,释放掉 ht[0],并将现在的 ht[1] 设置为 ht[0],在 ht[1] 新创建一个空白哈希表,为下一次 rehash 做准备。

哈希表的扩展和收缩时机

1.当服务器没有执行 BGSAVE 或者 BGREWRITEAOF 命令时,负载因子大于等于 1 触发哈希表的扩展操作;

2.当服务器在执行 BGSAVE 或者 BGREWRITEAOF 命令,负载因子大于等于 5 触发哈希表的扩展操作;

3.当哈希表负载因子小于 0.1,触发哈希表的收缩操作。

渐进式 rehash

前面讲过,扩展或者收缩需要将 ht[0] 里面的元素全部 rehash 到 ht[1] 中,如果 ht[0] 元素很多,显然一次性 rehash 成本会很大,从影响到 Redis 性能。

为了解决上述问题,Redis 使用了渐进式 rehash 技术,具体来说就是分多次,渐进式地将 ht[0] 里面的元素慢慢地 rehash 到 ht[1] 中。

下面是渐进式 rehash 的详细步骤:

为 ht[1] 分配空间; 在字典中维持一个索引计数器变量 rehashidx,并将它的值设置为 0,表示 rehash 正式开始; 在 rehash 进行期间,每次对字典执行添加、删除、查找或者更新时,除了会执行相应的操作之外,还会顺带将 ht[0] 在 rehashidx 索引位上的所有键值对 rehash 到 ht[1] 中,rehash 完成之后,rehashidx 值加 1; 随着字典操作的不断进行,最终会在啊某个时刻迁移完成,此时将 rehashidx 值置为 -1,表示 rehash 结束。

**渐进式 rehash 一次迁移一个桶上所有的数据。**设计上采用分而治之的思想,将原本集中式的操作分散到每个添加、删除、查找和更新操作上,从而避免集中式 rehash 带来的庞大计算。

因为在渐进式rehash时,字典会同时使用ht[0]和ht[1]两张表,所以此时对字典的删除、查找和更新操作都可能会在两个哈希表进行。比如,如果要查找某个键时,先在ht[0]中查找,如果没找到,则继续到ht[1]中查找。

hash对象中的hashtable

HSET profile name "tom"
HSET profile age 25
HSET profile career "Programmer"

还是上述三条命令,保存数据到 Redis 的哈希对象中,如果采用 hashtable 编码保存的话,那么该 Hash 对象在内存中的结构如下:

图片

当哈希对象保存的所有键值对的键和值的字符串长度都小于 64 个字节,并且数量小于 512 个时,使用 ziplist 编码,否则使用 hashtable 编码。

可以通过配置文件修改该上限值。

4. 集合对象

集合对象的编码可以是 intset 或者 hashtable。当集合对象保存的元素都是整数,并且个数不超过 512 个时,使用 intset 编码,否则使用 hashtable 编码。

4.1 set-intset

intset 编码的集合对象底层使用整数集合实现。

整数集合(intset)是 Redis 用于保存整数值的集合抽象数据结构,它可以保存类型为 int16_t、int32_t 或者 int64_t 的整数值,并且保证集合中的数据不会重复。Redis 使用 intset 结构表示一个整数集合。

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

contents 数组是整数集合的底层实现:整数集合的每个元素都是 contents 数组的一个数组项,各个项在数组中按值大小从小到大有序排列,并且数组中不包含重复项。

虽然 contents 属性声明为 int8_t 类型的数组,但实际上,contents 数组不保存任何 int8_t 类型的值,数组中真正保存的值类型取决于 encoding。

如果 encoding 属性值为 INTSET_ENC_INT16,那么 contents 数组就是 int16_t 类型的数组,以此类推。

当新插入元素的类型比整数集合现有类型元素的类型大时,整数集合必须先升级,然后才能将新元素添加进来。这个过程分以下三步进行:

1.根据新元素类型,扩展整数集合底层数组空间大小;

2.将底层数组现有所有元素都转换为与新元素相同的类型,并且维持底层数组的有序性;

3.将新元素添加到底层数组里面。

还有一点需要注意的是,整数集合不支持降级。一旦对数组进行了升级,编码就会一直保持升级后的状态。

举个例子,当执行 SADD numbers 1 3 5 向集合对象插入数据时,该集合对象在内存的结构如下:

Redis常用的5种数据类型底层结构是怎样构成的_数据类型_13

4.2 set-hashtable

hashtable 编码的集合对象使用字典作为底层实现。字典的每个键都是一个字符串对象,每个字符串对象对应一个集合元素,字典的值都是 NULL。

当我们执行 SADD fruits "apple" "banana" "cherry" 向集合对象插入数据时,该集合对象在内存的结构如下:

Redis常用的5种数据类型底层结构是怎样构成的_数据类型_14

5. 有序集合对象

有序集合的编码可以是 ziplist 或者 skiplist。当有序集合保存的元素个数小于 128 个,且所有元素成员长度都小于 64 字节时,使用 ziplist 编码,否则使用 skiplist 编码。

5.1 zset-ziplist

ziplist 编码的有序集合使用压缩列表作为底层实现。每个集合元素使用两个紧挨着一起的两个压缩列表节点表示,第一个节点保存元素的成员(member),第二个节点保存元素的分值(score)。

压缩列表内的集合元素按照分值从小到大排列。如果我们执行 ZADD price 8.5 apple 5.0 banana 6.0 cherry 命令向有序集合插入元素,该有序集合在内存中的结构如下:

Redis常用的5种数据类型底层结构是怎样构成的_Redis_15

5.2 zset-skiplist

skiplist 编码的有序集合对象使用 zset 结构作为底层实现,一个 zset 结构同时包含一个字典和一个跳跃表。

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

继续介绍之前,我们先了解一下什么是跳跃表。

跳跃表

跳跃表(skiplist)是一种有序的数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。

Redis 的跳跃表由 zskiplistNode 和 zskiplist 两个结构定义。zskiplistNode 结构表示跳跃表节点,zskiplist 保存跳跃表节点相关信息,比如节点的数量,以及指向表头和表尾节点的指针等。

跳跃表节点zskiplistNode

跳跃表节点zskiplistNode 结构定义如下:

typedef struct zskiplistNode {
    // 后退指针
    struct zskiplistNode *backward;
    // 分值
    double score;
    // 成员对象
    robj *obj;
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 跨度
        unsigned int span;
    } level[];
} zskiplistNode;

下图是一个层高为 5,包含 4 个跳跃表节点(1 个表头节点和 3 个数据节点)组成的跳跃表:

Redis常用的5种数据类型底层结构是怎样构成的_Redis_16

有序集合对象的skiplist实现

前面讲过,skiplist 编码的有序集合对象使用 zset 结构作为底层实现。一个 zset 结构同时包含一个字典和一个跳跃表。

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

zset 结构中的 zs1 跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素。

通过跳跃表,可以对有序集合进行基于 score 的快速范围查找。zset 结构中的 dict 字典为有序集合创建了从成员到分值的映射,字典的键保存了成员,字典的值保存了分值。通过字典,可以用 O(1) 复杂度查找给定成员的分值。

假如还是执行 ZADD price 8.5 apple 5.0 banana 6.0 cherry 命令向 zset 保存数据,如果采用 skiplist 编码方式的话,该有序集合在内存中的结构如下:

Redis常用的5种数据类型底层结构是怎样构成的_数据类型_17

6. 总结

总的来说,Redis 底层数据结构主要包括简单动态字符串(SDS)、链表、字典、跳跃表、整数集合和压缩列表六种类型。并且基于这些基础数据结构实现了字符串对象、列表对象、哈希对象、集合对象以及有序集合对象五种常见的对象类型。每一种对象类型都至少采用了 2 种数据编码,不同的编码使用的底层数据结构也不同。

最后

Redis常用的5种数据类型底层结构是怎样构成的_Redis_18

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Redis常用的5种数据类型底层结构是怎样构成的 的相关文章

随机推荐

  • C++入门AcWing讲义:字符串

    字符串是计算机与人类沟通的重要手段 闫学灿 字符与整数的联系 ASCII码 每个常用字符都对应一个 128 127的数字 二者之间可以相互转化 include
  • logback日志级别简介说明

    转自 logback日志级别简介说明 下文笔者将讲述logback的日志级别 如下所示 logback日志级别简介说明 logback有5种级别 其优先级依次如下所示 TRACE lt DEBUG lt INFO lt WARN lt ER
  • python使用字典暴力解析wifi密码

    前言 最近无wifi可用 搜到了很多高质量但是没有密码的WiFi 我在想应该可以用python调用常见的wifi字典包来暴力破解一下这些WiFi 也许可以成功 原理 使用pip install pywifi命令安装pywifi 在代码中设置
  • 基于Python3的接口自动化总结(一)——文件上传接口

    文件上传接口 api POST http host api import 接口入参 file 文件内容 import os import urllib3 import ast def import by rest self file pat
  • 原码,补码,反码的应用

    原码 补码 反码的应用 计算机专业的同学应该知道 课本中对原码反码补码的介绍大致是这样的几句话 正数 正数的原码是他的本身 正数的反码还是等于原码 正数的补码等于他的原码 负数 负数的原码等于该数值的二进制 最高位表示符号位 正数取0 负数
  • 微众银行马智涛:解放数据生产力,以金融科技助力数字经济发展|数字思考者50人...

    钛媒体2022特别专题策划 数字思考者50人 探访中国50位独具代表的数字化思考者 我们理解的 TechThinker 涵盖了中国数字化浪潮中的技术践行者 政策制定者与投资决策者 在这场长达10年的乘风破浪中 我们每个人都在分享技术进步的果
  • docker原理详解

    一 docker简介 docker是一个虚拟环境容器 可以将你的开发环境 代码 配置文件等一并打包到这个容器中 并发布和应用到任意平台中 docker容器和虚拟机都有虚拟隔离的特性 所以一般都会拿虚拟机和docker容器作比较 二 dock
  • C++数据结构 矩阵的转置、镜像及旋转

    C 数据结构 矩阵的转置 镜像及旋转 矩阵介绍 C 中的矩阵是一种二维数组 用于存储数值数据 矩阵可以用于存储图像数据 以及科学和工程计算中的数据 常用于以下场景 数学运算 矩阵乘法 行列式计算 特征值分解等 图像处理 图像缩放 旋转 颜色
  • facebook 审请AppID、AppSecret注意问题

    facebook https developers facebook com https developers facebook com apps 1596024967362217 dashboard 账号 密码 步骤 1 填入应用的包名
  • 文件夹上传到云服务器,文件夹怎么上传到云服务器上

    文件夹怎么上传到云服务器上 内容精选 换一换 RedisShake是一款开源的Redis迁移工具 支持Cluster集群的在线迁移与离线迁移 备份文件导入 但是部署在其他云厂商Redis服务上的Cluster集群数据 由于SYNC PSYN
  • MBR、GPT、LVM分区

    GPT分区 支持大于2T的空间分区UEFI系统 支持128个主分区 parted mklabel New disk label type gpt parted mkpart Partition name vdb1 File system t
  • 搭建zookeeper集群的时候报: JAVA_HOME is not set and java could not be found in PATH.错误

    我在搭建zookeeper集群的时候总是报 JAVA HOME is not set and java could not be found in PATH 的错误 但是我的java环境是没问题的 这个问题困扰我两天了 终于解决了 在此记录
  • 知道这20个正则表达式,能让你少写1,000行代码

    正则表达式 一个十分古老而又强大的文本处理工具 仅仅用一段非常简短的表达式语句 便能够快速实现一个非常复杂的业务逻辑 熟练地掌握正则表达式的话 能够使你的开发效率得到极大的提升 正则表达式经常被用于字段或任意字符串的校验 如下面这段校验基本
  • springcloud微服务系列教程(一) 什么是微服务?为什么要用springcloud?

    前言 这两年 微服务 一词被传得很火 关于微服务的文章在各大技术论坛和博客不断的兴起 不断被转发和评论 技术人员之间的交流也越来越多的青睐微服务这一话题 仿佛不懂微服务就不是一个合格的技术开发者 就连有些公司招聘的要求上都要求应聘者必须掌握
  • Hspice获取节点电流的方法

    如果mos调用时语句为 MNM1 则端口电流输出为 i1 x1 MNM1 表示输出子电路x1里面Device MNM1的第一个节点电流 如果调用语句为 XNM1 用于subckt模型Device的电流输出 则端口电流输出为 isub x1
  • Unity的Text Mesh Pro文字显示重叠处理

    在使用Text Mesh Pro的时候 出现文字重叠 如图 在编辑器内显示是正确的 最后发现是换行造成的 原本的文字是从pdf中复制过来 就会重叠 在记事本中删除换行用回车再次换行就能正确显示
  • CCS5.2.1/5.5 错误-No source available for main()

    1 错误现象 在CCS5 2 1开发环境中从别处导入项目并编译完成后 下载程序时出现如下图所示错误 备注 当前使用的compiler版本为C6000v7 4 14 2 解决办法 进入项目属性设置页面 可以看到当前的调试模式为 Suppres
  • 详解PyCharm配置Anaconda的艰难心路历程

    本文来源于公众号 csdn2299 喜欢可以关注公众号 程序员学府 这篇文章主要介绍了详解PyCharm配置Anaconda的艰难心路历程 小编觉得挺不错的 现在分享给大家 也给大家做个参考 一起跟随小编过来看看吧 在安装好pycharm后
  • 基于JAVA+SpringBoot+VUE的心理健康测试系统的设计与实现

    全网粉丝20W csdn特邀作者 博客专家 CSDN新星计划导师 java领域优质创作者 博客之星 掘金 华为云 阿里云 InfoQ等平台优质作者 专注于Java技术领域和毕业项目实战 文末获取项目下载方式 一 项目背景介绍 随着现代社会的
  • Redis常用的5种数据类型底层结构是怎样构成的

    前言 Redis是一个基于内存中的数据结构存储系统 可以用作数据库 缓存和消息中间件 Redis支持五种常见对象类型 字符串 String 哈希 Hash 列表 List 集合 Set 以及有序集合 Zset 我们在日常工作中也会经常使用它