redis链表结构和简单动态字符串(SDS)

news2024/11/23 21:12:25

1.双向链表

redis中的普通链表是双向链表。通过链表节点结构体可知有全驱节点和后继节点。

1.链表节点和链表

//adlist.h
typedef struct listNode {
    struct listNode *prev;    //前驱节点
    struct listNode *next;    //后继节点
    void *value;                //节点值
} listNode;


//链表结构
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;

链表中的函数指针主要是对节点值的操作,包括复制,析构,判断是否相等。

2.迭代器

此外,Redis还为链表提供迭代器的功能,主要是对链表节点的封装,另外通过链表节点的前驱节点和后继节点,可以轻松的完成向前移动和向后移动。

//adlist.h
typedef struct listIter {
    listNode *next;    //指向的实际节点
    int direction;    //表示迭代器方向,向前还是向后
} listIter;

/* Directions for iterators */
#define AL_START_HEAD 0    //从表头向表尾迭代
#define AL_START_TAIL 1    //从表尾向表头迭代

3.宏定义函数

这些看宏名字就很容易知道其功能的。

#define listLength(l) ((l)->len)
#define listFirst(l) ((l)->head)
#define listLast(l) ((l)->tail)
#define listPrevNode(n) ((n)->prev)
#define listNextNode(n) ((n)->next)
#define listNodeValue(n) ((n)->value)

#define listSetDupMethod(l,m) ((l)->dup = (m))
#define listSetFreeMethod(l,m) ((l)->free = (m))
#define listSetMatchMethod(l,m) ((l)->match = (m))

#define listGetDupMethod(l) ((l)->dup)
#define listGetFreeMethod(l) ((l)->free)
#define listGetMatchMethod(l) ((l)->match)

4.链表的操作

list *listCreate(void); //创建链表
void listRelease(list *list);   //释放链表,会释放内存,就是完全释放链表了
void listEmpty(list *list);     //置空链表,没有释放内存的
list *listAddNodeHead(list *list, void *value); //添加节点在头部
list *listAddNodeTail(list *list, void *value); //在尾部添加节点
list *listInsertNode(list *list, listNode *old_node, void *value, int after);
void listDelNode(list *list, listNode *node);   //删除该节点

listNode *listSearchKey(list *list, void *key);
listNode *listIndex(list *list, long index);
void listRewind(list *list, listIter *li);
void listRewindTail(list *list, listIter *li);
void listRotateTailToHead(list *list);
void listRotateHeadToTail(list *list);
void listJoin(list *l, list *o);

//有关迭代器的操作
listIter *listGetIterator(list *list, int direction); //创建一个链表迭代器,并根据传入的direction来返回链表的头或尾节点
listNode *listNext(listIter *iter);
void listReleaseIterator(listIter *iter);
list *listDup(list *orig);

链表的创建和添加节点这些比较好理解,就不多说。这里主要讲解下有关迭代器的操作。

4.1. listGetIterator

listIter *listGetIterator(list *list, int direction)
{
    listIter *iter;

    if ((iter = zmalloc(sizeof(*iter))) == NULL) return NULL;
    // 根据迭代方向,设置迭代器的起始节点
    if (direction == AL_START_HEAD)    //这个表示迭代器的方向是从头到尾
        iter->next = list->head;    //迭代器对应的节点就是头节点
    else
        iter->next = list->tail;
    iter->direction = direction;    //记录迭代方向
    return iter;
}

4.2.listRewind和listRewindTail

handleClientsWithPendingReadsUsingThreads函数中使用到的。

在列表专用迭代器结构中创建迭代器,listRewind即是通过传入一个listIter变量,从而创建了迭代器,从而可以得到链表中的节点了。

void listRewind(list *list, listIter *li) {
    li->next = list->head;
    li->direction = AL_START_HEAD;
}

void listRewindTail(list *list, listIter *li) {
    li->next = list->tail;
    li->direction = AL_START_TAIL;
}

4.3.listNext

返回迭代器当前所指向的节点,即是iter->next。

这个函数也是在handleClientsWithPendingReadsUsingThreads中有使用到的,通过一个 while((ln = listNext(&li)))循环获取链表中的节点

