当前位置: 首页 > news >正文

Redis3——线程模型与数据结构

Redis3——线程模型与数据结构

本文讲述了redis的单线程模型和IO多线程工作原理,以及几个主要数据结构的实现。

1. Redis的单线程模型

redis6.0之前,一个redis进程只有一个io线程,通过reactor模式可以连接大量客户端;redis6.0为了高并发下网络IO的性能,引入了多个IO线程。但是自始至终,redis进程只有一个命令处理线程,因此redis仍然可以看作一个单线程模型。

默认情况下,redis进程中的线程:

image-20241129104327215

1.1 Redis6.0为什么引入多个IO线程

引入多个IO线程以解决高并发场景下的网络IO瓶颈,具体来说,网络IO中与cpu有关的开销包括:

  1. 系统调用上下文切换。
  2. 内存数据拷贝虽然受内存频率和带宽限制,但仍需要cpu参与。
  3. CPU主导的协议栈处理。

因此,即使网络带宽和内存频率固定,采用多线程io仍然能够提高整体网络IO性能。

1.2 Redis命令处理为什么采用单线程模型

  1. 单线程实现简单,不用考虑线程安全和锁竞争。减少了线程切换开销。
  2. 天然满足事务的原子性、隔离性。
  3. 得益于数据结构的设计,redis基于内存的操作通常非常高效,cpu通常不是性能瓶颈。
  4. redis不同键值对的数据结构不同,多线程场景下,加锁操作复杂,加锁粒度不好控制。

Memcached是一个redis的竞品,采用多线程并发的处理客户请求,其支持的数据类型单一,因此能够方便的对某个局部加锁。

1.3 单线程的耗时操作处理

单线程的局限在于,不能有耗时操作:cpu密集型任务、io密集型任务。

磁盘IO

对于磁盘io任务,redis要么启用子进程来持久化,要么使用其它线程进行异步刷盘。

网络IO

对于网络io任务,redis启用多个io线程,每个线程都采用reactor网络模型,并发处理大量客户端连接。

cpu密集型任务

对于cpu密集型任务,redis需要保证其操作高效:

  1. 数据结构切换:redis采用的数据结构会根据数据量进行切换。当数据量少的时候,redis更注重空间效率,当数据量大时,redis更注重时间效率。
  2. 分治:例如间断遍历
  3. 渐进式的数据迁移:增量式rehash

1.4 单线程如何保证高效

  1. 基于内存的数据库
  2. 数据(键值对)的组织方式:hashtable,时间复杂度O(1),渐进式的rehash
  3. 高效的数据结构与数据结构切换
  4. 高效的reactor模型(redis6.0之前)

2. Redis的IO多线程工作原理

2.1 理论

对于每一个客户端发送来的命令,redis需要进行read、decode、compute、encode、send这五个步骤。

在这里插入图片描述

在redis6.0之前的单线程模式下,主线程通过一个reactor模型串行地完成这5个操作。

加入IO多线程后,主线程会在每次循环时,将IO一部分读写任务分配给IO线程执行。主线程仍然运行一个reactor事件循环,在客户端可读或者可写时将其分配给IO线程。

也就是说现在read、decode以及encode、send都是并行的了。compute操作仍然时串行。

这样设计地合理性在于,一般读写和解析协议操作不会出现竞态条件,至于一个客户端连接相关。而执行命令则需要访问全局数据结构,串行执行可以减少竞态条件。

2.2 业务流程

  1. 在accept建立新连接时,调用acceptCommonHandler创建客户端上下文

  2. 在createClient中,设置读事件的回调函数readQueryFromClient

  3. 读事件发生时,在readQueryFromClient中,读取数据,并用一个sds保存到客户端上下中的querybuf中

    ...
    qblen = sdslen(c->querybuf);
    if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;
    c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
    nread = connRead(c->conn, c->querybuf+qblen, readlen);
    ...
    processInputBuffer(c);
    
  4. 在processInputBuffer中处理读取到的数据

    1. 分割数据包,如果是单行命令执行processInlineBuffer,否则执行processMultibulkBuffer

      if (c->reqtype == PROTO_REQ_INLINE) {if (processInlineBuffer(c) != C_OK) break;...
      } else if (c->reqtype == PROTO_REQ_MULTIBULK) {if (processMultibulkBuffer(c) != C_OK) break;
      } else {serverPanic("Unknown request type");
      }
      
    2. 如果在IO线程中,则标记其为有命令需要执行的状态,否则直接执行

      /* If we are in the context of an I/O thread, we can't really execute the command here. All we can do is to flag the client as one that needs to process the command. */
      if (c->flags & CLIENT_PENDING_READ) {c->flags |= CLIENT_PENDING_COMMAND;break;
      }
      /* We are finally ready to execute the command. */
      if (processCommandAndResetClient(c) == C_ERR) {return;
      }
      
  5. 在主线程中事件循环中,每次循环会调用handleClientsWithPendingReadsUsingThreads函数,它会将仍有数据待读取的客户端分配给IO线程去并行的读取和解析,然后等待这些IO线程执行完毕,之后再调用processCommand处理所有有待处理的命令的客户端。

  6. 处理完毕后,调用addReply将输出写入到output buffer

  7. 当写事件发生时,会调用sendReplyToClient,将数据封装成协议然后发送给客户端。

    /* Write event handler. Just send data to the client. */
    void sendReplyToClient(connection *conn) {client *c = connGetPrivateData(conn);writeToClient(c,1);
    }
    
  8. 另外,主线程也会调用handleClientsWithPendingWritesUsingThreads,将可写的客户端分配给IO线程完成。

