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的主要内容,如果未能解决你的问题,请参考以下文章

Redis五大数据类型之List

Redis 基础 -- Redis数据类型之list

《闲扯Redis三》Redis五种数据类型之List型

系统学习redis之五——redis数据类型之list类型及操作

redis数据类型之list

redis数据类型之list