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

【Redis】初识 Redis 与基础数据结构

目录

1. 初识 Redis

1.1. 单机架构与分布式架构

1.2. Redis 的特性

1.3. Redis 的主要应用场景

1.4. Redis 不适合的应用场景

1.5. 安装并启动 Redis

1.6 Redis 的基本全局命令

2. Redis 的单线程架构

3. 数据结构

3.1 概览

3.2 String

3.3 Hash

3.4 List

3.5 Set

3.6 Zset

3.7 渐进式遍历

4. 数据库管理


1. 初识 Redis

        Redis 在分布式系统中发挥最大威力。单机架构也可以使用 Redis 作为缓存,降低 DB 压力,但分布式系统的核心痛点(高可用、高并发、大容量、跨节点协同),才是 Redis 真正能 “发力” 的地方。

        Redis 是客户端服务器结构的程序,不同主机间通过网络通信实现变量的跨主机调用。

        Mysql 将数据存储在硬盘中,Redis 将数据存储在内存中。计算机读取内存的速度远快于读取硬盘的速度。

        但 Redis 的存储空间是有限的。更优的方案,是将 Redis 和 Mysql 结合使用,Redis 仅存储热点数据。通常情况下,热点数据可以满足大部分的访问需求。

        并且,引入 Redis 会大大提升系统的复杂度,从而涉及到数据同步等新问题。

1.1. 单机架构与分布式架构

        单机程序中,服务器主要包括两方面内容,第一是应用服务,第二是数据库服务。

        应用服务就是我们使用 Java Spring 编写的 HTTP 服务器。

        数据库服务一般就是指我们电脑上的 Mysql 服务器,负责存储和组织数据。Mysql 是一个客户端服务器结构的程序,我们在应用服务中编写 SQL 语句,就是在编写 Mysql 的客户端。

        今日,单机架构依然被大多数企业采用。因为当今的计算机硬件发展水平很高,即使是一台主机也可以应对很高的并发。   

1.2. Redis 的特性

        1. 速度快。Redis 将数据存储于内存,使用单线程。

        2. 基于键值对组织数据结构。

        3. 稳定。

        4. 支持数据持久化。

1.3. Redis 的主要应用场景

        1. 缓存。加速数据访问速度,降低后端数据的压力。

        2. 排行榜系统。Redis 提供了较为复杂的数据结构来构建列表。

        3. 计数器。网站的播放数,浏览数需要经常自增。Redis 对计数提供性能强大的支持。

        4. 社交网络的点踩收藏等功能。

1.4. Redis 不适合的应用场景

        Redis 不适合存储大规模的数据,内存空间需要合理使用。

1.5. 安装并启动 Redis

        环境:Linux,Ubuntu 20

        在 Ubuntu 系统中,推荐以 systemd 托管的方式来启动 Redis 服务器。核心的启动 / 停止指令需通过 systemctl 工具执行(需管理员权限,通常加 sudo),常用指令如:

        启动 --> sudo systemctl start redis-server

        停止 --> sudo systemctl stop redis-server

        重启 --> sudo systemctl restart redis-server

        查看运行状态 --> sudo systemctl status redis-server

        自启(生产环境必备)--> sudo systemctl enable redis-server

        Redis 是一个客户端服务器结构的程序,服务器启动后监听 6379 端口,等待客户端连接。客户端使用 redis-cli -h{host} -p{port} 指令发起与服务器的连接。由于目前连接的 Redis 服务位于 127.0.0.1,端口也使用默认的 6379,所以可以省略 -h{host} -p{port}。

1.6 Redis 的基本全局命令

        keys pattern

        注:返回所有满足 pattern 的 key。支持 * ? [ ] 等通配符。O(N) 时间复杂度。一般在实际开发中禁止使用 keys,因为其需遍历全部的 key,而 Redis 是一个单线程的服务器,一旦花费大量时间在遍历上,将无法为其他客户端提供服务。Redis 作为缓存挡在 MySQL 的前面,若 Redis 被阻塞,所有请求会瞬间打到数据库,数据库很可能被压垮,导致整个应用不可用。                                

        exists key key ...

        注:判断某个或多个 key 是否存在,返回存在的个数。O(1) 时间复杂度。

        del key key ...

        注:删除某个或多个 key,返回删除的个数。O(1) 时间复杂度。

        expire key seconds

        注:为指定的 key 添加秒级的过期时间。返回 1 表示设置成功,0 表示设置失败。pexpire 设置毫秒级时间。O(1) 时间复杂度。

        ttl key

        注:获取指定 key 的秒级过期时间。返回剩余过期时间,-1 表示无关联过期时间,-2 表示 key 不存在。O(1) 时间复杂度。

        Redis 检查 key 是否过期的策略:

        定期删除:抽取一部分数据,验证过期时间。遍历所有的 key 效率太低,并且 Redis 是单线程的程序,不能在扫描过期 key 这件事上花费太长时间。

        惰性删除:检查工作推迟至客户端访问该 key。

        其他内存淘汰策略 ...

        type key

        注:返回 key 对应的 val 的数据类型。