2.3 IO线程池

IO线程的工作非常简单,主线程会将待处理的客户端分配个各个io线程,当io线程检测到有客户端分配来时,就根据主线程设置的操作类型取执行。

while(1) {/* Wait for start */for (int j = 0; j < 1000000; j++) {if (getIOPendingCount(id) != 0) break;}/* Give the main thread a chance to stop this thread. */if (getIOPendingCount(id) == 0) {pthread_mutex_lock(&io_threads_mutex[id]);pthread_mutex_unlock(&io_threads_mutex[id]);continue;}serverAssert(getIOPendingCount(id) != 0);/* Process: note that the main thread will never touch our list before we drop the pending count to 0. */listIter li;listNode *ln;listRewind(io_threads_list[id],&li);while((ln = listNext(&li))) {client *c = listNodeValue(ln);if (io_threads_op == IO_THREADS_OP_WRITE) {writeToClient(c,0);} else if (io_threads_op == IO_THREADS_OP_READ) {readQueryFromClient(c->conn);} else {serverPanic("io_threads_op value is unknown");}}listEmpty(io_threads_list[id]);setIOPendingCount(id, 0);}

3. Redis数据结构

在这里插入图片描述

2.1 字典

redis的字典采用hash表实现。

2.1.1 扩容缩容机制
  1. 哈希冲突:采用拉链法

  2. 负载因子:1

  3. 扩容机制:加倍扩容,如果扩容时正在fork(在rdb或aof复写),则阻止扩容,但是若此时负载因子>5,则利用写时复制原理马上扩容。

    // redis v=6.2.16
    if ((dict_can_resize == DICT_RESIZE_ENABLE &&d->ht[0].used >= d->ht[0].size) ||(dict_can_resize != DICT_RESIZE_FORBID &&d->ht[0].used / d->ht[0].size > dict_force_resize_ratio)){return dictExpand(d, d->ht[0].used + 1);}
    
  4. 缩容机制:如果负载因子<0.1,则恰好大于等于used的2的n次幂为新的容量。例如此时负载因子小于0.1,元素总数为17,则新的容量为2的5次方 = 32 > 17。

    // redis v=6.2.16
    int htNeedsResize(dict *dict) {long long size, used;size = dictSlots(dict);used = dictSize(dict);return (size > DICT_HT_INITIAL_SIZE &&(used*100/size < HASHTABLE_MIN_FILL));
    }/* If the percentage of used slots in the HT reaches HASHTABLE_MIN_FILL* we resize the hash table to save memory */
    void tryResizeHashTables(int dbid) {if (htNeedsResize(server.db[dbid].dict))dictResize(server.db[dbid].dict);if (htNeedsResize(server.db[dbid].expires))dictResize(server.db[dbid].expires);
    }
    
2.1.2 渐进式rehash

渐进式rehash:每个dict都有两个hash表,当没有触发扩容时,第二个hash表为NULL,当进行扩容时,则分多次渐进地迁移旧的hash表的每个槽位。有两种迁移策略,这两种策略可以并行。

  1. 将rehash的操作分摊在之后增删改查操作中。例如当进行查询操作时,就迁移一个槽位。当所有的元素都被迁移完毕,就将旧的删除,用新的hash替换旧的hash表。
  2. 对于主hash表,使用定时器,每隔一段时间(100ms)就花费1ms时间执行迁移操作。

增删改查时rehash一个:

每次增删改查,rehash一个槽位:

static void _dictRehashStep(dict *d) {// 判断是否选择这种每次rehash一步的迁移策略if (d->pauserehash == 0) dictRehash(d,1);
}// 增
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{long index;dictEntry *entry;dictht *ht;if (dictIsRehashing(d)) _dictRehashStep(d);...
}
// 删
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {uint64_t h, idx;dictEntry *he, *prevHe;int table;if (d->ht[0].used == 0 && d->ht[1].used == 0) return NULL;if (dictIsRehashing(d)) _dictRehashStep(d);...
}// 查
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);...
}dictEntry *dictGetRandomKey(dict *d)
{dictEntry *he, *orighe;unsigned long h;int listlen, listele;if (dictSize(d) == 0) return NULL;if (dictIsRehashing(d)) _dictRehashStep(d);...
}unsigned int dictGetSomeKeys(dict *d, dictEntry **des, unsigned int count) {unsigned long j; /* internal hash table id, 0 or 1. */unsigned long tables; /* 1 or 2 tables? */unsigned long stored = 0, maxsizemask;unsigned long maxsteps;if (dictSize(d) < count) count = dictSize(d);maxsteps = count*10;/* Try to do a rehashing work proportional to 'count'. */for (j = 0; j < count; j++) {if (dictIsRehashing(d))_dictRehashStep(d);elsebreak;}...
}

