新版 redis 的 list 实际上只有一种数据结构 quicklist ,而且是一种双向链表, 代码如下:
数据结构如下:
我们再来看看源码:
/* quicklistNode is a 32 byte struct describing a listpack for a quicklist. * We use bit fields keep the quicklistNode at 32 bytes. * count: 16 bits, max 65536 (max lp bytes is 65k, so max count actually < 32k). * encoding: 2 bits, RAW=1, LZF=2. * container: 2 bits, PLAIN=1, PACKED=2. * recompress: 1 bit, bool, true if node is temporary decompressed for usage. * attempted_compress: 1 bit, boolean, used for verifying during testing. * extra: 10 bits, free for future use; pads out the remainder of 32 bits */ typedef struct quicklistNode { // 双向链表前驱节点 struct quicklistNode *prev; // 双向链表的后节点 struct quicklistNode *next; // 不设置压缩数据参数 recompres 指向一个 ziplist 结构 // 不设置压缩数据参数 recompres 指向一个 quicklistLZF 结构 unsigned char *entry; // 压缩列表 ziplist 的总长度 size_t sz; /* entry size in bytes */ // 每个 ziplist 中 entry 的个数 unsigned int count : 16; /* count of items in listpack */ // 表示是否采用了 LZF 压缩 quickList 节点 1 表示压缩过,2 表示没有压缩站 2bit 长度 unsigned int encoding : 2; /* RAW==1 or LZF==2 */ // 表示是否开启 ziplist 进行压缩 unsigned int container : 2; /* PLAIN==1 or PACKED==2 */ // 表示该节点是否被压缩过 unsigned int recompress : 1; /* was this node previous compressed? */ // 测试使用 unsigned int attempted_compress : 1; /* node can't compress; too small */ // 额外拓展位,占 10bit 长度 unsigned int extra : 10; /* more bits to steal for future usage */ } quicklistNode; /* 当指定使用 lzf 压缩算法压缩 ziplist entry 节点时 quicklistNode 结构的 zl 成员执行 quicklistLZF 结构 quicklistLZF is a 8+N byte struct holding 'sz' followed by 'compressed'. * 'sz' is byte length of 'compressed' field. * 'compressed' is LZF data with total (compressed) length 'sz' * NOTE: uncompressed length is stored in quicklistNode->sz. * When quicklistNode->entry is compressed, node->entry points to a quicklistLZF */ typedef struct quicklistLZF { // 表示被LZF 压缩后的 ziplist 的大小 size_t sz; /* LZF size in bytes*/ // 压缩有的数据,柔性数组 char compressed[]; } quicklistLZF;
quicklist 结构体定义:
/* quicklist is a 40 byte struct (on 64-bit systems) describing a quicklist. * 'count' is the number of total entries. * 'len' is the number of quicklist nodes. * 'compress' is: 0 if compression disabled, otherwise it's the number * of quicklistNodes to leave uncompressed at ends of quicklist. * 'fill' is the user-requested (or default) fill factor. * 'bookmarks are an optional feature that is used by realloc this struct, * so that they don't consume memory when not used. */ typedef struct quicklist { // 链表表头 quicklistNode *head; // 链表表尾巴 quicklistNode *tail; // 所有 quicklistNode 节点中所有的 entry 个数 unsigned long count; /* total count of all entries in all listpacks */ // quickListNode 节点个数,也就是 quickList 的长度 unsigned long len; /* number of quicklistNodes */ //单个节点的填充因子,也就是 ziplist 的大小 signed int fill : QL_FILL_BITS; /* fill factor for individual nodes */ // 保存压缩成都只,配置文件设置,64位操作系统占 16bit , 6 表示压缩 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;
为什么这样设计?
- 双向链表,插入和删除效率高,但是对内存不友好,而且需要存取额外的前后两个指针
- 数组代表的是连续的内存存储空间,插入和删除时间复杂度高,但是对内存友好(符合局部性原理),因此综合了两个, 产生了 quicklist 数据结构,其实在 C++ 的 stl 中的 deque 也是种设计模式。
思考: 如何设置来均衡链表长度和数组(ziplist)为代表的比例呢?我们来考虑一下两者极端情况,如果链表长度为 1 那么就退化成一个 ziplist 了 ,此时可能是找不到一块很大的连续内存而导致 OOM;如果ziplist 为1 , 就退化成双向链表了,此时对内存是非常不友好的,而且会造成大量的内存碎片,影响性能?对于处理方案,看我们后面的具体分析。
核心参数
- fill : 16bit ,控制 ziplist 大小,存放
list-max-ziplist-size
参数值。
可以看出正值表示 quicklist 上 ziplist 的 entry 个数
- compress: 16bit 节点压缩深度值,存放
list-compress-depth
参数值。
由于当前数据很多, 容易被访问的可能是两端的数据,中间的数据访问频率比较低(访问起来性能也比较低),如果应用场景符合这个特点,那么list 还提供了一个选项,能够把中间的数据节点进行压缩,从而进一步节省内存空间。
list-compress-depth 0
- 0 是一个特殊值,表示都不压缩,这个是 redis 的默认值。
- 1 表示 quicklist 两端各有1个节点不压缩,中间节点压缩
- 2 表示 quicklist 两端各有2个节点不压缩,中间节点压缩
- 3 表示 quicklist 两端各有3个节点不压缩,中间节点压缩
以此类推
- recompress
执行 index 等操作会对节点进行暂时解压缩, recompress 表示后某个时刻在进行压缩。
可以看看插入过程
更具 AllowInsert 判断是否需要新增节点
列表特征
- 阻塞和非阻塞
list 的部分操作支持阻塞和非阻塞。 重点看阻塞--- 当所给定的 key 不存在,或者 key 中包含的是 空列表, 那么 blrpop 命令就会被阻塞链接, 知道另外一个 client 对这些 key 执行 「LR」PSUH 命令将一个新的数据在任意的 key 列表中, 那么这个命令解除调用BLPOP或者 BRPOP la命令 clent 的阻塞状态 。
其实很简单,判断是否超时,或者有数据,否则不返回就可以了。
使用场景
- 常见数据结构
- lpush + lpop = Stack (栈)
- lpush + rpop = Queue (队列)
- lpush + ltrim = Capped Collection (有限集合)
- lpush + brpop = MessageQueue (消息队列)
- 文章列表
常见命令