listNode *listNext(listIter *iter)
{
    listNode *current = iter->next;

    if (current != NULL) {
        //之后要更行迭代器当前所指向的节点
        if (iter->direction == AL_START_HEAD)
            iter->next = current->next;    //保存下一个节点,防止当前节点被删除而造成指针丢失
        else
            iter->next = current->prev;
    }
    return current;
}

4.4.listDup,复制链表

前面说了那么多操作,其实就是为了把它们组合起来使用,handleClientsWithPendingReadsUsingThreads函数就是把他们组合起来使用的。也比如listDup函数,就是上面的综合体。

list *listDup(list *orig)
{
    list *copy;
    listIter iter;
    listNode *node;
    
    //创建链表
    if ((copy = listCreate()) == NULL)
        return NULL;
    // 设置节点值处理函数
    copy->dup = orig->dup;
    copy->free = orig->free;
    copy->match = orig->match;
    listRewind(orig, &iter);    //迭代整个输入链表
    while((node = listNext(&iter)) != NULL) {
        void *value;

        if (copy->dup) {    // 复制节点值到新节点
            value = copy->dup(node->value);
            if (value == NULL) {
                listRelease(copy);
                return NULL;
            }
        } else
            value = node->value;

        if (listAddNodeTail(copy, value) == NULL) {    //将节点添加到链表
            listRelease(copy);
            return NULL;
        }
    }
    return copy;
}

2.简单动态字符串

2.1为什么不用char传统数组来表示字符串

Redis使用一种称为 简单动态字符串(Simple Dynamic Strings,SDS) 来存储字符串和整型数据。 这是为什么呢?

首先想想c语言字符串的一些问题

  • 获取字符串长度strlen,效率低。其时间复杂度是O(N)。
  • 缓冲区溢出问题。使用mepcy(a,b,n)时候,可能a长度不够,就导致溢出。
  • 二进制不安全。在 C 语言中,字符串实际上是使用字符 '\0'终止的一维字符数组。所以在 C 语言获取一个字符串长度,会逐个字符遍历,直到遇到代表字符串结尾的空字符为止。这就会导致二进制安全问题。通俗的讲,C 语言中,用 “\0” 表示字符串的结束,如果字符串本身就有 “\0” 字符,字符串就会被截断,即非二进制安全;若通过某种机制,保证读写字符串时不损害其内容,则是二进制安全。
  • 修改字符串可能需要进行多次分配内存,耗时耗空间。C 字符串本身不记录字符串的长度,所以对于一个包含了 N 个字符的 C 字符串来说, 这个 C 字符串的底层实现总是一个 N+1 个字符长的数组。因为 C 字符串的长度和底层数组的长度之间存在着这种关联性,所以每次增长或缩短一个 C 字符, 程序都总要对保存这个 C 字符串的数组进行一次内存重分配操作。比如执行增长操作之前需要先进行内存重分配,以扩展底层数组的空间大小,如果忘了就会导致缓冲区溢出。然而内存重分配需要执行系统调用,所以它通常是一个比较耗时的操作。

所以 Redis 为 SDS 设计了冗余空间,追加时只要内容不是太大,是可以不必重新分配内存的。

基于以上的问题,Redis就设计了SDS。

2.2 SDS类型

Redis为了灵活的保存不同大小的字符串节省内存空间,设计了不同的结构头sdshdr64sdshdr32sdshdr16sdshdr8和sdshdr5(已不再使用)。

2.2.1SDS结构

//sds.h
typedef char *sds;

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
//还有sdshdr32,sdshdr64没有展示.......................


//这些就是对应结构体中的flags
#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
#define SDS_TYPE_MASK 7    //二进制是0b0111,用来按位与
#define SDS_TYPE_BITS 3

虽然结构头不同,但是他们都具有相同的属性:

  • len:字符数组buf实际使用的大小,也就是字符串的长度(不包括'\0')
  • alloc:字符数组buf分配的空间大小,不包含结束标识符('\0')
  • flags:标记SDS的类型:sdshdr64、sdshdr32、sdshdr16、sdshdr8
  • buf[]:字符数组,用来存储实际的字符数据