2. Redis 的单线程架构

        Redis 的核心业务,对数据的操作始终是单线程的。但在 6.0 之后,也使用了额外的线程来处理网络 IO。

        Redis 在处理网络时利用 I/O 多路复用机制。该机制允许一个线程同时监视多个网络请求,当某 socket 变得可读或可写时,才通知主线程处理。

        前提是,该业务场景并不涉及非常频繁的交互,如直播、竞技游戏等。当同一时刻,大部分 socket 都是静默的,没有数据传输,IO 多路复用就显得非常高效了。Redis 的主线程就是通过这个机制管理数十万的网络连接。

        工作流程具体为:

        Redis 的主线程使用 epoll 这组 I/O 多路复用的 API,监听所有连接。

        当连接真正可读时,主线程将读写网络数据的任务交由 IO 线程并行处理。

        IO 线程读完数据后,主线程串行执行全部读取到的命令。

        执行完毕,主线程将响应交由 IO 线程写入。

        所有的网络操作(accept、read、write、close)都不再是传统的阻塞式调用,而是被转换为对事件的监听和回调。主线程只在事件确实发生时(数据就绪)才进行实际操作,避免了在空的网络 I/O 上无谓地等待和阻塞。

        因此再来总结 Redis 快的原因

        1. 纯内存访问。这是 Redis 快的基础。

        2. 非阻塞 IO。

        3. 单线程避免线程竞争开销,也无需考虑线程安全问题。Redis 的核心业务基本都是对内存数据的简单操作,不会特别消耗 CPU,单线程足以应对。

        4. Redis 并没有关系型数据库那样复杂的功能支持。比如关系型数据库中的各种约束。   

        单线程固然为 Redis 带来许多优势,但缺点也很明显。一旦某个命令执行过长,会导致其他全部命令处于等待,造成客户端的阻塞。这对于 Redis 这种提供高性能服务的中间件是非常致命的。因此说,Redis 是面向快速执行场景的数据库。 

3. 数据结构

3.1 概览

        Redis 提供给用户的每种数据类型,在内部都有多种不同的底层实现方式。在储存小型数据时,自动采用更紧凑、节省内存的结构。数据量变大时,改用查询效率更高的结构。这一切对用户透明,由 Redis 内部自行转换,具体参数可配置。

数据类型内部代码
string

raw:长字符串

int:整数

embstr:短字符串

hash

hashtable:标准字典实现

ziplist:压缩列表

listquicklist:由 ziplist 作为节点的双向链表
set

hashtable:value 被设为 NULL,只用 key 来表示。

intset:整数数组

zset 有序集合

ziplist

skiplist:跳表

        对 ziplist 的解释:

        对于普通双向链表来说,为每个节点维护 prev、next 这些指针域是很大的内存开销,哈希表就更多了。ziplist 就是为了最大限度节省内存而设计的。ziplist 的数据在内存中是紧挨着存放的,通过一些算法来定位,用 CPU 计算时间换内存空间。 

        此时 key 和 val 都作为 ziplist 的 entry,被紧挨着存放。entry 中也存储了长度等信息用于计算每个节点的位置。

        不过 ziplist 的查找只能通过遍历,在数据量变大时,性能会下降。

        对 skiplist 的解释:

        一种可替代传统红黑树的数据结构。通过在有序链表的基础上添加多级索引,使得查找插入删除的操作都能在概率上达到 O(log N)。

        跳表建立多级索引的底层是一个数组,数组中的每个元素都是一个指向后续元素的指针,这称为层高。如下图,6 元素的层高就为 2,因为它在两级索引中出现。L0 级索引中包括全部元素,在其他层数字的出现与否是随机的,但需要算法保证索引呈金字塔形。

        下图展示了跳表找到 12 的逻辑。

        

        下图根据真实跳表结构,展示找 11 的逻辑(仅展示部分 forward[ ] 数组)。

        为什么 Redis 选择跳跃表而不是红黑树来实现 ZSet?

        因为跳表非常擅长范围查询。但是一个跳表节点元数据的开销实际上比红黑树的大,因为存引用的数组要花很大开销,但是 Redis 认为这是值得的。

        跳表也是典型空间换时间的体现。