空闲时,迁移1ms

// rehash‘ms’毫秒
int dictRehashMilliseconds(dict *d, int ms) {if (d->pauserehash > 0) return 0;long long start = timeInMilliseconds();int rehashes = 0;while(dictRehash(d,100)) {rehashes += 100;if (timeInMilliseconds()-start > ms) break;}return rehashes;
}
// 渐进式rehash,每次rehash 1ms
int incrementallyRehash(int dbid) {/* Keys dictionary */if (dictIsRehashing(server.db[dbid].dict)) {dictRehashMilliseconds(server.db[dbid].dict,1);return 1; /* already used our millisecond for this loop... */}/* Expires */if (dictIsRehashing(server.db[dbid].expires)) {dictRehashMilliseconds(server.db[dbid].expires,1);return 1; /* already used our millisecond for this loop... */}return 0;
}

redis何时执行增量式rehash

在redis的定时任务中,会判断是否开启了自动rehash,如果开启就执行。

void databasesCron(void) {...// 没有fork子进程来持久化的是否进行rehashif (!hasActiveChildProcess()) {...// 如果开启了rehashif (server.activerehashing) {for (j = 0; j < dbs_per_call; j++) {int work_done = incrementallyRehash(rehash_db);...}...}
}

在配置文件中,可以选择是否开启:

activerehashing yes
2.1.3 哈希表的遍历

遍历数据库的原则为:①不重复出现数据;②不遗漏任何数据。熟悉Redis命令的读者应该知道,遍历Redis整个数据库主要有两种方式:全遍历 (例如keys命令)、间断遍历 (hscan命令),这两种方式将在下面进行详细讲解。

2.1.3.1 全遍历

全遍历:一次命令执行就遍历完整个数据库。

全遍历使用普通迭代器或者间断迭代器

typedef struct dictIterator {dict *d;	// 当前遍历的字典long index;	// 当前bucket索引int table, safe;	// hash表id和是否是安全迭代器dictEntry *entry, *nextEntry;	// 当前元素和下一个元素/* unsafe iterator fingerprint for misuse detection. */unsigned long long fingerprint;	// 由hash表中所有元素生成的指纹,如果遍历过程中元素发生变化,就能够根据此值检查出来
} dictIterator;

普通迭代器与安全迭代器的区别在于,普通迭代器迭代期间允许rehash,但是如果数据元素发生变动导致fingerprint发生变化,就会发出异常。而安全迭代器在迭代期间不允许进行渐进式rehash。

一次迭代的操作如下:

dictEntry *dictNext(dictIterator *iter)
{while (1) {if (iter->entry == NULL) {dictht *ht = &iter->d->ht[iter->table];if (iter->index == -1 && iter->table == 0) {// 初始时,如果时安全迭代器,就禁止rehash操作,否则就计算指纹if (iter->safe)dictPauseRehashing(iter->d);elseiter->fingerprint = dictFingerprint(iter->d);}iter->index++;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->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;
}
2.1.3.2 间断遍历

间断遍历:每次命令执行只取部分数据,分多次遍历。

间断遍历期间允许进行渐进式rehash,它通过采用reverse binary iteration来保证不重复数据也不遗漏数据。

由于扩容及缩容正好为整数倍增长或减少的原理,根据这个特征,很容易就能推导出同一个节点的数据扩容/缩容后在新的Hash表中的分布位置,例如一个hash表大小为4,索引为0的元素,扩容后新的索引可能为0或者4。一个hash表大小为8,索引为6的元素,缩容后新的索引为2。

根据这个规律,可以设计一种迭代顺序规则,即游标值v变化的规则:

v |= ~mask;
v = rev(v);	// 二进制逆转
v++;
v = rev(v); // 二进制逆转

两次遍历间隔期间发生扩容:举例,如果size为4,则mask为二进制011,遍历顺序为00, 10, 01, 11,对应十进制索引值0, 2, 1, 3。假如遍历到1时发生了扩容,则mask变为0111,则遍历顺序变为00, 10, 001, 101, 011, 111,对应十进制索引值为0, 2, 1, 5, 3, 7,少了4和6。正好扩容前的0,2中的元素可能被放入4,6。这样就防止了重复遍历。

同理,如果两个遍历地间隔期间发生缩容,这种算法也可以避免重复护着遗漏元素。

两次遍历间隔期间发送缩容:举例,如果size为8,则mask为二进制0111,遍历顺序为000,100,010,110,001,101,011,111,对应十进制0,4,2,6,1,5,3,7。如果在遍历1时,发生了缩容,则mask为011,遍历顺序就变为000,100,010,110,01,11,对应十进制为0,4,2,6,1,3,少了5和7,因为缩容前5和7的元素在缩容后会被放入1和3。这样就防止了重复和遗漏。

这种高位累加的游标变更算法巧妙地利用扩容缩容前后元素索引值地变化规律,设计出游标变化规则,从而避免了重复遍历或漏遍历。

在redis中可以用scan和hscan进行间断遍历:

127.0.0.1:6379> scan 0 match * count 1
1) "2"
2) 1) "yy"2) "person"
127.0.0.1:6379> scan 2 match * count 1
1) "1"
2) 1) "dsaa"
127.0.0.1:6379> scan 1 match * count 1
1) "3"
2) 1) "name"
127.0.0.1:6379> scan 3 match * count 1
1) "0"
2) (empty array)

