redis学习笔记-list原理
list 基本功能
命令 | 描述 |
---|---|
BLPOP key1,key2,…… timeout | 移除并获取列表的第一个元素,如果列表没有元素会阻塞列表直到等待超时或者弹出元素为止。 |
BRPOP key1 [key2 ] timeout | 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 |
BRPOPLPUSH source destination timeout | 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它;如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 |
LIndex key index | 通过索引获取列表中的元素 |
Linsert key before/after pivot value | 在列表的元素前或者后插入元素 |
LLEN key | 获取列表长度 |
LPOP key | 移出并获取列表的第一个元素 |
LPUSH key value1,value2,… | 将一个或者多个值插入到列表头部 |
LPUSHX key value | 将一个值插入到已经存在的列表头部 |
LRANGE key srart stop | 获取列表指定范围内的元素 |
LREM key count value | 移除列表元素 |
LSET key index value | 通过索引设置列表元素的值 |
LTRIM key start stop | 对一个列表进行修剪,就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都被删除。index从0开始,区间均包含。 |
RPOP key | 移除列表的最后一个元素,返回值为移除的元素 |
RPOPPUSH source destination | 移除列表的最后一个元素,并将该元素添加到另一个列表并返回 |
RPUSH key value1 value2 …… | 添加一个或者多个值到list的尾部 |
RPUSHX key value | 为已经存在的;列表添加值 |
图表来自:https://www.cnblogs.com/amyzhu/p/13466311.html
单链表
在学习 redis 的 list 实现之前,我们先来看一下单链表 list 怎么实现:
每一个节点都有一个向后的指针(引用)指向下一个节点,最后一个节点指向NULL表示结束,有一个Head(头)指针指向第一个节点表示开始。
类似于这样,新建和删除虽然只需要 O(1)
O(1)
,但是查找需要 O(n)
,但是查找需要 O(n)
的时间;无法反向查找,一旦错过需要从头开始。
🎜增加一个节点:🎜🎜删除一个节点:
双向链表
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。
特点:
每次在插入或删除某个节点时, 需要处理四个节点的引用, 而不是两个. 实现起来要困难一些
相对于单向链表, 必然占用内存空间更大一些.
既可以从头遍历到尾, 又可以从尾遍历到头
这好像就解决 redis 可以实现前后都可以遍历的问题了。
那么我们来看看 redis 的链表 怎么处理的:
再来看一下它的结构体定义源码
ListNode:
typedef struct listNode { // 前驱节点 struct listNode *prev; // 后继节点 struct listNode *next; // 节点的值 void *value; } listNode;
List:
typedef struct listNode { // 表头节点 listNode *head; // 表尾节点 listNode *tail; // 链表所包含的节点数量 unsigned long len; // 节点值复制函数 void *(*dup)(void *ptr); // 节点值释放函数 void *(*free)(void *ptr); // 节点值对比函数 int (*match)(void *ptr,void *key) } list;
redis 链表的特性:
双向无环:链表节点带有前驱、后继指针获取某个节点的前驱、后继节点的时间复杂度为O(1),表头节点的前驱指针和表尾节点的后继指针都指向NULL,对链表的访问以NULL为终点。
长度计数器:通过List结构的len属性获取节点数量的时间复杂度为O(1)。
由于 list 还存在一个内存分配不连续,并引发的内存碎片的问题,是否有办法让他们的内存优化一下?
redis 压缩列表
ZipList不是基础数据结构,是Redis自己设计的一种数据存储结构。它有点类似数组,通过一片连续的内存空间来存储数据。
与数组不同的是,它允许所存储的列表元素所占据的内存空间不同。说到压缩这两个字,大家第一时间可能想到的就是节省内存。之所以说这种存储结构节省内存,是相对数组而言的。
我们都知道,数组要求每个元素存储空间的大小相同,如果我们要存储不同长度的字符串,就要以最大长度的字符串所占的存储空间作为字符串数组每个元素存储空间的大小(假如是50字节)。
因此在字符数数值中存储小于50字节长度的字符串时就会浪费一部分存储空间。
数组 的优势就是占用一片连续的空间,可以很好地利用CPU缓存快速访问数据。
如果既想保留数组的这种优势又想节省存储空间,那么我们可以对数组进行压缩:
不过,有一个问题,在遍历压缩列表的时候我们并不知道每个元素占用的内存大小是多少,因此无法计算出下一个元素的具体起始位置。
但是转念一想,如果每个元素访问之前我们能有它的长度,那么不就可以解决这个问题了嘛。
接下来我们看看Redis是如何通过实现ZipList使之结合既保留了数组的优点又节省了内存。
zlbytes:压缩列表的字节长度,是uint32_t类型,占4字节,因此压缩列表最多有232-1字节,可以通过该值计算zlend的位置,也可以在内存重新分配的时候使用。
zltail:压缩列表尾元素相对于压缩列表起始地址的偏移量,是uint32_t类型,占4字节,可以通过该值快速定位到列表尾元素的位置。
zllen:压缩列表元素的个数,是uint16_t类型,占2字节,表示最多存储的元素个数为216-1=65 535,如果需要计算总数量,则需要遍历整个压缩列表。
entryx:压缩列表存储的元素,既可以是字节数组,也可以是整数,长度不限。
zlend:压缩列表的结尾,是uint8_t类型,占1字节,恒为0xFF。
previous_entry_length:表示前一个元素的字节长度,占1字节或者5字节,当前元素的长度小于254时用1字节,大于等于254时用5字节,previous_entry_length 字段的第一个字节固定是0xFE,后面的4字节才是真正的前一个元素的长度。
encoding:表示元素当前的编码,有整数或者字节数。为了节省内存,encoding字段长度可变。
content:表示当前元素的内容。
ZipList变量的读取和赋值都是通过宏来实现的,代码片段如下:
//返回整个压缩列表的总字节 #define ZIPLIST_BYTES(zl) (*((uint32_t*)(zl))) //返回压缩列表的tail_offset变量,方便获取最后一个节点的位置 #define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t)))) //返回压缩列表的节点数量 #define ZIPLIST_LENGTH(zl) (*((uint16_t*)((zl)+sizeof(uint32_t)*2))) //返回压缩列表的表头的字节数 //(内存字节数zlbytes,最后一个节点地址ztail_offset,节点总数量zllength) #define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t)) //返回压缩列表最后结尾的字节数 #define ZIPLIST_END_SIZE (sizeof(uint8_t)) //返回压缩列表首节点地址 #define ZIPLIST_ENTRY_HEAD(zl) ((zl)+ZIPLIST_HEADER_SIZE) //返回压缩列表尾节点地址 #define ZIPLIST_ENTRY_TAIL(zl) ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) //返回压缩列表最后结尾的地址 #define ZIPLIST_ENTRY_END(zl) ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)
对此,可以通过定义的结构体和对应的操作定义知道大概 redis 设计 压缩list 的思路;
这种方式虽然节约了空间,但是会存在每次插入和创建存在内存重新分配的问题。
quicklist
quicklist是Redis 3.2中新引入的数据结构,能够在时间效率和空间效率间实现较好的折中。Redis中对quciklist的注释为A doubly linked list of ziplists。顾名思义,quicklist是一个双向链表,链表中的每个节点是一个ziplist结构。quicklist可以看成是用双向链表将若干小型的ziplist连接到一起组成的一种数据结构。
当ziplist节点个数过多,quicklist退化为双向链表,一个极端的情况就是每个ziplist节点只包含一个entry,即只有一个元素。当ziplist元素个数过少时,quicklist可退化为ziplist,一种极端的情况就是quicklist中只有一个ziplist节点。
quicklist 结构定义:
typedef struct quicklist { // 指向quicklist的首节点 quicklistNode *head; // 指向quicklist的尾节点 quicklistNode *tail; // quicklist中元素总数 unsigned long count; /* total count of all entries in all ziplists */ // quicklistNode节点个数 unsigned long len; /* number of quicklistNodes */ // ziplist大小设置,存放list-max-ziplist-size参数的值 int fill : 16; /* fill factor for individual nodes */ // 节点压缩深度设置,存放list-compress-depth参数的值 unsigned int compress : 16; /* depth of end nodes not to compress;0=off */ unsigned int bookmark_count: 4; quicklistBookmark bookmarks[]; } quicklist; typedef struct quicklistBookmark { quicklistNode *node; char *name; } quicklistBookmark;
quicklistNode定义如下:
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;
redis为了节省内存空间,会将quicklist的节点用LZF压缩后存储,但这里不是全部压缩,可以配置compress的值,compress为0表示所有节点都不压缩,否则就表示从两端开始有多少个节点不压缩;compress如果是1,表示从两端开始,有1个节点不做LZF压缩。compress默认是0(不压缩),具体可以根据你们业务实际使用场景去配置。
redis 的配置文件可以配置该参数
list-compress-depth 0
值 | 含义 |
---|---|
0 | 特殊值,表示都不压缩 |
1 | quicklist两端各有1个节点不压缩,中间的节点压缩 |
2 | quicklist两端各有2个节点不压缩,中间的节点压缩 |
n | quicklist两端各有n个节点不压缩,中间的节点压缩 |
还有个fill字段,它的含义是每个quicknode的节点最大容量,不同的数值有不同的含义,默认是-2,当然也可以配置为其他数值;
list-max-ziplist-size -2
当值为正数时,表示quicklistNode节点上的ziplist的长度。比如当这个值为5时,每个quicklistNode节点的ziplist最多包含5个数据项
当值为负数时,表示按照字节数来限制quicklistNode节点上的ziplist的的长度,可选值为 -1 到 -5。
值 | 含义 |
---|---|
-1 | ziplist节点最大为4kb |
-2 | ziplist节点最大为8kb |
-3 | ziplist节点最大为16kb |
-4 | ziplist节点最大为32kb |
-5 | ziplist节点最大为64kb |
为什么会有配置提供出来呢?
ziplist越短,内存碎片增多,影响存储效率。当一个ziplist只存一个元素时,quicklist又退化成双向链表了
ziplist越长,为ziplist分配大的连续的内存空间难度也就越大,会造成很多小块的内存空间被浪费,当quicklist只有一个节点,元素都存在一个ziplist上时,quicklist又退化成ziplist了
结束语
虽然没有完全去理解它的源码,但我们也可以通过这篇文章来熟悉 redis 的一个设计思路。并知道它是怎么一步一步优化过来的。让我们有一个大概的性能认知。
以上是redis学习笔记-list原理的详细内容。更多信息请关注PHP中文网其他相关文章!

热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

热门话题

Redis集群模式通过分片将Redis实例部署到多个服务器,提高可扩展性和可用性。搭建步骤如下:创建奇数个Redis实例,端口不同;创建3个sentinel实例,监控Redis实例并进行故障转移;配置sentinel配置文件,添加监控Redis实例信息和故障转移设置;配置Redis实例配置文件,启用集群模式并指定集群信息文件路径;创建nodes.conf文件,包含各Redis实例的信息;启动集群,执行create命令创建集群并指定副本数量;登录集群执行CLUSTER INFO命令验证集群状态;使

使用 Redis 指令需要以下步骤:打开 Redis 客户端。输入指令(动词 键 值)。提供所需参数(因指令而异)。按 Enter 执行指令。Redis 返回响应,指示操作结果(通常为 OK 或 -ERR)。

要查看 Redis 中的所有键,共有三种方法:使用 KEYS 命令返回所有匹配指定模式的键;使用 SCAN 命令迭代键并返回一组键;使用 INFO 命令获取键的总数。

Redis 使用哈希表存储数据,支持字符串、列表、哈希表、集合和有序集合等数据结构。Redis 通过快照 (RDB) 和追加只写 (AOF) 机制持久化数据。Redis 使用主从复制来提高数据可用性。Redis 使用单线程事件循环处理连接和命令,保证数据原子性和一致性。Redis 为键设置过期时间,并使用 lazy 删除机制删除过期键。

Redis计数器是一种使用Redis键值对存储来实现计数操作的机制,包含以下步骤:创建计数器键、增加计数、减少计数、重置计数和获取计数。Redis计数器的优势包括速度快、高并发、持久性和简单易用。它可用于用户访问计数、实时指标跟踪、游戏分数和排名以及订单处理计数等场景。

启动 Redis 服务器的步骤包括:根据操作系统安装 Redis。通过 redis-server(Linux/macOS)或 redis-server.exe(Windows)启动 Redis 服务。使用 redis-cli ping(Linux/macOS)或 redis-cli.exe ping(Windows)命令检查服务状态。使用 Redis 客户端,如 redis-cli、Python 或 Node.js,访问服务器。

理解 Redis 源码的最佳方法是逐步进行:熟悉 Redis 基础知识。选择一个特定的模块或功能作为起点。从模块或功能的入口点开始,逐行查看代码。通过函数调用链查看代码。熟悉 Redis 使用的底层数据结构。识别 Redis 使用的算法。

使用Redis进行锁操作需要通过SETNX命令获取锁,然后使用EXPIRE命令设置过期时间。具体步骤为:(1) 使用SETNX命令尝试设置一个键值对;(2) 使用EXPIRE命令为锁设置过期时间;(3) 当不再需要锁时,使用DEL命令删除该锁。