有成员len就可以常数时间复杂度得到sds字符串的长度了,也因为以len 属性而不是空字符‘\0’来判断字符串是否结束,那也就可以存放二进制数据了。

redis一些设计点 

Redis使用了_attribute_ (( __packed__))节省内存空间,它可以告诉编译器使用紧凑的方式分配内存,不使用字节对齐的方式给变量分配内存。这也是代表着sdshdr结构体是按1字节对齐(默认结构体会按其所有变量大小的最小公倍数做字节对齐),即结构体的各属性是连续存储的,可以使用flags = s[-1] 这种操作。

在结构体定义中,可以看到最后一个buf数组是没有设置大小的,这种放在结构体中最后一个元素位置并且没有设置大小的数组称为柔性数组,它可以在程序运行过程中进行动态内存分配。

2.3宏定义SDS_HDR_VAR和SDS_HDR

//sds.h
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))

关于 sdshdr##T ,不太懂,在这里理解成它将 sdshdrT 连接在一起,即表示不同的 sdshdr 类型。

关于 SDS_HDR_VAR(T,s), 参数 s 是sdshdr 结构体中的字符串指针,即等价于 buf,参数 T 则是表示不同类型的 sdshdr,取值可以为 8/16/32/64。然后看其实现,struct sdshdr##T *sh 是宏定义的一个变量, void* 是将结果转换为void* 类型,以便 sh 接收。而 (s) - ( sizeof(struct sdshdr##T) )则表示指向结构体变量的地址。首先sizeof(struct sdshdr##T) 计算的大小不包含 buf (柔性数组特点),而 s 的指向的地址就跟在 sdshdr 之后。

通过sdslensdsavail两个函数来看看他们的区别和使用:

SDS_HDR是SDS_HDR(8,s)->len;

SDS_HDR_VAR是直接就使用SDS_HDR_VAR(8,s);sh->alloc->sh->len

所以 SDS_HDR(T, S) 就是返回一个 保存字符串 s 的结构体地址,然后就可以调用其成员。

SDS_HDR_VAR定义一个指向保存字符串 s 的结构体指针,即是先定义一个结构体指针sh然后再使用sh.

注意两者的差别:SDS_HDR_VAR定义变量保存结构体地址,SDS_HDR返回结构体地址。

函数中的s[-1],即表示 sdshdr 结构中的成员 flags,因为其结构体的各属性是连续存储的。通过 flags 我们可以判断 sdshdr 的类型。

//得到字符串长度,即是已使用长度
static inline size_t sdslen(const sds s) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_8:
            return SDS_HDR(8,s)->len;
        case SDS_TYPE_16:
            return SDS_HDR(16,s)->len;
        //...............................
    }
    return 0;
}

//得到还没使用的长度
static inline size_t sdsavail(const sds s) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s);
            return sh->alloc - sh->len;
        }
        case SDS_TYPE_16: {
            SDS_HDR_VAR(16,s);
            return sh->alloc - sh->len;
        }
        //......................
    }
    return 0;
}

还有一些其他关于sds属性的函数

这些函数的实现是相似的。

sdssetlen:修改 sds 的有效长度。

sdsinclen:增加 sds 的有效长度

sdsalloc:返回 sds 的已分配空间大小。也可以通过 sdsavail() + sdslen() 来获取。

sdssetalloc:设置 sds 的已分配空间大小。

//sds.h
//修改 sds 的有效长度
static inline void sdssetlen(sds s, size_t newlen) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_8:
            SDS_HDR(8,s)->len = newlen;
            break;
        case SDS_TYPE_16:
            SDS_HDR(16,s)->len = newlen;
            break;
        //...............
    }
}

//增加 sds 的有效长度
static inline void sdsinclen(sds s, size_t inc) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_8:
            SDS_HDR(8,s)->len += inc;
            break;
        case SDS_TYPE_16:
            SDS_HDR(16,s)->len += inc;
            break;
        //...............
    }
}

//返回 sds 的已分配空间大小。也可以通过 sdsavail() + sdslen() 来获取
/* sdsalloc() = sdsavail() + sdslen() */
static inline size_t sdsalloc(const sds s) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_8:
            return SDS_HDR(8,s)->alloc;
        case SDS_TYPE_16:
            return SDS_HDR(16,s)->alloc;
        //...............
    }
    return 0;
}