2.2 跳表

请参考我之前的博客高级数据结构——跳表skiplist。

2.3 压缩列表

压缩列表ziplist本质上就是一个字节数组,一段连续的内存,是Redis为了节约内存而设计的一种线性数据结构,可以包含多个元素,每个元素可以是一个字节数组或一个整数。

redis6.0之后,逐渐用listpack取代ziplist。redis7.0之后完全移除了ziplist的支持。

Redis的有序集合、散列和列表都直接或者间接使用了压缩列表。当有序集合或散列表的元素个数比较少,且元素都是短字符串时,Redis便使用压缩列表作为其底层数据存储结构。列表使用快速链表(quicklist)数据结构存储,而快速链表就是双向链表与压缩列表的组合。

2.4 quicklist

quicklist由双向链表adlist和ziplist结合而成。Redis中对quciklist的注释为A doubly linked list of ziplists。顾名思义,quicklist是一个双向链表,链表中的每个节点是一个ziplist结构。

结构体:

typedef struct quicklist {quicklistNode *head;quicklistNode *tail;unsigned long count;        /* total count of all entries in all ziplists */unsigned long len;          /* number of quicklistNodes */int fill : QL_FILL_BITS;              /* fill factor for individual nodes */unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off */unsigned int bookmark_count: QL_BM_BITS;quicklistBookmark bookmarks[];
} quicklist;typedef struct quicklistNode {struct quicklistNode *prev;struct quicklistNode *next;unsigned char *zl;unsigned int sz;             /* ziplist size in bytes */unsigned int count : 16;     /* count of items in ziplist */unsigned int encoding : 2;   /* RAW==1 or LZF==2 */unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */unsigned int recompress : 1; /* was this node previous compressed? */unsigned int attempted_compress : 1; /* node can't compress; too small */unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;

2.5 动态字符串

2.5.1 对象结构

Redis没有直接使用C语言传统的字符串表示(以空字符结尾的字符数组,以下简称C字符串),而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。

下面是redis6.2中sds对象结构:

// 可以存储小于32字节的字符串
struct __attribute__ ((__packed__)) sdshdr5 {unsigned char flags; /* 3 lsb of type, and 5 msb of string length */char buf[];
};
// 可以存储小于256字节的字符串
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[];
};
struct __attribute__ ((__packed__)) sdshdr32 {uint32_t len; /* used */uint32_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {uint64_t len; /* used */uint64_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[];
};

为了节省空间,不同大小的字符串对应的头部大小应该不同,对于一个小字符串,没有必要使用64位整数来保存字符串长度。

sds中的len表示字符串长度,alloc表示总的空间大小(不包含头部和末尾空白字符)。与C风格的字符串相比,这样设计的好处是:

  1. 内存安全:不依赖末尾空白字符,这样可以防止内存越界等问题。
  2. 兼容二进制数据

末尾仍然以’\0’结尾,可以兼容C风格字符串。

flags字段用来标识类型,由于一共有5种类型,至少需要3位来存储,于是对于最短的字符串来说,剩余5位可以用来存储字符串的长度,可以存储最长31字节的字符串。

**__attribute__ ((_packed_))**需要注意,一般情况下,结构体的整体大小必须是其成员最大对齐要求的倍数。最大对齐要求通常是结构体中对齐要求最高的成员的对齐数,而用packed修饰后,结构体则变为按1字节对齐。

使用该编译器扩展属性后,sdshdr32减少了3字节的填充。

struct sdshdr32_no_packed {uint32_t len; /* used */uint32_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[];
};int main()
{// sizeof sdshdr32:9printf("sizeof sdshdr32:%lld\n", sizeof (struct sdshdr32));// sizeof sdshdr32_no_packed:12printf("sizeof sdshdr32_no_packed:%lld\n", sizeof (struct sdshdr32_no_packed));return 0;
}
2.5.2 柔性数组

buf是一个柔性数组成员(flexible array member),也叫伸缩性数组成员,只能被放在结构体的末尾。包含柔性数组成员的结构体,通过malloc函数为柔性数组动态分配内存。

柔性数组的限制:

  1. 只能是结构体的最后一个成员
  2. 不能是结构体的唯一一个成员
  3. 运行时要手动为包含柔性数组的结构体分配足够的连续内存

使用柔性数组的好处:

  1. 和sds结构体成员共用一个连续内存,减少动态内存分配和间接访问,效率更高。

  2. 通过首地址偏移方便得到结构体的首地址,进而获取其它成员变量。

    首地址偏移示例:

    /* Free an sds string. No operation is performed if 's' is NULL. */
    void sdsfree(sds s) {if (s == NULL) return;s_free((char*)s-sdsHdrSize(s[-1]));
    }
    
2.5.3 相关面试题
  1. 为什么sds小于等于44字节时采用embstr编码,大于44字节时采用raw编码?