3.2 String

        Redis 服务器中的字符串是直接以二进制储存的,不会做任何编码转换。因此我们可以储存任何东西,包括文字、数字、图片等等。并且不会因字符编码问题产生任何意外。Redis 客户端可以完成从二进制到字符串的解码工作,需要在启动客户端时加上 --raw 这个选项。

        典型应用场景:

        1. 与 MySQL 组成缓存储存架构:

        根据 Redis 支撑高并发的特性,将其作为缓存层挡在 MySQL 前面,以加速读写,降低数据库压力。

        与 MySQL 等关系型数据库不同的是,Redis 没有表这种命名空间。因此需设计合理的键名,以防止键冲突。如,MySQL 的数据库名为 vs,表名 user_info,那么对应的键可以使用 "vs:user_info:6379"、"vs:user_info:6379:name" 来表示。也可以使用团队内部都认同的缩写替代,例如 "user:6379:friends:messages:5217" 可以被 "u:6379:fr:m:5217" 代替。避免键名过长导致 Redis 的性能下降。

        2. 计数功能

        使用 Redis 作为计数的基础工具,同时数据可以异步处理到其他数据源,对用户数据做进一步分析。

        Redis 并不擅长数据统计,因为 Redis 并不对数据做像 MySQL 那样精细的控制,也不提供有关数据统计的 API。

        实际上一个成熟的计数系统也非常复杂,需要考虑很多问题。如,防作弊、单点问题、数据持久化等。并且,目前的计数需要更多维度。比如,用户看到视频的哪个位置,是否一开头就划走,这都是感知用户偏好的关键。

        3. 对 Session 的集中管理

        在分布式系统中,用户的请求会被负载均衡系统分配到不同的服务器上。而 Session 是与用户强关联的,因此可以将 Session 集中到 Redis 上进行管理,无论用户的请求被均衡到哪台服务器上,都从 Redis 中进行查询和更新。

        4. 防重复提交

        Redis 的基础特性是 key 唯一。使用 set 命令为已存在的 key 设置新值,那么新值会覆盖旧值。但是可以通过使用 SET key value NX 来实现只有当 key 不存在时才设置。NX 意为 Not Exists。加 NX 选项是分布式系统中一个非常常见的场景,例如用于实现分布式锁、防止重复提交等。

        例如,如果想限制用户获取验证码的频率,我们就可以设置 NX 加过期时间。

        set key 5 ex 60 nx,每次用户尝试获取要执行 decr val。

        因为 NX 在当该键存在时会返回 nil,我们就可以判断用户已经在 60 秒内请求过了,再看看 val 是否为 0,如果为 0 就不允许用户获取了。这表示只允许用户在 60 秒内最多获取 5 次验证码。

3.3 Hash

        引入 Hash 之后的数据结构如下图所示:

        

        此时相当于两层索引,想要找到 Hash 中的 value,需要对应的 key 和对应的 field。

        贸然使用 hkeys、hvals、hgetall 这样的命令是很危险的。因为该 Hash 表中可能有很多元素,这样的命令会去遍历 Hash 表。如果一定要获取较大 Hash 表中的值,可以使用 hscan。(后面介绍)

        使用 hlen 可以获取 field 个数,这是 Redis 维护的变量,无需遍历 Hash。

        典型应用场景:

        1. 记录用户信息:

        用户信息往往是结构化的数据,比如使用关系型数据库时,构建的用户信息表,表头是有固定字段的。在 Redis 中,我们可以通过 Hash 实现类似的效果。比如为 key 添加用户 ID 后缀,对应 val 的每一个 field 都能表示用户的不同信息,如姓名、性别、年龄等。

        但这依然与关系型数据库有差距。因为关系型数据库的数据是完全结构化的,表头字段是严格设置不可轻易变更的,并且每个格子都要有值(即使为 null)。这的确浪费了部分空间,但我们此时可以做复杂的关系查询,而 Redis 基本不可能做到这点。

        除此以外,我们还可以手动序列化数据后,再作为 String 类型传入 Redis 保存。比如使用 JSON 格式。此时内存的利用效率比 Hash 要高不少,因为这是连续的内存。对于经常需要整体调用的数据,使用这种方式比较合理。如果经常需要调用其中的某个属性,则显得没有 Hash 灵活。毕竟序列化和反序列化是需要时间开销的。

        使用 Hash 相当于空间换时间,手动序列化数据再使用 String 相当于时间换空间了。