//设置 sds 的已分配空间大小
static inline void sdssetalloc(sds s, size_t newlen) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_8:
            SDS_HDR(8,s)->alloc = newlen;
            break;
        case SDS_TYPE_16:
            SDS_HDR(16,s)->alloc = newlen;
            break;
        //.......................
    }
}

2.4.SDS的创建

要创建一个 sds 对象,首先要确认其 sds 结构属于哪一种,要根据字符串长度来选择 sdshdr 。

下面是根据字符串长度来确认 sds 类型的几个相关函数

//sds.c
//通过长度去获取类型
static inline char sdsReqType(size_t string_size) {
    if (string_size < 1<<8)
        return SDS_TYPE_8;
    if (string_size < 1<<16)
        return SDS_TYPE_16;
    //............
}

//得到对应的结构体的大小
static inline int sdsHdrSize(char type) {
    switch(type&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return sizeof(struct sdshdr5);
        case SDS_TYPE_8:
            return sizeof(struct sdshdr8);
        //............................
    }
    return 0;
}

创建函数sdsnewlen

创建sds的思路是:

  1. 根据字符串长度来计算出sds类型,
  2. 申请sdshdr的内存
  3. 根据sdsHdr的类型设置 len, alloc, flag 属性,
  4. 将字符串内容设置到sdsHdr中,并最后添加'\0'结尾
//sds.c
//用于标识, 创建sds时, 不初始化字符数组
extern const char *SDS_NOINIT;

