大家好,我是白晨,一个不是很能熬夜,但是也想日更的人。如果喜欢这篇文章,点个赞👍,关注一下👀白晨吧!你的支持就是我最大的动力!💪💪💪
文章目录
- List源码剖析
- 回顾
- List底层解析
- 概述
- ziplist 与 listpack
- ziplist
- ziplist 的结构
- ziplist 节点结构
- ziplist 的劣势
- listpack
- listpack 的结构
- listpack 元素的结构
- listpack 与 ziplist 结构对比
- listpack 的优势
- quicklist
- 为什么选择 quicklist
- quicklist 结构
- quicklist 节点结构
- quicklist 中的 listpack 到底能存储多少元素?
- lpush是如何执行的?
- 函数调用关系图
- 总结
- List 数据类型的发展历程:
- ziplist
- listpack
- quicklist
- LPUSH 命令的实现
List源码剖析
回顾
首先,我们先复习一下redisObject
和Redis中的内部编码,这里只做简单回顾,详细介绍见《String源码剖析》。
redisObject:
在 Redis 中,redisObject
是用于表示 Redis 数据类型的通用结构。它封装了所有的数据对象,提供了对不同数据类型的一致接口。redisObject
结构体的设计使得 Redis 能够灵活、高效地管理内存和执行操作。下面是 redisObject
的结构:
关于其中每个类项的具体介绍,见《String源码剖析》。
List底层解析
概述
在Redis的发展历程中,List数据类型的底层实现经历了几次重要的演变,从最初的双向链表到后来的快速列表。
在Redis的早期版本(2.2及以前),List类型的数据结构采用的是双向链表(LinkedList)。这种实现方式的主要优点在于其插入和删除操作的时间复杂度为O(1),非常高效,特别适合频繁插入和删除的场景。然而,每个节点需要额外的指针存储空间,导致内存开销较大,并且对于大量数据的线性扫描性能较低。
随着Redis的发展,开发团队意识到需要一种更节省内存的解决方案。因此,在Redis 2.2到3.2版本中,引入了压缩列表(ZipList) 作为List数据类型的另一种底层实现。双向链表继续用于存储较大规模的List,而压缩列表则用于存储元素数量较少或每个元素字节数较小的List。 压缩列表的优点在于其内存紧凑,大大减少了内存开销,但其插入和删除操作的性能较差,因为需要移动大量数据,不适合频繁变更的List。
为了结合双向链表和压缩列表的优点,Redis 3.2版本引入了快速列表(QuickList)。QuickList是一种混合结构,每个节点是一个压缩列表,多个压缩列表通过双向链表连接在一起。这种设计结合了压缩列表的内存紧凑性和双向链表的高效插入、删除操作,在保持内存利用率的同时,提供了更好的操作性能。
在Redis 6.0及以后的版本中,QuickList继续作为List数据结构的主要实现方式。开发团队对QuickList的实现和细节进行了进一步的优化,增强了其性能和内存利用效率。
总结一下:
- Redis 2.2及以前:主要使用双向链表(LinkedList)。
- Redis 2.2至3.2:根据情况选择双向链表(LinkedList)或压缩列表(ZipList)。
- Redis 3.2及以后:主要使用快速列表(QuickList),兼顾内存紧凑性和操作性能。
在详细介绍底层实现前,我们先从上面的redisObject
中的type
和encoding
在List
结构中的取值看起。
注:示例源码如无特殊声明,都来自Redis7。
type
:
#define OBJ_LIST 1 /* List object. */
encoding
:
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* No longer used: old list/hash/zset encoding. */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of listpacks */
#define OBJ_ENCODING_LISTPACK 11 /* Encoded as a listpack */
可以看出,在Redis7
中linkedlist
和ziplist
都已经不再使用,而是使用quicklist
和listpack
。
由于linkedlist
已经久久没有使用了,所以本篇文章将不会介绍它,但是ziplist
在Redis6
中还依然在quicklist
中使用,直到Redis7
才更换为listpack
,所以为了介绍listpack
,ziplist
的介绍是必不可少的。
ziplist 与 listpack
ziplist
与 listpack
详细解析见 《ziplist与listpack源码剖析》这篇文章。这里我对文章的主要内容做一总结,以便于大家理解 list
底层结构。
ziplist
ziplist
是一种以字节数组形式存在的紧凑数据结构,主要用于存储小型列表。它通过减少内存占用,优化存储空间,提高数据访问的效率。ziplist
适用于元素数量较少且不经常变动的场景。
ziplist 的结构
一个 ziplist
由以下几个部分组成:
- zlbytes:
ziplist
的总字节数,4 个字节。 - zltail:到列表尾节点的偏移量,4 个字节。
- zllen:
ziplist
中包含的节点数量,2 个字节。 - entry:实际存储的节点,节点数量根据
zllen
决定。 - zlend:特殊标记,标识
ziplist
的结束,1 个字节,值为 0xFF。
ziplist 节点结构
每个节点简单可以看作三部分组成(具体的实现见下面源码):
- prevlen:前一个节点的长度,用于快速向后遍历,1 或 5 个字节。
- 如果前一个
entry
占用字节数小于 254,那么prevlen
只用 1 个字节来表示就足够了。 - 如果前一个
entry
占用字节数大于等于 254,那么prevlen
就用 5 个字节来表示,其中第 1 个字节的值是 254(作为这种情况的标记),后面 4 个字节存储一个整型值来表示前一个entry
的占用字节数。
- 如果前一个
- encoding:当前节点的编码方式,1 个字节。
- entry-data:实际存储的内容,根据编码方式不同,长度可变。
ziplist 的劣势
- 连锁更新:由于
ziplist
中的元素存储了上一个元素的长度,当插入一个大于等于 254 字节的节点时,后续节点的prevlen
需要扩展为 5 个字节,这会导致连锁更新,直到最后一个节点。 - 紧凑性不足:虽然
ziplist
使用紧凑的编码方式存储数据,但其实现相对复杂,编码和解码的过程需要更多的计算。 - 操作性能不高:
listpack
对插入和删除操作进行了优化,尤其是在处理大数据量的场景下,相比ziplist
有显著的性能提升。 - 逻辑复杂,维护性不高:
ziplist
的实现复杂,编码和内存管理逻辑导致维护和调试难度较大。
listpack
在 Redis 7 之前,ziplist
是一种常用的数据结构,用于实现压缩列表和哈希表中的小型数据。然而,随着 Redis 的发展,ziplist
逐渐被 listpack
取代。listpack
是一种更新、更高效的数据存储结构,专为优化内存使用和提高性能而设计。
listpack
是一个紧凑的、连续的内存块,用于存储一组小的字符串或整数。它的设计目标是通过高效的内存布局和紧凑的编码格式,最大限度地减少内存占用。与 ziplist
相比,listpack
在存储密度和访问效率上都有显著提升。
listpack 的结构
一个 listpack
的基本结构如下:
- Total Bytes:表示整个
listpack
的总字节数(包括自身)。 - Number of Elements:表示
listpack
中元素的数量。 - Entry 1, Entry 2, … Entry N:存储的每个元素,可以是字符串或整数。
- End Byte:结束标志,固定为 0xFF。
listpack 元素的结构
- encoding-type :定义该元素的编码类型,会对不同长度的整数和字符串进行编码。
- element-data:实际存放的数据。
- element-tot-len:整个元素的长度,包含 encoding + data 的长度,用于反向遍历。
listpack 与 ziplist 结构对比
listpack 的优势
- 无连锁更新:现在每个
listpack
元素都只存储自己的长度,不会发生像ziplist
那样插入一个 254 字节及以上的元素引起的连锁更新。 - 内存效率:
listpack
通过紧凑的编码方式大幅减少了内存占用,特别是对小整数和短字符串的存储进行了优化。 - 操作性能:改进了内存布局,使得插入和删除操作更加高效,避免了大规模的数据移动,提升了操作性能。
- 兼容性和扩展性:作为新的数据结构,
listpack
设计时考虑了更多的扩展性和兼容性问题,能够更好地适应 Redis 的未来发展需求。
通过对 ziplist
和 listpack
的理解,可以更好地了解 Redis 中 quicklist
的设计和性能改进。
quicklist
在 Redis 中,quicklist
是一种高效的数据结构,用于实现列表类型的数据存储。quicklist
结合了 listpack
和双向链表(linked list)的优势,旨在优化内存使用和操作性能。
为什么选择 quicklist
Redis 的列表类型数据在使用过程中会面临两种需求:
- 存储大量的小元素时,希望减少内存开销。
- 对列表进行频繁的插入、删除和访问操作时,希望保证操作的高效性。
传统的 ziplist
虽然在内存使用上表现不错,但在处理大量数据和频繁操作时性能较差。而双向链表则在频繁操作上表现良好,但内存开销较大。为了解决这些问题,Redis 引入了 quicklist
。
quicklist 结构
quicklist
是一种结合了 listpack
和双向链表的混合结构。它将多个 listpack
节点组成一个双向链表,每个链表节点存储一个 listpack
。
Quicklist
+-------+ +-------+ +-------+
| Node1 |<-> | Node2 |<-> | Node3 |
+-------+ +-------+ +-------+
| | |
v v v
+---------+ +---------+ +---------+
| Listpack| | Listpack| | Listpack|
+---------+ +---------+ +---------+
quicklist
包含以下字段:
head
:指向第一个quicklistNode
的指针。tail
:指向最后一个quicklistNode
的指针。count
:所有listpack
中所有条目的总计数。len
:quicklistNode
的数量。fill
:各个节点的填充因子。compress
:两端不压缩的节点深度,0 表示不压缩。bookmark_count
:书签的数量。bookmarks[]
:书签数组,用于快速访问特定位置。
源码如下:
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count; /* total count of all entries in all listpacks */
unsigned long len; /* number of quicklistNodes */
signed 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;
quicklist 节点结构
每个 quicklist
节点存储一个 listpack
,并包含以下字段:
prev
:指向前一个节点的指针。next
:指向下一个节点的指针。entry
:指向listpack
数据的指针。sz
:当前listpack
的字节大小。count
:当前listpack
中的元素个数。encoding
:编码方式。container
:容器类型。recompress
:用于重新压缩的标志。attempted_compress
:重试压缩的次数。extra
:额外的标志位。
源码如下:
typedef struct quicklistNode {
struct quicklistNode *prev;
struct quicklistNode *next;
unsigned char *entry;
size_t sz; /* entry size in bytes */
unsigned int count : 16; /* count of items in listpack */
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
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 */
unsigned int dont_compress : 1; /* prevent compression of entry that will be used later */
unsigned int extra : 9; /* more bits to steal for future usage */
} quicklistNode;
quicklist 中的 listpack 到底能存储多少元素?
每个quicklistNode
中都有一个listpack
,但是这个listpack
到底能存储多少个数据呢?太多会影响插入删除效率,太少又无法压缩多少空间。所以,这个中间值很难决定,要结合实际的应用场景来决定。
使用者可以在客户端或者Redis
服务端的配置文件中使用list-max-ziplist-size
这个参数来决定quicklistNode
中的listpack
到底可以存储多少数据。
我们可以在Redis客户端查询一下相关的参数:
127.0.0.1:6379> config get list*
1) "list-max-listpack-size"
2) "-2"
3) "list-compress-depth"
4) "0"
5) "list-max-ziplist-size"
6) "-2"
- list-max-listpack-size
这个参数定义了 listpack
在 quicklist
中的最大大小。取值可以是负值或者正值:
- 负值:表示每个
listpack
的最大字节数。取值范围通常是-1
到-9
,其中:-1
表示listpack
的最大字节数为 2 1 2^1 21 = 2 KB。-2
表示listpack
的最大字节数为 2 2 2^2 22 = 4 KB。-3
表示listpack
的最大字节数为 2 3 2^3 23 = 8 KB。- 以此类推,
-9
表示listpack
的最大字节数为 2 9 2^9 29 = 512 KB。
- 正值:表示每个
listpack
中允许的最大元素个数。具体值可以根据应用场景和需求设置,例如1
表示每个listpack
中最多存储 1 个元素,10
表示最多存储 10 个元素。
- list-compress-depth
这个参数定义了 quicklist
两端不压缩的节点深度。取值为非负整数:
- 0:表示禁用压缩,即不压缩任何节点。
- 正整数:表示
quicklist
的两端有多少个节点不会被压缩。例如:1
表示quicklist
的头部和尾部各保留一个未压缩的节点,其他节点可以被压缩。2
表示头部和尾部各保留两个未压缩的节点,其他节点可以被压缩。- 以此类推,具体值可以根据性能和内存需求进行调整。
- list-max-ziplist-size
这是一个旧参数,已经被list-max-listpack-size取代,与list-max-listpack-size用法相同。
list-max-listpack-size这个参数在Redis内部实现中就是quicklist
中的fill
类项,这两个项的取值和意义都是相同的。
lpush是如何执行的?
首先,先来看源码中lpush
对应的函数:
- lpushCommand:这个函数是 Redis 中 LPUSH 命令的具体实现。它调用
pushGenericCommand
函数,并传递参数以表示应将元素推入列表的头部。 - pushGenericCommand:这是一个更通用的函数,用于实现多个类似的命令(LPUSH、RPUSH、LPUSHX、RPUSHX)。它接受参数来确定将元素推入列表的头部(
LIST_HEAD
)还是尾部(LIST_TAIL
),以及是否仅在键存在时执行推送操作(xx
参数)。- 查找键:使用
lookupKeyWrite
在数据库中查找键c->argv[1]
。 - 类型检查:使用
checkType
检查键是否存在以及是否为列表类型。如果类型不匹配,则返回。 - 处理不存在的键:
- 如果键不存在且
xx
标志被设置(表示仅在键存在时推送),则发送零回复给客户端并返回。 - 如果键不存在且
xx
未设置,则创建一个新的列表对象,并使用dbAdd
将其添加到数据库中。
- 如果键不存在且
- 推送元素:
- 调用
listTypeTryConversionAppend
尝试转换列表的内部表示。 - 使用
listTypePush
将从c->argv[2]
开始的元素逐个推入列表的头部或尾部(根据where
参数决定)。 - 每次推送元素后,增加服务器的脏计数器
server.dirty
。
- 调用
- 查找键:使用
/* LPUSH <key> <element> [<element> ...] */
void lpushCommand(client *c) {
pushGenericCommand(c,LIST_HEAD,0);
}
/* Implements LPUSH/RPUSH/LPUSHX/RPUSHX.
* 'xx': push if key exists. */
void pushGenericCommand(client *c, int where, int xx) {
int j;
robj *lobj = lookupKeyWrite(c->db, c->argv[1]);
if (checkType(c,lobj,OBJ_LIST)) return;
if (!lobj) {
if (xx) {
addReply(c, shared.czero);
return;
}
lobj = createListListpackObject();
dbAdd(c->db,c->argv[1],lobj);
}
listTypeTryConversionAppend(lobj,c->argv,2,c->argc-1,NULL,NULL);
for (j = 2; j < c->argc; j++) {
listTypePush(lobj,c->argv[j],where);
server.dirty++;
}
addReplyLongLong(c, listTypeLength(lobj));
char *event = (where == LIST_HEAD) ? "lpush" : "rpush";
signalModifiedKey(c,c->db,c->argv[1]);
notifyKeyspaceEvent(NOTIFY_LIST,event,c->argv[1],c->db->id);
}
其次,我们来看看createListListpackObject
这个创建listpack
的函数:
createListListpackObject
函数用于创建一个新的列表对象,并将其初始化为一个空的 listpack。
robj *createListListpackObject(void) {
unsigned char *lp = lpNew(0);
robj *o = createObject(OBJ_LIST,lp);
o->encoding = OBJ_ENCODING_LISTPACK;
return o;
}
可以看到,Redis7
中不直接创建一个quicklist
,而是先创建一个listpack
,当超过一个listpack
的最大长度后才将其转换为quicklist
,这与Redis6
中直接创建quicklist
的做法不同。
再者,从listTypeTryConversionAppend
这个函数开始向下继续挖,可以看到listpack
是如何进行内部类型的转换的:
当列表以 listpack
编码存储并且即将添加的元素可能导致其超过配置的大小限制时,系统会自动将列表从 listpack
转换为 quicklist
编码。这种转换确保了列表在大小增长时的高效存储和操作,从而优化了 Redis 的性能和内存使用。函数通过计算要添加的元素的总字节数和长度,并在必要时执行转换操作,以实现这一目的。
/* This is just a wrapper for listTypeTryConversionRaw() that is
* able to try conversion before adding elements to the list. */
void listTypeTryConversionAppend(robj *o, robj **argv, int start, int end,
beforeConvertCB fn, void *data)
{
listTypeTryConversionRaw(o, LIST_CONV_GROWING, argv, start, end, fn, data);
}
/* Check if the list needs to be converted to appropriate encoding due to
* growing, shrinking or other cases. */
static void listTypeTryConversionRaw(robj *o, list_conv_type lct,
robj **argv, int start, int end,
beforeConvertCB fn, void *data)
{
if (o->encoding == OBJ_ENCODING_QUICKLIST) {
if (lct == LIST_CONV_GROWING) return; /* Growing has nothing to do with quicklist */
listTypeTryConvertQuicklist(o, lct == LIST_CONV_SHRINKING, fn, data);
} else if (o->encoding == OBJ_ENCODING_LISTPACK) {
if (lct == LIST_CONV_SHRINKING) return; /* Shrinking has nothing to do with listpack */
listTypeTryConvertListpack(o, argv, start, end, fn, data);
} else {
serverPanic("Unknown list encoding");
}
}
/* Check the length and size of a number of objects that will be added to list to see
* if we need to convert a listpack to a quicklist. Note that we only check string
* encoded objects as their string length can be queried in constant time.
*
* If callback is given the function is called in order for caller to do some work
* before the list conversion. */
static void listTypeTryConvertListpack(robj *o, robj **argv, int start, int end,
beforeConvertCB fn, void *data)
{
serverAssert(o->encoding == OBJ_ENCODING_LISTPACK);
size_t add_bytes = 0;
size_t add_length = 0;
if (argv) {
for (int i = start; i <= end; i++) {
if (!sdsEncodedObject(argv[i]))
continue;
add_bytes += sdslen(argv[i]->ptr);
}
add_length = end - start + 1;
}
if (quicklistNodeExceedsLimit(server.list_max_listpack_size,
lpBytes(o->ptr) + add_bytes, lpLength(o->ptr) + add_length))
{
/* Invoke callback before conversion. */
if (fn) fn(data);
quicklist *ql = quicklistCreate();
quicklistSetOptions(ql, server.list_max_listpack_size, server.list_compress_depth);
/* Append listpack to quicklist if it's not empty, otherwise release it. */
if (lpLength(o->ptr))
quicklistAppendListpack(ql, o->ptr);
else
lpFree(o->ptr);
o->ptr = ql;
o->encoding = OBJ_ENCODING_QUICKLIST;
}
}
最后,通过listTypePush
这个函数,看看lpush
是如何添加元素到list
的:
listTypePush
函数的功能是将一个元素插入指定的列表对象 subject
中,位置由 where
参数指定,可以是头部(LIST_HEAD
)或尾部(LIST_TAIL
)。函数会根据列表对象的编码类型(quicklist
或 listpack
)以及元素的编码类型(整数或字符串)来决定如何进行插入操作。
对于 quicklist
编码的列表,函数将整数值转换为字符串后插入;对于 listpack
编码的列表,函数直接插入整数值或字符串值。
/* The function pushes an element to the specified list object 'subject',
* at head or tail position as specified by 'where'.
*
* There is no need for the caller to increment the refcount of 'value' as
* the function takes care of it if needed. */
void listTypePush(robj *subject, robj *value, int where) {
if (subject->encoding == OBJ_ENCODING_QUICKLIST) {
int pos = (where == LIST_HEAD) ? QUICKLIST_HEAD : QUICKLIST_TAIL;
if (value->encoding == OBJ_ENCODING_INT) {
char buf[32];
ll2string(buf, 32, (long)value->ptr);
quicklistPush(subject->ptr, buf, strlen(buf), pos);
} else {
quicklistPush(subject->ptr, value->ptr, sdslen(value->ptr), pos);
}
} else if (subject->encoding == OBJ_ENCODING_LISTPACK) {
if (value->encoding == OBJ_ENCODING_INT) {
subject->ptr = (where == LIST_HEAD) ?
lpPrependInteger(subject->ptr, (long)value->ptr) :
lpAppendInteger(subject->ptr, (long)value->ptr);
} else {
subject->ptr = (where == LIST_HEAD) ?
lpPrepend(subject->ptr, value->ptr, sdslen(value->ptr)) :
lpAppend(subject->ptr, value->ptr, sdslen(value->ptr));
}
} else {
serverPanic("Unknown list encoding");
}
}
函数调用关系图
总结
List 数据类型的发展历程:
- Redis 2.2 及以前:使用双向链表(linkedlist)。
- Redis 2.2 到 3.2:根据情况使用双向链表或压缩列表(ziplist)。
- Redis 3.2 及以后:引入快速列表(quicklist),结合了压缩列表和双向链表的优点。
- Redis 7:Listpack 取代了 Ziplist,成为新的数据存储结构。
ziplist
- ziplist 是一种紧凑的字节数组,用于存储小型列表,减少内存占用。
- ziplist 结构包括总字节数、尾节点偏移量、节点数量、实际存储的节点和结束标记。
- ziplist 的劣势包括连锁更新、紧凑性不足、操作性能不高和维护复杂性。
listpack
- listpack 是一种更新的、更高效的数据存储结构,设计目标是通过高效的内存布局和紧凑的编码格式,最大限度地减少内存占用。
- listpack 结构包括总字节数、元素数量、实际存储的元素和结束标记。
- listpack 相比 Ziplist 具有无连锁更新、内存效率高、操作性能好、兼容性和扩展性强的优势。
quicklist
- quicklist 结合了 Listpack 和双向链表的优点,旨在优化内存使用和操作性能。
- quicklist 结构由多个 Listpack 组成的双向链表,每个节点存储一个 listpack。
- quickList 提供了高效的内存利用和快速的插入、删除操作。
LPUSH 命令的实现
- 详细解析了 LPUSH 命令的实现过程,包括创建新的 Listpack 对象、转换内部类型和推入元素等步骤。
如果讲解有不对之处还请指正,我会尽快修改,多谢大家的包容。
如果大家喜欢这个系列,还请大家多多支持啦😋!
如果这篇文章有帮到你,还请给我一个大拇指
👍和小星星
⭐️支持一下白晨吧!喜欢白晨【Redis】系列的话,不如关注
👀白晨,以便看到最新更新哟!!!
我是不太能熬夜的白晨,我们下篇文章见。