    embstr编码将redisObjectsds存储在一个连续内存中,而raw编码将两者分开存储。redisObject的大小为16字节,44字节对应sdshdr8占用3字节,还有1字节的空白字符,所以44字节的sds实际占用64字节。

    首先,cache line大小为64字节,如果整个sds结构体小于等于64字节,就能够利用缓存加快访问速度,此时采用压缩编码效率最高。

    其次,大多数内存分配器都是按照2的整数次幂的大小分配内存,选择64字节可以减少内存碎片。

    综上,当字符串小于等于44字节时,采用嵌入编码能够利用缓存行和减少内存碎片。

  2. 为什么其它类型中数据结构切换都有一个元素长度不超过64字节这个分界线?

    首先当字符串总长度小于等于64字节时,采用的是embstr编码,将sds嵌入到redisObject中,整个元素占用一段连续的内存空间。这可以节省内存空间,减少内存分配开销。

    此外,cache line大小为64字节,元素长度不超过64字节可以更高的利用缓存行加速访问。

    数据结构切换之前,为了节约内存,通常采用压缩列表,而元素长度不超过64字节的字符串采用embstr也是为了节约内存。

学习参考

学习更多相关知识请参考零声 github。

相关文章:

Redis3——线程模型与数据结构

Redis3——线程模型与数据结构 本文讲述了redis的单线程模型和IO多线程工作原理&#xff0c;以及几个主要数据结构的实现。 1. Redis的单线程模型 redis6.0之前&#xff0c;一个redis进程只有一个io线程&#xff0c;通过reactor模式可以连接大量客户端&#xff1b;redis6.0为了…...

JavaScript(JS)的对象

目录 1.array 数组对象 2.String 字符串对象 3.JSON 对象&#xff08;数据载体&#xff0c;进行数据传输&#xff09; 4.BOM 浏览器对象 5.DOM 文档对象&#xff08;了解&#xff09; 1.array 数组对象 定义方式1&#xff1a;var 变量名 new Array(元素列表); 定义方式…...

计算机毕业设计Python+LSTM天气预测系统 AI大模型问答 vue.js 可视化大屏 机器学习 深度学习 Hadoop Spark

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…...

二阶信息在机器学习中的优化;GPTQ算法利用近似二阶信息;为什么要求近似二阶(运算量大,ReLu0点不可微)

目录 二阶信息在机器学习中的优化 GPTQ算法利用近似二阶信息来找到合适的量化权重 详细解释 举例说明 近似二阶信息 定义与解释 举例说明 总结 为什么要求近似二阶(运算量大,ReLu0点不可微) 计算复杂性 精度需求 实际应用场景中的权衡 二阶信息在机器学习中的优…...

Spring事务管理学习记录

一、概念 事务&#xff08;Transaction&#xff09;是指一组操作的集合&#xff0c;这些操作要么全部成功&#xff0c;要么全部失败。事务的四大特性&#xff08;ACID&#xff09;确保了数据的完整性和一致性&#xff1a; 原子性&#xff08;Atomicity&#xff09;&#xff1a…...

Linux locate 命令详解

简介 locate 命令用于通过查询预构建的数据库来快速搜索文件和目录&#xff0c;该数据库包含来自文件系统的索引文件路径。它比 find 之类的命令要快得多&#xff0c;因为它不会实时搜索整个文件系统。 关键概念 locate 命令依赖于数据库&#xff0c;通常位于 /var/lib/mloca…...

uniapp手机端一些坑记录

关于 z-paging-x 组件&#xff0c;在ios上有时候通过弹窗去粗发它reload时会触发闪退&#xff0c;可能是弹框插入进去导致的DOM 元素已经被移除或者不可用&#xff0c;解决办法是加上他自带属性 :showRefresherWhenReload"true" 加上showRefresherWhe…...

快速排序算法

快速排序是一种非常高效的排序算法&#xff0c;采用分治策略来对一个数组进行排序。它由C. A. R. Hoare在1960年提出。快速排序的基本思想是通过一趟排序将待排记录分割成独立的两部分&#xff0c;其中一部分记录的关键字均比另一部分的关键字小&#xff0c;然后分别对这两部分…...

CSS定位

定位 其中&#xff0c;绝对定位和固定定位会脱离文档流 设置定位之后&#xff1a;可以使用四个方向值进行调整位置&#xff1a;left、top、right、bottom 相对定位 温馨提示 设置定位之后&#xff0c;相对定位和绝对定位他是相对于具有定位的父级元素进行位置调整&#xff0c…...

追加docker已运行容器添加或修改端口映射方法

docker run可以指定端口映射 【】docker run -d -p 80:80 --name name 但是容器一旦生成&#xff0c;就没有一个命令可以直接修改。通常间接的办法是&#xff0c;保存镜像&#xff0c;再创建一个新的容器&#xff0c;在创建时指定新的端口映射。 【】 docker stop A 【】 doc…...

53 基于单片机的8路抢答器加记分

目录 一、主要功能 二、硬件资源 三、程序编程 四、实现现象 一、主要功能 首先有三个按键 分别为开始 暂停 复位&#xff0c;然后八个选手按键&#xff0c;开机显示四条杠&#xff0c;然后按一号选手按键&#xff0c;数码管显示&#xff13;&#xff10;&#xff0c;这…...

ubuntu多版本安装gcc

1.ubuntu安装gcc 9.3.1 $ sudo apt update $ sudo apt install gcc-9 g-9 二、配置GCC版本 安装完成后&#xff0c;需要使用update-alternatives命令来配置GCC版本。这个命令允许系统在多个安装的版本之间进行选择 1.添加GCC 9.3.1到update-alternatives管理 $ sudo update-a…...

异步处理优化:多线程线程池与消息队列的选择与应用

目录 一、异步处理方式引入 &#xff08;一&#xff09;异步业务识别 &#xff08;二&#xff09;明确异步处理方式 二、多线程线程池&#xff08;Thread Pool&#xff09; &#xff08;一&#xff09;工作原理 &#xff08;二&#xff09;直面优缺点和适用场景 1.需要快…...

音视频技术扫盲之预测编码的基本原理探究

预测编码是一种数据压缩技术&#xff0c;广泛应用于图像、视频和音频编码等领域。其基本原理是利用数据的相关性&#xff0c;通过对当前数据的预测和实际值与预测值之间的差值进行编码&#xff0c;从而实现数据压缩的目的。 一、预测编码的基本概念 预测编码主要包括预测器和…...

计算机毕业设计SpringCloud+大模型微服务高考志愿填报推荐系统 高考大数据 SparkML机器学习 深度学习 人工智能 Python爬虫 知识图谱

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…...

AIGC训练效率与模型优化的深入探讨

文章目录 1.AIGC概述2.AIGC模型训练效率的重要性3.模型优化的概念与目标4.模型优化策略4.1 学习率调节4.2 模型架构选择4.3 数据预处理与增强4.4 正则化技术4.5 量化与剪枝 5.代码示例6.结论 人工智能领域的发展&#xff0c;人工智能生成内容&#xff08; AIGC&#xff09;越来…...

《深入浅出HTTPS》读书笔记(13):块密码算法之迭代模式(续)

CTR模式 每次迭代运算的时候要生成一个密钥流&#xff08;keystream&#xff09;。 各个密钥流之间是有关系的&#xff0c;最简单的方式就是密钥流不断递增&#xff0c;所以才叫作计数器模式。 ◎在处理迭代之前&#xff0c;先生成每个密钥流&#xff0c;有n个数据块&#xff0…...

定时任务删除MongoDB历史数据

前言 MongoDB数据过多&#xff0c;导致存储成本飙升&#xff0c;为了降低成本&#xff0c;需要将历史数据删除。 删除逻辑 添加配置文件控制删除逻辑 syncconfig:deleteMongoConfig:#同步状态&#xff0c;true同步&#xff0c;false不同步syncStatus: true#删除数据的时间&…...

Simulink的SIL软件在环测试

以基于模型的设计&#xff08;MBD&#xff09;的软件开发时&#xff0c;需要进行SIL&#xff08;软件在环测试&#xff09;。SIL测试就是在PC上验证模型是否与代码功能一致。在项目开展中&#xff0c;用在需要将控制器生成移植到硬件前&#xff0c;把控制器的模块生成代码&…...

你能穿过迷雾看清一切吗

很多事情的真相有谁知道&#xff1f; 我和家里人被看不见的攻击攻击和操控&#xff0c;失控和无助状态被假鬼录制&#xff0c;然后安排某些不知道整个实际情况和真相的人去听&#xff0c;间接歪曲了整件事情。 各种高科技配合和各种脑功能操控伤害是一般人想都想不到的&#…...

8 设计模式之简单工厂模式

设计模式是软件开发中的一套通用解决方案&#xff0c;而简单工厂模式则是最基础、最常用的一种创建型模式。在这篇博客中&#xff0c;我将为大家详细介绍简单工厂模式的概念、优缺点&#xff0c;以及通过一个饮料制作的案例&#xff0c;帮助大家更好地理解和应用这种模式。 一、…...

一步一步写线程之十六线程的安全退出之一理论分析

一、多线程的开发 多线程的开发&#xff0c;在实际场景中几乎是无法避开的。即使是前端看似没有使用线程&#xff0c;其实在底层的框架中也使用了线程进行了支撑。至少到现在&#xff0c;不管是协程还是其它什么新的编程方式&#xff0c;仍然无法撼动线程的主流地位。 多线程的…...

《Learn Three.js》学习(4) 材质

前言&#xff1a; 材质为scene中物体的皮肤&#xff0c;有着不同的特性和视觉效果。 材质的共有属性&#xff1a; 基础属性&#xff1a; 融合属性&#xff1a; 融合决定了我们渲染的颜色如何与它们后面的颜色交互 高级属性&#xff1a; 与WebGL内部有关 简单材质&#xff1…...

【QNX+Android虚拟化方案】128 - QNX 侧触摸屏驱动解析

【QNX+Android虚拟化方案】128 - QNX 侧触摸屏驱动解析 一、QNX 侧触摸屏配置基于原生纯净代码,自学总结 纯技术分享,不会也不敢涉项目、不泄密、不传播代码文档!!! 本文禁止转载分享 !!! 汇总链接:《【QNX+Android虚拟化方案】00 - 系列文章链接汇总》 本文链接:《【…...

Oracle SCN与时间戳的映射关系

目录 一、基本概述 二、相关操作 三、参考文档 一、基本概述 Oracle 数据库中的 SYS.SMON_SCN_TIME 表是一个关键的内部表&#xff0c;主要用于记录过去时间段中SCN与具体的时间戳之间的映射关系。这种映射关系可以帮助用户将 SCN 值转换为可读性更强的时间戳&#xff0c;从而…...

量化交易系统开发-实时行情自动化交易-8.2.发明者FMZ平台

19年创业做过一年的量化交易但没有成功&#xff0c;作为交易系统的开发人员积累了一些经验&#xff0c;最近想重新研究交易系统&#xff0c;一边整理一边写出来一些思考供大家参考&#xff0c;也希望跟做量化的朋友有更多的交流和合作。 接下来会对于发明者FMZ平台介绍。 发明…...

HBU深度学习作业9

1. 实现SRN &#xff08;1&#xff09;使用Numpy实现SRN import numpy as npinputs np.array([[1., 1.],[1., 1.],[2., 2.]]) # 初始化输入序列 print(inputs is , inputs)state_t np.zeros(2, ) # 初始化存储器 print(state_t is , state_t)w1, w2, w3, w4, w5, w6, w7, …...

关于otter监控告警使用

一、背景 近期在使用otter完成单机房单向同步时&#xff0c;常常遇到channel假死的情况&#xff0c;导致Pipeline同步停止&#xff0c;系统表数据同步停止&#xff0c;影响生产环境用户数据查询相关的功能&#xff0c;虽然事后能够通过停channel后再启用channel重新启用…...

复合查询和内外连接

文章目录 1. 简单查询2. 多表查询2.1 显示雇员名、雇员工资以及所在部门的名字2.2 显示部门号为10的部门名&#xff0c;员工名和工资2.3 显示各个员工的姓名&#xff0c;工资&#xff0c;及工资级别 3. 自连接4. 子查询4.1 where后的子查询4.1.1 单行子查询4.1.2 多行子查询 (i…...

动态规划【C++优质版】

&#xff08;本文未经作者书面允许&#xff0c;禁止以任何形式传播&#xff08;包括但不限于转载&#xff0c;翻译……&#xff09;如需引用 请标注原作者&#xff09; Intro&#xff1a; 动态规划是一种用于解决优化问题的算法策略。在 C 中&#xff0c;它主要用于处理那些具…...

柔性芯片:实现万物互联的催化剂

物联网 (IoT) 市场已经非常成熟&#xff0c;麦肯锡预测&#xff0c;物联网将再创高峰&#xff0c;到 2030 年将达到 12.5 万亿美元的估值。然而&#xff0c;万物互联 (IoE) 的愿景尚未实现&#xff0c;即由数十亿台智能互联设备组成&#xff0c;提供大规模洞察和效率。 究竟是…...

【分布式】分布式事务

目录 1、事务的发展 2、本地事务 &#xff08;1&#xff09;如何保障原子性和持久性&#xff1f; &#xff08;2&#xff09;如何保障隔离性&#xff1f; 2、全局事务 &#xff08;1&#xff09;XA事务的两段式提交 &#xff08;2&#xff09;XA事务的三段式提交…...

nacos安装部署

nacos安装部署 1.安装nacos 1.安装nacos nacos的安装很简单下载后解压启动即可&#xff0c;但是在启动前请确保jdk环境正常&#xff1b; 1.首先我们要下载nacos安装包&#xff1a;可以到官网下载&#xff0c;注意我这里使用的是2.1.0版本&#xff1b; 2.下载完成后&#xff0…...

git 上传代码时报错

在上传代码时&#xff0c;显示无法上传 PS E:\JavaWeb\vue3-project> git push To https://gitee.com/evening-breeze-2003/vue3.git! [rejected] master -> master (non-fast-forward) error: failed to push some refs to https://gitee.com/evening-breeze-20…...

【C++】数字位数提取:从个位到十位的深入分析与理论拓展

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C 文章目录 &#x1f4af;前言&#x1f4af;第一题&#xff1a;提取个位数解法代码解法分析代码优化拓展思考&#xff1a;取模运算的普适性 &#x1f4af;第二题&#xff1a;提取十位数题目解读与思路分析方法一&…...

数据结构--二叉树的创建和遍历

目录 引入 定义 性质 二叉树的创建 迭代法 注意事项&#xff1a; 递归法 注意事项&#xff1a; 二叉树的遍历 深度优先 广度优先 先序遍历&#xff08;前序遍历&#xff09; 中序遍历 后序遍历 层序遍历 查找树结构中是否存在某数值 方法一&#xff1a; 方法…...

CEF127 编译指南 Linux篇 - 安装Git和Python(三)

1. 引言 在前面的文章中&#xff0c;我们已经完成了基础开发工具的安装和配置。接下来&#xff0c;我们需要安装两个同样重要的工具&#xff1a;Git 和 Python。这两个工具在 CEF 的编译过程中扮演着关键角色。Git 负责管理和获取源代码&#xff0c;而 Python 则用于运行各种编…...

计算机网络的类型

目录 按覆盖范围分类 个人区域网&#xff08;PAN&#xff09; 局域网&#xff08;LAN&#xff09; 城域网&#xff08;MAN&#xff09; 4. 广域网&#xff08;WAN&#xff09; 按使用场景和性质分类 公网&#xff08;全球网络&#xff09; 外网 内网&#xff08;私有网…...

Web入门(学习笔记)

Web入门 文章目录 Web入门SpringSpringBootWeb入门HTTP协议HTTP-概述HTTP特点 HTTP-请求协议HTTP-请求数据格式 HTTP-响应协议响应状态码 HTTP-协议解析 Web服务器-TomcatWeb服务器简介基本使用Tomcat文件夹目录解析常见问题Tomcat部署项目 入门程序解析**内嵌的Tomcat服务器**…...

mind+自定义库编写注意事项

在mind图形化命令编写中&#xff0c;main.ts 文件是通过图形化编程工具生成 C 代码&#xff0c;然后将生成的 C 代码上传到 Arduino Uno 上执行。 这些由main.ts定义的图形化代码通过生成的代码&#xff0c;需要包含调用arduinoc/libraries文件夹的*.h和*.cpp文件&#…...

jQuery零基础入门速通(上)

大家好&#xff0c;我是小黄。 在前端开发的世界里&#xff0c;jQuery以其简洁的语法和强大的功能&#xff0c;一直是许多开发者手中的利器。它不仅简化了HTML文档遍历和操作、事件处理、动画以及Ajax交互&#xff0c;还极大地提高了开发效率。本文将带你走进jQuery的世界&…...

计算机网络-Wireshark探索IPv4

使用工具 Wiresharkcurl(MacOS)traceroute: This lab uses “traceroute” to find the router level path from your computer to a remote Internet host. traceroute is a standard command-line utility for discovering the Internet paths that your computer uses. It i…...

【05】Selenium+Python 两种文件上传方式(AutoIt)

上传文件的两种方式 一、input标签上传文件 可以用send_keys方法直接上传文件 示例代码 input标签上传文件import time from selenium import webdriver from chromedriver_py import binary_path # this will get you the path variable from selenium.webdriver.common.by i…...

《构建 C++分布式计算框架:赋能人工智能模型并行训练》

在人工智能迅猛发展的今天&#xff0c;模型训练所需的计算资源呈指数级增长。为了高效地支持人工智能模型在多节点、多 GPU/CPU 集群上的并行训练&#xff0c;基于 C构建分布式计算框架成为了关键之举。 一、分布式计算框架的核心意义 随着人工智能模型复杂度的不断攀升&…...

分支定价算法Branch and price

分支定价算法是进阶版的列生成算法&#xff0c;是用来专门求解整数规划问题的。 目录 1.整数规划与线性规划的关系 2.限制主问题&#xff08;RLMP&#xff09;求得整数解 3.B&P用法&#xff1a;以VRPTW为例 列生成是求解线性规划问题的算法&#xff0c;通过不断往限制主…...

【信息系统项目管理师】第5章:信息系统工程 考点梳理

文章目录 5.1 软件工程5.1.1 架构设计1、软件架构风格2、软件架构评估 5.1.2 需求分析1、需求的层次2、需求过程&#xff08;重点&#xff09;3、UML事务、关系和视图4、面向对象分析 5.1.3 软件设计1、结构化设计2、面向对象设计3、设计模式 5.1.4 软件实现1、软件配置管理2、…...

kdump调试分析(适用于麒麟,ubuntu等OS)

1. kdump基本原理 1.1 内核崩溃处理机制 当 Linux 系统内核发生崩溃时,通常会触发 panic,系统停止正常运行。Kdump 在这种情况下: 使用一个备用的内核(称为 crash kernel)来启动最小化的环境。从崩溃的主内核中复制内存内容(转储文件)。将转储文件保存到预定义的存储位…...

Ubuntu在NVME硬盘使用Systemback安装记录

问题 使用Systemback重装系统找不到NVME硬盘。 0.使用Systemback制作iso后&#xff0c;制作启动盘 1.插入启动盘进入live mode模式 2.安装gparted sudo apt-get update sudo apt-get install gparted3.使用gparted对待分区硬盘进行分区 gparted按照你希望的分区方式分区即…...

C++多态的实现原理

【欢迎关注编码小哥&#xff0c;学习更多实用的编程方法和技巧】 1、类的继承 子类对象在创建时会首先调用父类的构造函数 父类构造函数执行结束后&#xff0c;执行子类的构造函数 当父类的构造函数有参数时&#xff0c;需要在子类的初始化列表中显式调用 Child(int i) : …...

com.github.gavlyukovskiy依赖是做什么的呢?

p6spy-spring-boot-starter 是一个Spring Boot的starter&#xff0c;用于集成P6Spy库。P6Spy是一个开源的数据库连接池代理工具&#xff0c;它可以拦截和记录所有的SQL语句及其执行时间&#xff0c;从而帮助开发者进行SQL性能分析和调试。 功能概述 SQL日志记录&#xff1a; P…...