Redis数据类型之List
Posted 猿祖
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis数据类型之List相关的知识,希望对你有一定的参考价值。
前言:list即链表,它是一个能维持数据先后顺序的列表,便于在表的两端追加和删除数据,中间位置的存取具有O(N)的时间复杂度,是一个双向链表。
一、内部原理
redis内部实现代码在quicklist.c(注释:A doubly linked list of ziplists)中,它确实是一个双向链表,并且是一个ziplist双向列表。
ziplist是什么?
一个经过特殊编码的的双向链表,它的设计目的是为了提高存储效率。ziplist可以用于存储字符串或整数,其中整数是真正的二进制进行编码的,而不是编码成字符串序列。普通的双向链表每一项都独立的占用一块内存,各项之间用地址指针连接起来。这中方式会带来大量的内存碎片,而且地址指针也会占用额外的内存。而ziplist将列表的每一项存放在前后连续的地址空间内,一个大的ziplist整体占用一大块内存,它是一个列表,但不是一个链表。ziplist为了在细节上节省内存,对于值的存储采用了变长的编码方式,对于大的整数,就多一些字节来存储,对于小的少一些字节来存储。
ziplist的数据结构如下:
<zlbytes><zltail><zllen><entry>...<entry><zlend>
含义:
<zlbytes>:32字节,表示ziplist占用的字符总数(本身占用4个字节)。
<zltail>: 32字节,表示ziplist表中最后一项(entry)在ziplist中的偏移字节数。<zltail>的存在,使得我们可以很方便地找到最后一项,从而可以在ziplist尾端快速地执行push或pop操作。
<zllen> : 16字节, 表示ziplist中数据项(entry)的个数。zllen字段因为只有16bit,所以可以表达的最大值为2^16-1。这里需要特别注意的是,如果ziplist中数据项个数超过了16bit能表达的最大值,ziplist仍然可以来表示。那怎么表示呢?这里做了这样的规定:如果<zllen>小于等于2^16-2(也就是不等于2^16-1),那么<zllen>就表示ziplist中数据项的个数;否则,也就是<zllen>等于16bit全为1的情况,那么<zllen>就不表示数据项个数了,这时候要想知道ziplist中数据项总数,那么必须对ziplist从头到尾遍历各个数据项,才能计数出来。
<entry> : 表示真正存放数据的数据项,长度不定。一个数据项(entry)也有它自己的内部结构。
<zlend> : ziplist最后1个字节,是一个结束标记,值固定等于255。
entry的数据结构:
<prevrawlen><len><data>
<prevrawlen>: 表示前一个数据项占用的总字节数。这个字段的用处是为了让ziplist能够从后向前遍历(从后一项的位置,只需向前偏移prevrawlen个字节,就找到了前一项)。这个字段采用变长编码。
<prevrawlen>。它有两种可能,或者是1个字节,或者是5个字节:
1. 如果前一个数据项占用字节数小于254,那么<prevrawlen>就只用一个字节来表示,这个字节的值就是前一个数据项的占用字节数。
2. 如果前一个数据项占用字节数大于等于254,那么<prevrawlen>就用5个字节来表示,其中第1个字节的值是254(作为这种情况的一个标记),而后面4个字节组成一个整型值,来真正存储前一个数据项的占用字节数。
<len>: 表示当前数据项的数据长度(即<data>部分的长度)。也采用变长编码。根据第一个字节的不同分为下面九种方式
|00pppppp| - 1 byte String value with length less than or equal to 63 bytes (6 bits).
|01pppppp|qqqqqqqq| - 2 bytes String value with length less than or equal to 16383 bytes (14 bits).
|10______|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5 bytes String value with length greater than or equal to 16384 bytes.
|11000000| - 1 byte Integer encoded as int16_t (2 bytes).
|11010000| - 1 byte Integer encoded as int32_t (4 bytes).
|11100000| - 1 byte Integer encoded as int64_t (8 bytes).
|11110000| - 1 byte Integer encoded as 24 bit signed (3 bytes).
|11111110| - 1 byte Integer encoded as 8 bit signed (1 byte).
|1111xxxx| - (with xxxx between 0000 and 1101) immediate 4 bit integer. Unsigned integer from 0 to 12. The encoded value is actually from 1 to 13 because 0000 and 1111 can not be used, so 1 should be subtracted from the encoded 4 bit value to obtain the right value.
|11111111| - End of ziplist.
quicklist是什么?
双向链表都是有多个node组成,而quicklist的每个节点都是一个ziplist。ziplist本身也是一个能维持数据项先后顺序的列表,而且内存是紧凑的,例如一个包含2个node的quicklist,如果每个节点的ziplist包含了四个数据项
那么对外表现就是8个数据项。quicklist的设计是一个空间和时间的折中,双向链表便于在表的两端进行push和pop操作,但是它的内存开销很大。开销如下
1.每个节点上除了要保存数据之外,还要额外的保存两个指针。
2.各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。
ziplist是一块连续的内存,所以存储效率很高。但是,它不利于修改操作,每次数据变动都会引发内存的realloc。一次realloc可能会导致大量的数据拷贝,进一步降低性能。
quicklist结合了双向链表和ziplist的优点,但是同样也存在一个问题,一个quicklist包含多长的ziplist合适呢?需要找到一个平衡点
1.ziplist太短,内存碎片越多。
2.ziplist太长,分配大块连续内存空间的难度就越大。
如果保持ziplist的合理长度,取决于具体的应用场景。redis提供了默认配置
list-max-ziplist-size -2
参数的含义解释,取正值时表示quicklist节点ziplist包含的数据项。取负值表示按照占用字节来限定quicklist节点ziplist的长度。
-5: 每个quicklist节点上的ziplist大小不能超过64 Kb。
-4: 每个quicklist节点上的ziplist大小不能超过32 Kb。
-3: 每个quicklist节点上的ziplist大小不能超过16 Kb。
-2: 每个quicklist节点上的ziplist大小不能超过8 Kb。(默认值)
-1: 每个quicklist节点上的ziplist大小不能超过4 Kb。
list设计最容易被访问的是列表两端的数据,中间的访问频率很低,如果符合这个场景,list还有一个配置,可以对中间节点进行压缩(采用的LZF——一种无损压缩算法),进一步节省内存。配置如下
list-compress-depth 0
含义:
0: 是个特殊值,表示都不压缩。这是Redis的默认值。
1: 表示quicklist两端各有1个节点不压缩,中间的节点压缩。
2: 表示quicklist两端各有2个节点不压缩,中间的节点压缩。
以此类推
quicklist数据结构:
/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist. * We use bit fields keep the quicklistNode at 32 bytes. * count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k). * encoding: 2 bits, RAW=1, LZF=2. * container: 2 bits, NONE=1, ZIPLIST=2. * recompress: 1 bit, bool, true if node is temporarry decompressed for usage. * attempted_compress: 1 bit, boolean, used for verifying during testing. * extra: 12 bits, free for future use; pads out the remainder of 32 bits */ typedef struct quicklistNode { struct quicklistNode *prev; /*指向链表前一个节点的指针*/ struct quicklistNode *next; /*指向链表后一个节点的指针*/ unsigned char *zl;/*数据指针。如果当前节点的数据没有压缩,那么它指向一个ziplist结构;否则,它指向一个quicklistLZF结构。*/ unsigned int sz; /*表示zl指向的ziplist的总大小(包括zlbytes, zltail, zllen, zlend和各个数据项)。需要注意的是:如果ziplist被压缩了,那么这个sz的值仍然是压缩前的ziplist大小。/* unsigned int count : 16; /* 表示ziplist里面包含的数据项个数。 */ unsigned int encoding : 2; /* RAW==1(未压缩) or LZF==2 (压缩了并采用LZF压缩算法)*/ unsigned int container : 2; /* 使用的容器 NONE==1 or ZIPLIST==2(默认值) */ unsigned int recompress : 1; /* 我们使用类似lindex这样的命令查看了某一项本来压缩的数据时,需要把数据暂时解压,这时就设置recompress=1做一个标记,等有机会再把数据重新压缩 */ unsigned int attempted_compress : 1; /* node can‘t compress; too small */ unsigned int extra : 10; /* 其他扩展字段(未使用) */ } quicklistNode; /* quicklistLZF is a 4+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->zl is compressed, node->zl points to a quicklistLZF */ typedef struct quicklistLZF { unsigned int sz; /* 表示压缩后的ziplist大小*/ char compressed[]; /*是个柔性数组(flexible array member),存放压缩后的ziplist字节数组/* } quicklistLZF; /* quicklist is a 32 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: -1 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. */ typedef struct quicklist { quicklistNode *head; ?/*指向头节点(左侧第一个节点)的指针。*/ quicklistNode *tail; /*指向尾节点(右侧第一个节点)的指针。*/ unsigned long count; /* quicklist节点的个数 */ unsigned int len; /* number of quicklistNodes */ int fill : 16; /* ziplist大小设置,存放list-max-ziplist-size参数的值 */ unsigned int compress : 16; /* 节点压缩深度设置,存放list-compress-depth参数的值 */ }
二:相关命令
lpush key value[value...] 将一个或多个value插入到列表的表头,如果有多个 value 值,那么各个 value 值按从左到右的顺序依次插入到表头: 比如说,对空列表 mylist 执行命令 LPUSH mylist a b c,列表的值将是 c b a ,这等同于原子性地执行 LPUSH mylist a 、 LPUSH mylist b 和 LPUSH mylist c 三个命令。如果 key 不存在,一个空列表会被创建并执行 lpush 操作。当 key 存在但不是列表类型时,返回一个错误。
lpushx key value 将值 value 插入到列表 key 的表头,若key不存在,不执行任何操作。
lpop key 移除并返回列表key的头元素(后进先出),若key不存在返回nil。
blpop key[key...] timeout lpop的阻塞版本,若给定列表中没有任何元素可供弹出时,链接会被blpop命令阻塞,直到等待超时(单位:秒)或发现可弹出元素时为止,若发现其中任何一个列表中有值则返回列表key和第一个元素的值。
rpush key value[value...] 将一个或多个值 value 插入到列表 key 的表尾(最右边),如果有多个 value 值,那么各个 value 值按从左到右的顺序依次插入到表尾:比如对一个空列表 mylist 执行 RPUSH mylist a b c ,得出的结果列表为 a b c ,等同于执行命令 RPUSH mylist a 、 RPUSH mylist b 、 RPUSH mylist c 。如果 key 不存在,一个空列表会被创建并执行 Rpush 操作。当 key 存在但不是列表类型时,返回一个错误。
rpushx key value 将值 value 插入到列表 key 的表尾,若key不存在,不执行任何操作。
rpop key 移除并返回列表的末尾,若key不存在则返回nil。
brpop key[key...] timeout 它是 rpop命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 brpop 命令阻塞,直到等待超时或发现可弹出元素为止。当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的尾.
rpoplpush source destination 命令 rpoppush 在一个原子时间内,执行以下两个动作:将列表 source 中的最后一个元素(尾元素)弹出,并返回给客户端。将 source 弹出的元素插入到列表 destination ,作为 destination 列表的的头元素。如果 source 不存在,值 nil 被返回,并且不执行其他动作。如果 source 和 destination 相同,则列表中的表尾元素被移动到表头,并返回该元素,可以把这种特殊情况视作列表的旋转(rotation)操作。
brpoplpush source destination brpoplpush是 rpoplpush的阻塞版本,当给定列表 source 不为空时, brpoplpush 的表现和 rpoplpush 一样。当列表 source 为空时, brpoplpush 命令将阻塞连接,直到等待超时,或有另一个客户端对 source 执行 lpush或 rpush 命令为止。超时参数 timeout 接受一个以秒为单位的数字作为值。超时参数设为 0 表示阻塞时间可以无限期延长(block indefinitely)
lset key index value 将列表 key 下标为 index 的元素的值设置为 value 。当 index 参数超出范围,或对一个空列表( key 不存在)进行 lset时,返回一个错误。
linsert key before|after pivot value 将值 value 插入到列表 key 当中,位于值 pivot 之前或之后。当 pivot 不存在于列表 key 时,不执行任何操作。当 key 不存在时, key 被视为空列表,不执行任何操作。如果 key 不是列表类型,返回一个错误。
llen key 返回列表 key 的长度。如果 key 不存在,则 key 被解释为一个空列表,返回 0 .如果 key 不是列表类型,返回一个错误。
lindex key index 返回列表 key 中,下标为 index 的元素。下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。如果 key 不是列表类型,返回一个错误。
lrange key start stop 返回列表 key 中指定区间内的元素,区间以偏移量 start 和 stop 指定。下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。
ltrim key start stop 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。当 key 不是列表类型时,返回一个错误。
lrem key count value 移除列表中与value相等的元素,若count>0从左到右移除与count个与value相等的元素;若count<0从右向左移除count个与value相等的元素;若count==0移除所有与value相等的元素。
以上是关于Redis数据类型之List的主要内容,如果未能解决你的问题,请参考以下文章