3.4 List

        Redis 列表的底层是以 ziplist 作为节点的双端链表,称为 quicklist。

        quicklist 平衡了 linkedlist 的节点内存开销和 ziplist 的插入删除时间开销。节点内的 ziplist 保证了储存的紧凑,外层的链表将大的 ziplist 分散保证插入删除效率。

        不过,Redis 的 list 被设计为更侧重于序列,而不是数组。它的核心效率考量是基于队列的头尾增删操作,而非随机访问。

                

        list 支持逆向下标,即可以使用 -1 下标获取最后一个元素。

        lpush 因为是头插,插入多个元素时,顺序与命令中数据的顺序相反。如果 key 不存在,则创建新列表。

        lpushx 当且仅当 key 存在时插入。

        在 Redis 6 以后,lpop 和 rpop 支持在后面加一个可选的 count 参数,表示这次操作要 pop 几个元素。如 rpop key1 6,表示从 key1 对应的 list 右侧 pop 6 个元素。

                

        lindex 操作需要遍历链表的外层节点来定位目标元素在哪个 ziplist 中,因此它的时间复杂度为 O(N)。

        linsert 在插入时要选取基准值,并指定在其前或后插入。入过基准值有多个,则匹配从左向右的第一个。

        对于 lrange 命令,若传入下标范围大于 list 已有下标,则 Redis 会尽可能返回其中包括的值,不会报错。

        lrem key count element -----> count > 0 表示从左向右找 count 个 element 删除,< 0 表示从右向左找 count 个 element 删除,= 0 表示删除全部的 element。

        阻塞版本的命令:

        list 的命令中,有一部分以 b 开头,这表示 block,即阻塞。如 blpop、brpop 等。

        语法:BLPOP key [key ...] timeout

        当 timeout 并未超时,且指定的一个或多个 key 对应的 list 中有 element 时,返回给 client。若 list 均为空,则执行命令的客户端被 Redis 标记为阻塞状态,在收到返回结果之前不能进行任何操作。

        Redis 使用一个 Hash 维护这些处于等待的 client 和其等待的 keys,称为 blocking_keys,结构如下:

        Key --> 被 client 等待的键

        Value --> 一个链表,每个节点保存一个 client 结构体

        由此,Redis server 本身不会有任何阻塞,依旧正常执行其他客户端的命令。当其他 client 的命令尝试将某 list 由空转为非空的时候,Redis 检查 blocking_keys 种是否有 client 在等待此 key。若有,Redis 不会立即唤醒客户端,而是先将该 key 推入另一个 Hash,名为 ready_keys。这相当于一步程序的解耦,先执行好本次插入操作,将处理阻塞客户端注册为其他事件,依次执行。       

        客户端可以同时等待多个 key。其信息会被分别添加到 blocking_keys 中 key1 和 key2 对应的链表里。只要其中任意一个 key 有数据到达,客户端就会被唤醒,并从所有相关的阻塞链表中移除。

        Redis 同样维护了一个最小堆来管理所有阻塞客户端的超时时间。事件循环会定期检查这个堆,如果某个客户端等待超时,就会将其唤醒并返回一个 nil 回复,然后将其从所有相关的阻塞数据结构中清理出去。

        典型应用场景:

        1. 简单的消息队列:

        使用 lpush + brpop 即可实现一个简单的消息队列。不过,目前关于消息队列有更加成熟的实现,我们对消息队列的要求也比较复杂。

        2. 记录时间轴:

        list 的元素是按照插入顺序排列的,并且使用 lrange 命令能高效地批量获取元素(遍历一次就可以返回一批连续的元素)。基于这两点,可以用其记录时间线。

        比如用户想查看自己最近发布的动态,那么就可以使用 lrange 命令非常高效地一次性返回一批有序的动态 ID,之后再用这些 ID 去哈希表里查询记录的详细信息。

        在时间轴中添加元素时,可以将 lpush 与 ltrim 命令配合使用,二者组合起来可以自动维护一个固定大小的集合,保证内存不会无限增长。