//如果init指针是NULL, 则将内存初始化为0, 如果init是SDS_NOINIT, 则不进行初始化
sds sdsnewlen(const void *init, size_t initlen) {
    void *sh;
    sds s;
        //1.根据长度得出要使用的sds的类型
    char type = sdsReqType(initlen);    
          //如果类型是 5, 或者初始字符串长度是 0 , 则默认给 8
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    int hdrlen = sdsHdrSize(type);    //得到sds类型的大小
    unsigned char *fp; /* flags pointer. */

    assert(initlen + hdrlen + 1 > initlen);     //防止溢出
        //2.分配内存
    sh = s_malloc(hdrlen+initlen+1);    
    if (sh == NULL) return NULL;

    if (init==SDS_NOINIT)    //如果init是SDS_NOINIT, 则不进行初始化
        init = NULL;
    else if (!init)    //如果init指针是NULL, 则将内存初始化为0,
        memset(sh, 0, hdrlen+initlen+1);
    s = (char*)sh+hdrlen;    //字符串的开始位置
    fp = ((unsigned char*)s)-1;    //得到flags
        //3.根据sdsHdr的类型设置 len, alloc, flag 属性,
    switch(type) {
        case SDS_TYPE_5: {
            *fp = type | (initlen << SDS_TYPE_BITS);
            break;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s);    //这是找到结构体初始的地址,并赋值给sh
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_16: {
            SDS_HDR_VAR(16,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_32: {
            SDS_HDR_VAR(32,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_64: {
            SDS_HDR_VAR(64,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
    }
    //4.将字符串内容设置到sdsHdr中
    if (initlen && init)
        memcpy(s, init, initlen);
    s[initlen] = '\0';    //最后添加'\0'结尾
    return s;
}

还有一些其他的创建函数,其就是内部调用了sdsnewlen。

//sds.c
//创建一个空的sds
sds sdsempty(void) {
    return sdsnewlen("",0);
}
//根据给定的C字符串创建sds
sds sdsnew(const char *init) {
    size_t initlen = (init == NULL) ? 0 : strlen(init);
    return sdsnewlen(init, initlen);
}
//复制一个sds
sds sdsdup(const sds s) {
    return sdsnewlen(s, sdslen(s));
}

2.5.预分配SDS内存,可以动态扩容

 sds是简单动态字符串,说明是可以自动扩容的。那sdsMakeRoomFor就是来实现这个功能的。

扩大sds字符串末尾的可用空间,以便调用方确保在调用此函数后,可以覆盖字符串末尾后最多addlen个字节,再加上一个nul项字节。

过程:

  1. 得到剩余的空间大小和sds类型,若剩余的空间>=要扩容的空间,那就直接返回即可。
  2. 若剩余的空间不够,则获取已使用的长度和该结构体起始的地址位置,并计算出新的长度(已使用的长度+要扩容的长度),并进行判断,最终得到新长度。
  3. 根据新长度来确定新的sds类型,并得到结构体大小,若旧类型和新类型是一致的,那就在原有内存上重分配,扩展空间;否则,重新分配空间,再设置好结构体的一下成员数据。
sds sdsMakeRoomFor(sds s, size_t addlen) {  //要扩容addlen长度
    void *sh, *newsh;
        //步骤1
    size_t avail = sdsavail(s);     //得到剩余的空间大小
    size_t len, newlen, reqlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;    //得到类型
    int hdrlen;    

    if (avail >= addlen) return s;

        //步骤2
    len = sdslen(s);    //得到已使用的长度
    sh = (char*)s-sdsHdrSize(oldtype);  //该结构体起始的地址位置
    reqlen = newlen = (len+addlen);     //已使用的长度+要扩容的长度,即是新的总长度
    assert(newlen > len);   /* Catch size_t overflow */
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;

        //步骤3
    type = sdsReqType(newlen);//根据新总长度来确定sds类型

    if (type == SDS_TYPE_5) type = SDS_TYPE_8;

    hdrlen = sdsHdrSize(type);      //得到结构体大小
    assert(hdrlen + newlen + 1 > reqlen);  /* Catch size_t overflow */
    if (oldtype==type) {
        newsh = s_realloc(sh, hdrlen+newlen+1); //重分配,扩展空间
        if (newsh == NULL) return NULL;
        s = (char*)newsh+hdrlen;        //字符串开始的位置
    } else {
        //表示 sds 类型发生了变化,分配新的内存
        newsh = s_malloc(hdrlen+newlen+1);
        if (newsh == NULL) return NULL;
        memcpy((char*)newsh+hdrlen, s, len+1);
        s_free(sh);
        s = (char*)newsh+hdrlen;
        s[-1] = type;
        sdssetlen(s, len);  //设置已使用的长度
    }
    sdssetalloc(s, newlen); //设置总的长度
    return s;
}

那么这个调用该函数不会改变函数返回的sds字符串的长度len,只会改变其可用的剩余空间。

那来看看这个函数的使用例子

sdscatlen是把字符串t追加到s的末尾。类似于c语言的char *strcat(char *dest, const char *src)。

sds sdscatlen(sds s, const void *t, size_t len) {
    size_t curlen = sdslen(s);    //得到已使用的长度,即是字符串长度

    s = sdsMakeRoomFor(s,len);    //分配空间,确保不会越界
    if (s == NULL) return NULL;
    memcpy(s+curlen, t, len);
    sdssetlen(s, curlen+len);
    s[curlen+len] = '\0';
    return s;
}

 sdscpylen函数就类似c语言的 void *memcpy(void *str1, const void *str2, size_t n)。

sds sdscpylen(sds s, const char *t, size_t len) {
    if (sdsalloc(s) < len) {    //总的分配的长度小于要复制的长度len,所以要预分配新空间
        s = sdsMakeRoomFor(s,len-sdslen(s));
        if (s == NULL) return NULL;
    }
    memcpy(s, t, len);
    s[len] = '\0';
    sdssetlen(s, len);
    return s;
}

每次需要字符串增长的时候,就会调用sdsMakeRoomFor,若是需要就进行预分配空间,就可以确保不会出现缓冲区溢出问题。

而通过空间预分配策略,Redis 可以减少连续执行字符串增长操作所需的内存重分配次数。

2.6.字符串释放(惰性空间释放)

 SDS 提供了直接释放内存的方法——sdsfree,该方法通过对 s 的偏移,可定位到 SDS 结构体的首部,然后调用 s_free 释放内存。

void sdsfree(sds s) {
    if (s == NULL) return;
    s_free((char*)s-sdsHdrSize(s[-1]));
}

而 为了优化性能(减少申请内存的开销),SDS 也提供了不直接释放内存,而是通过重置统计值达到清空目的的方法——sdsclear。该方法仅将 SDS 的 len 归零,此处已存在的 buf 并没有真正被清除,新的数据可以覆盖写,而不用重新申请内存。

void sdsclear(sds s) {
    sdssetlen(s, 0);    //设置已使用的长度
    s[0] = '\0';
}

通过惰性空间释放策略,SDS 避免了缩短字符串时所需的内存重分配操作,并为将来可能有的增长操作提供了优化。

2.7. SDS总结

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

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

相关文章

element-ui result 组件源码分享

今日简单分享 result 组件的源码实现&#xff0c;主要从以下三个方面&#xff1a; 1、result 组件页面结构 2、result 组件属性 3、result 组件 slot 一、result 组件页面结构 二、result 组件属性 2.1 title 属性&#xff0c;标题&#xff0c;类型 string&#xff0c;无默…

深入OceanBase内部机制:多租户架构下的资源隔离实现精讲

码到三十五 &#xff1a; 个人主页 心中有诗画&#xff0c;指尖舞代码&#xff0c;目光览世界&#xff0c;步履越千山&#xff0c;人间尽值得 ! 目录 一、什么是OceanBase的多租户二、兼容模式2.1 MySQL 模式2.2 Oracle 模式 三、租户介绍3.1 系统租户3.2 用户租户3.3 Meta 租…

STM32应用开发——使用PWM+DMA驱动WS2812

STM32应用开发——使用PWMDMA驱动WS2812 目录 STM32应用开发——使用PWMDMA驱动WS2812前言1 硬件介绍1.1 WS2812介绍1.1.1 芯片简介1.1.2 引脚描述1.1.3 工作原理1.1.4 时序1.1.5 传输协议 1.2 电路设计 2 软件编程2.1 软件原理2.2 测试代码2.2.1 底层驱动2.2.2 灯效应用 2.3 运…

联想 Y9000P 连接网线速度慢 的 问题解决

参考帖子&#xff1a;求助&#xff0c;拯救者Y9000P 2022 i73060版本 有线网非常慢 无线网正常【笔记本吧】_百度贴吧 问题原因&#xff1a; 网卡驱动版本不对。不能用Win11版&#xff0c;要用Win10版。 问题解决&#xff1a; 1、卸载原驱动 2、下载Win10 驱动 并安装 下载…

探寻大数据思想的主要贡献者与核心内容

引言&#xff1a; 在当今数字化时代&#xff0c;大数据已成为企业和科学研究的关键要素。其背后的思想和概念不仅引领了数据处理和分析的革新&#xff0c;也推动了人类对于信息时代的理解与认知。 大数据思想的起源&#xff1a; 在信息爆炸的时代背景下&#xff0c;大数据思…

QT5-qmediaplayer播放视频及进度条控制实例

qmediaplayer是QT5的播放视频的一个模块。它在很多时候还是要基于第三方的解码器。这里以Ubuntu系统为例&#xff0c;记录其用法及进度条qslider的控制。 首先&#xff0c;制作一个简单的界面文件mainwindow.ui&#xff1a; 然后&#xff0c;下载一个mp4或其他格式视频&#x…

Java设计模式:代理模式的静态和动态之分(八)

码到三十五 &#xff1a; 个人主页 心中有诗画&#xff0c;指尖舞代码&#xff0c;目光览世界&#xff0c;步履越千山&#xff0c;人间尽值得 ! 在软件设计中&#xff0c;代理模式是一种常用的设计模式&#xff0c;它为我们提供了一种方式来控制对原始对象的访问。在Java中&a…

微信开发工具——进行网页授权

微信开发工具——进行网页授权 微信公众平台设置 1.在首页创建好自己的订阅号 网站&#xff1a;https://mp.weixin.qq.com/ 点击立即注册,在选择订阅号&#xff08;个人创建使用&#xff09; 之后按流程填写后&#xff0c;点击设置与开发-------->基本配置&#xff0c;这…

【苍穹外卖】SkyApplication类启动报错

报的这个错 The PoM for com.sky:sky-common:jar:1.0-SNAPSHoT is missing, no dependency information available Maven里重新install一下就好

DOTS:Burst

目录 一&#xff1a;简介 1.1 Getting started 1.2 C# language support 1.2.1 HPC# overview 1.2.1.1 Exception expressions 1.2.1.2 Foreach and While 1.2.1.3 Unsupported C# features in HPC# 1.2.2 Static read-only fields and static constructor support 1.…

Linux 使用管理线程、多线程、分离线程

目录 一、使用线程 1、pthread_create创建线程 2、pthread_join等待线程 主线程获取新线程退出结果 获取新线程退出返回的数组 3、线程异常导致进程终止 4、pthread_exit 5、pthread_cancel 6、主线程可以取消新线程&#xff0c;新线程可以取消主线程吗 二、如何管理线…

open Gauss 数据库-03 openGauss数据库维护管理指导手册

发文章是为了证明自己真的掌握了一个知识&#xff0c;同时给他人带来帮助&#xff0c;如有问题&#xff0c;欢迎指正&#xff0c;祝大家万事胜意&#xff01; 目录 前言 openGauss数据库维护管理 1 操作系统参数检查 1.1 实验介绍 1.2 场景设置及操作步骤 2 openGauss 运…

【三】EMQX 手动创建集群

EMQX 手动创建集群 简介 因为项目中使用到了emqx中间件&#xff0c;所以近期对中间件进行了进一步的研究&#xff0c;每次选用中间件我都会考虑可用性方案&#xff0c;如下是本地实践的记录。 一、部署 1、创建一个 Docker 网络&#xff0c;用于节点间通信。处于同一网络下的…

Golang Channel底层实现原理

1、本文讨论Channel的底层实现原理 首先&#xff0c;我们看Channel的结构体 简要介绍管道结构体中&#xff0c;几个关键字段 在Golang中&#xff0c;管道是分为有缓冲区的管道和无缓冲区的管道。 这里简单提一下&#xff0c;缓冲区大小为1的管道和无缓冲区的管道的区别&…

基于Scala开发Spark ML的ALS推荐模型实战

推荐系统&#xff0c;广泛应用到电商&#xff0c;营销行业。本文通过Scala&#xff0c;开发Spark ML的ALS算法训练推荐模型&#xff0c;用于电影评分预测推荐。 算法简介 ALS算法是Spark ML中实现协同过滤的矩阵分解方法。 ALS&#xff0c;即交替最小二乘法&#xff08;Alte…

练习 16 Web [极客大挑战 2019]LoveSQL

extractvalue(1,concat(‘~’, (‘your sql’) ) )报错注入&#xff0c;注意爆破字段的时候表名有可能是table_name不是table_schema 有登录输入框 常规尝试一下 常规的万能密码&#xff0c;返回了一个“admin的密码”&#xff1a; Hello admin&#xff01; Your password is…

Java获取IP地址以及MAC地址(附Demo)

目录 前言1. IP及MAC2. 特定适配器 前言 需要获取客户端的IP地址以及MAC地址 import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader;public class test {public static void main(String[] args) {try {// 执行命令Process process…

基于springboot实现房屋租赁管理系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现房屋租赁系统演示 摘要 房屋是人类生活栖息的重要场所&#xff0c;随着城市中的流动人口的增多&#xff0c;人们对房屋租赁需求越来越高&#xff0c;为满足用户查询房屋、预约看房、房屋租赁的需求&#xff0c;特开发了本基于Spring Boot的房屋租赁系统。 …

蓝桥杯第八届c++大学B组详解

目录 1.购物单 2.等差素数列 3.承压计算 4.方格分割 5.日期问题 6.包子凑数 7.全球变暖 8.k倍区间 1.购物单 题目解析&#xff1a;就是将折扣字符串转化为数字&#xff0c;进行相加求和。 #include<iostream> #include<string> #include<cmath> usin…

【Python项目】基于django的【医用耗材网上申领系统】

医院信息化是社会发展的一个重要标志&#xff0c;它涉及到医院的各个方面&#xff0c;包括人员和物资&#xff0c;因此受到社会各界的广泛关注。近年来&#xff0c;随着医疗耗材数量的不断增加&#xff0c;如何有效管理这些耗材已经成为管理人员、医生以及社会各方面共同面临的…