3.5 Set

        Redis 对集合除了提供基本的增删查改命令,还提供了不同集合之间的运算。

        这包括,迁移(smove)、交集(sinter)、并集(sunion)、差集(sdiff)。

        还有相应的 sinterstore、sunionstore、sdiffstore,将集合间运算的结果储存到填入的 destination 中。

        需要注意的是,sdiff 对传入 key 的顺序是有要求的,因为差集需要描述为谁对谁的差集。如下图第一条 sdiff,如果求 set1 对 set2 和 set3 的差集,那返回的是 set1 中有但是另外两个没有的元素。其他也是同理。

                       

        典型应用场景:

        1. 保存用户标签

        根据 set 的无序且不重复的特性,可以使用它将用户的兴趣点抽象为标签,从而分析用户画像。还可以通过 sinter 等命令分析几个用户的共同爱好。

3.6 Zset

        Zset 为每个 set 的 member 关联了一个 score 属性(浮点数),并按照 score 属性为数据进行升序排列。Zset 中的 member 依然不能重复,但 score 可以重复。

        其他重要的命令:

        zpopmax key count ---> 删除并返回分数最高的 count 个元素, 分数相同则按照字典序删除。此处的时间复杂度为 O(log(N)*M),N 为表中元素个数,M 为 count。也就是说,删除一次表中的最大元素,是需要 log 级时间复杂度的。这是因为,根据前文介绍过的跳表结构,删除任一个元素,都需要涉及到修改全部 forward[ ] 数组对该元素的引用。即使我们保存了跳表尾节点的引用,也不能直接删除,我们必须知道都有哪些元素存在它的引用。因此必须从头开始完成整个下降过程。不过,log 级别的时间复杂度已经很快了。

        zrank 和 zrevrank,获取指定元素的排名。排名其实就是下标。zrank 是直接返回下标,即升序排列的排名。zrevrank 为降序排列,得到降序排列的排名。

        zscore,根据 member 找元素。这里的时间复杂度是 O(1)。Redis 对 zset 的查询操作做了进一步优化,因为查询 score 是非常高频的命令。Redis 使用一个额外的 hash 来记录 member 和 score 的映射关系,从而达到 O(1)。虽然这会增加一定的内存开销,但是不会很多,因为元素对象在内存中依然只有一份,两个数据结构只是通过指针引用它们,额外的开销只是 hash 的指针开销。

        集合间运算:

        典型应用场景:

        排行榜相关场景。如果我们使用 Redis 的 zset 在内存中存储排行榜,那就必须使用单一 zset,因为 Redis 不擅长聚合查询,如果分几个 zset,那么将很难统计总排名。

        那么,使用单个 zset 可不可行呢?假设这是一个用户体量极大的游戏,有 1 亿玩家,根据 Redis 的内存结构,每个 zset 节点将占用 100 字节左右的空间(这包括数据、指针、LRU/LFU 信息等)。

        对于 1 亿玩家,空间就是 1 亿 * 100 字节。根据 B -> KB -> MB -> GB,近似使用 1000 进制,1 亿 B 是 0.1 GB,再乘 100 就是 10 GB。看起来,这样的内存占用倒也不算太大。

        不过,上述情况也是比较极端的。因为我们完全不需要将全部玩家都记录在内存的排行榜中,我们只需要记录活跃度稍高一些的玩家,或者排名在百万级别以内的。完整的排行还是记录在关系型数据库中更划算。

3.7 渐进式遍历

        SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]

        以渐进式的方式进行 Redis 中 key 的遍历,返回下一次 scan 的游标(cursor)以及本次得到的 key。

        cursor 并不是下标,这个概念仅存在于 Redis 服务器,因此 Redis 告诉我们下一次该传什么 cursor,我们传什么就可以了,这里没有规律。直到 cursor 为 0,即遍历结束,或者说重新开启一次遍历。

        MATCH 即匹配规则,这里的规则和 keys 命令一致。

        

        COUNT 是本次遍历的个数,默认为 10。

        TYPE 是限制这次遍历的 value 类型,如指定为 list,那么该次就只获取 list 类型的 key。

        渐进式遍历的过程中,服务器不会储存任何状态信息,遍历随时可以终止。

        渐进性遍历虽然解决了阻塞的问题,但如果在遍历期间键有所变化(增加、修改、删除),可能导致遍历时键的重复遍历或者遗漏。

4. 数据库管理

        Redis 默认配置中有 16 个数据库。默认情况下我们处于数据库 0。各个数据库保存的数据是完全不冲突的,各有各的键值对。

        Redis 中虽然支持多数据库,但不建议使用。如果需要完全隔离两套键值对,更好的做法是维护多个 Redis 实例,而不是在一个 Redis 实例中维护多数据库。因为 Redis 并没有为多数据库提供太多的特性,并且无论是否有多个数据库,Redis 都是单线程的,彼此之间依旧需要排队等待命令的执行。


文章转载自:

http://g4gIH9qy.fycjx.cn
http://GB8hO9g8.fycjx.cn
http://AdW8n3mV.fycjx.cn
http://Rg4Rn44g.fycjx.cn
http://KfCQZCuM.fycjx.cn
http://KdcSFEaG.fycjx.cn
http://ymFt3wnP.fycjx.cn
http://zQs9dsWF.fycjx.cn
http://tdkTK9EY.fycjx.cn
http://OcPY93FF.fycjx.cn
http://JLRY3R7z.fycjx.cn
http://slV9ZJrz.fycjx.cn
http://ZZOBJyK6.fycjx.cn
http://G9Pesyr9.fycjx.cn
http://CX0wt1f3.fycjx.cn
http://lwEW1xDk.fycjx.cn
http://5HlW9GhO.fycjx.cn
http://VbxbsHMP.fycjx.cn
http://pxEzatjf.fycjx.cn
http://MFVW8R8x.fycjx.cn
http://QYognMAm.fycjx.cn
http://8DSOcsqY.fycjx.cn
http://6NPWDuX9.fycjx.cn
http://1Yu8toMi.fycjx.cn
http://z76AbIiO.fycjx.cn
http://qqMTEfeR.fycjx.cn
http://ya1IJiwt.fycjx.cn
http://86qkul8T.fycjx.cn
http://lB4yJJkI.fycjx.cn
http://X6yHUGXN.fycjx.cn
http://www.dtcms.com/a/368260.html

相关文章:

  • 分布式常见面试题整理
  • “卧槽,系统又崩了!”——别慌,这也许是你看过最通俗易懂的分布式入门
  • 数字时代的 “安全刚需”:为什么销售管理企业都在做手机号码脱敏
  • 乐观并发: TCP 与编程实践
  • 两条平面直线之间通过三次多项式曲线进行过渡的方法介绍
  • if __name__=‘__main__‘的用处
  • MySQL知识回顾总结----数据类型
  • WeaveFox AI智能开发平台介绍
  • Oracle:select top 5
  • sub3G、sub6G和LB、MB、HB、MHB、LMHB、UHB之间的区别和联系
  • Tenda AC20路由器缓冲区溢出漏洞分析
  • 52核心52线程,Intel下一代CPU憋了个大的
  • 50kNm风能传动轴扭转疲劳检测试验台指标
  • 蓓韵安禧DHA温和配方:安全营养的智慧守护
  • Kafka面试精讲 Day 8:日志清理与数据保留策略
  • 轨迹文件缺少时间
  • 国产数据库之YashanDB:新花怒放
  • 医疗AI中GPU集群设计与交付实践
  • 基于Compute shader的草渲染
  • go webrtc - 1 go基本概念
  • OSI七层模型与tcp/ip四层模型
  • WebRTC进阶--WebRTC错误Failed to unprotect SRTP packet, err=9
  • 自由学习记录(95)
  • 商业融雪系统解决方案:智能技术驱动下的冬季安全与效率革命
  • 用 epoll 实现的 Reactor 模式详解(含代码逐块讲解)
  • Linux ARM64 内核/用户虚拟空间地址映射
  • linux inotify 功能详解
  • C++中虚函数与构造/析构函数的深度解析
  • 工业客户最关心的,天硕工业级SSD固态硬盘能解答哪些疑问?
  • 在宝塔面板中修改MongoDB配置以允许远程连接