Redis 03_Redis列表Lists

一、Redis中列表Lists数据类型简介

1.1 列表(Lists)简介

Redis 的 列表(Lists) 类型是用来存储多个有序的字符串,列表中的每个字符串称为元素(element),列表中元素可以重复出现,一个列表最多可以存储 2^32^ - 1 个元素。

列表(Lists)是基于Linked List实现的(相当于 Java 语言里面的 LinkedList),注意它是链表而不是数组,而且是双向链表,这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为O(n)。

在 Redis 中,可以对列表两端插人(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。列表是⼀种比较灵活的数据结构,它可以充当 队列 的角色,在实际开发上有很多应用场景。

向 Lists 中插入元素时,如果该键不存在,redis 将为该键创建一个新的链表。与此相反,如果链表中所有的元素均被移除,那么该键也将会被从数据库删除。

列表两端插入和弹出操作: 列表的索引,从左至右,从0开始;从右至左,从-1开始;

列表的获取、删除等操作: 列表获取和删除的区别,如上图 中的 lrem 1 b 是从列表中把从左数遇到的前 1 个 b 元素删除,这个操作会导致列表的长度从 5 变成 4;但是执行 lindex 4 只会获取元素,但列表长度不会变化。

列表(Lists)类型的特点

  • 列表中的元素是有序的(按插入顺序),可以通过索引下标获取某个元素或者某个范围的元素列表;
  • 列表中的元素是允许重复的;

1.2 Lists 类型内部编码及底层结构介绍

在 Redis3.2 版本前,列表 Lists 使用两种数据结构作为底层实现:

  • 压缩列表 ZipList:插入元素过多或字符串太大,就需要调用 Realloc 扩展内存;
  • 双向链表 LinkedList:需附加指针 Prev 和 Next,较浪费空间,加重内存的碎片化

Redis3.2 首先以 ZipList 进行存储,在不满足 ZipList 的存储要求后转换为 LinkedList 列表。当列表对象同时满足以下两个条件时,使用 ZipList 进行存储,否则用 LinkedList 存储。

  • 列表对象保存的所有字符串元素的长度小于 64 字节;
  • 列表对象保存的元素数量小于 512 个;

在 Redis3.2 版本后,列表使用 快速链表 QucikList 结构作为底层实现

1、快速链表QucikList

Tips: Redis源码剖析之快速列表(quicklist): https://zhuanlan.zhihu.com/p/266627741

Redis3.2 版本开始,Lists 类型数据使用的底层数据结构是 快速链表(QucikList), 它是 ziplist、linkedlist结合的版本,快速链表(QucikList) 以压缩列表为节点的双向链表,将双向链表按段切分,每一段使用压缩列表进行内存的连续存储,多个压缩列表通过 prev 和 next 指针组成双向链: 考虑到链表的缺点,Redis 后续版本对 Lists 列表数据结构进行改造,使用 QucikList 代替了 ZipList 和 LinkedList。作为 ZipList 和 LinkedList 的混合体,它将 LinkedList 按段切分,每一段使用 ZipList 来紧凑存储,多个 ZipList 之间使用双向指针串接起来。这样,性能就的得到了更大的提升。

quicklist 中每个链表节点中保存一个 ziplist,然后每个 ziplist 中存一批 list 中的元素数据 (具体 ziplist 大小可配置),这样既可以避免大量链表指针带来的内存消耗,也可以避免 ziplist 更新导致的大量性能损耗,将大的 ziplist 化整为零。

quicklist 结构体定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
typedef struct quicklist {
    quicklistNode *head; /* 头结点 */
    quicklistNode *tail; /* 尾结点 */
    unsigned long count; /* 在所有的ziplist中的entry总数 */
    unsigned long len; /* quicklist节点总数 */
    int fill : QL_FILL_BITS; /* 16位,每个节点的最大容量 */
    unsigned int compress : QL_COMP_BITS; /* 16位,quicklist的压缩深度,0表示所有节点都不压缩,否则就表示从两端开始有多少个节点不压缩 */
    unsigned int bookmark_count: QL_BM_BITS; /*4位,bookmarks数组的大小,bookmarks是一个可选字段,用来quicklist重新分配内存空间时使用,不使用时不占用空间*/
    quicklistBookmark bookmarks [];
} quicklist;

其中,fill 和 compress 是两个重要的字段,它们决定了 quicklist 的内存和性能特性。

  • fill 表示每个 quicklistNode 节点的最大容量,不同的数值有不同的含义,默认是 -2,当然也可以配置为其它数值,具体数值含义如下:
    • -1|每个 quicklistNode 节点的 ziplist 所占字节数不能超过 4kb。
    • -2|每个 quicklistNode 节点的 ziplist 所占字节数不能超过 8kb。 (默认配置&建议配置)
    • -3|每个 quicklistNode 节点的 ziplist 所占字节数不能超过 16kb。
    • -4|每个 quicklistNode 节点的 ziplist 所占字节数不能超过 32kb。
    • -5|每个 quicklistNode 节点的 ziplist 所占字节数不能超过 64kb。
    • 任意正数|表示:ziplist 结构所最多包含的 entry 个数,最大为 215215。

quicklistNode 结构体定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
typedef struct quicklistNode {
    struct quicklistNode *prev;
    struct quicklistNode *next;
    unsigned char *zl; /* quicklist节点对应的ziplist */
    unsigned int sz; /* ziplist的字节数 */
    unsigned int count : 16; /* ziplist的item数*/
    unsigned int encoding : 2; /* 数据类型,RAW==1 or LZF==2 */
    unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
    unsigned int recompress : 1; /* 这个节点以前压缩过吗? */
    unsigned int attempted_compress : 1; /* node can't compress; too small */
    unsigned int extra : 10; /* 未使用到的10位 */
} quicklistNode;

其中,zl 指向了一个 ziplist 结构,encoding 表示该 ziplist 是否被 LZF 压缩过,recompress 表示该节点是否需要重新压缩。

quicklist 的常用操作 创建一个 quicklist 的函数如下: fill值默认是-2,也就是说每个quicklistNode中的ziplist最长是8k字节,可以根据业务需求调整具体配置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/* 创建一个新的quicklist.
 * 使用quicklistRelease()释放quicklist. */
quicklist *quicklistCreate(void) {
    struct quicklist *quicklist;

    quicklist = zmalloc(sizeof(*quicklist));
    quicklist->head = quicklist->tail = NULL;
    quicklist->len = 0;
    quicklist->count = 0;
    quicklist->compress = 0;
    quicklist->fill = -2;
    quicklist->bookmark_count = 0;
    return quicklist;
}

对于 list 而已,头部或者尾部插入是最常见的操作了,但其实头插和尾插还算是比较简单。 头插和尾插都调用了_quicklistNodeAllowInsert先判断了是否能直接在当前头|尾节点能插入,如果能就直接插入到对应的ziplist里,否则就需要新建一个新节点再操作了。 还记得上文中我们说的fill字段吗,_quicklistNodeAllowInsert其实就是根据fill的具体值来判断是否已经超过最大容量。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/* 在quicklist的头部插入一条数据  
 * 如果在已存在节点插入,返回0
 * 如果是在新的头结点插入,返回1 */
int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
    quicklistNode *orig_head = quicklist->head;
    if (likely(
            _quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
        quicklist->head->zl =
            ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);  // 在头结点对应的ziplist中插入 
        quicklistNodeUpdateSz(quicklist->head);
    } else { // 否则新建一个头结点,然后插入数据 
        quicklistNode *node = quicklistCreateNode();
        node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);

        quicklistNodeUpdateSz(node);
        _quicklistInsertNodeBefore(quicklist, quicklist->head, node);
    }
    quicklist->count++;
    quicklist->head->count++;
    return (orig_head != quicklist->head);
}

/* 在quicklist的尾部插入一条数据  
 * 如果在已存在节点插入,返回0
 * 如果是在新的头结点插入,返回1 */
int quicklistPushTail(quicklist *quicklist, void *value, size_t sz) {
    quicklistNode *orig_tail = quicklist->tail;
    if (likely(
            _quicklistNodeAllowInsert(quicklist->tail, quicklist->fill, sz))) {
        quicklist->tail->zl =
            ziplistPush(quicklist->tail->zl, value, sz, ZIPLIST_TAIL);
        quicklistNodeUpdateSz(quicklist->tail);
    } else {
        quicklistNode *node = quicklistCreateNode();
        node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_TAIL);

        quicklistNodeUpdateSz(node);
        _quicklistInsertNodeAfter(quicklist, quicklist->tail, node);
    }
    quicklist->count++;
    quicklist->tail->count++;
    return (orig_tail != quicklist->tail);
}

对于 list 而已,头部或者尾部删除也是很常见的操作了,但其实头删和尾删也还算是比较简单。

1
int quicklistPop(quicklist *quicklist, int where, unsigned char **data, size_t *sz, long long *slong); // 头删或尾删

查找操作需要遍历整个 quicklist,对每个 ziplist 进行查找。如果找到了匹配的元素,就返回一个 quicklistEntry 结构体,表示该元素在哪个 ziplist 中,以及在 ziplist 中的位置。

1
2
3
4
5
int quicklistIndex(const quicklist *quicklist, const long long idx, quicklistEntry *entry); // 根据索引查找元素
void quicklistRewind(const quicklist *quicklist, quicklistIter **iter); // 创建一个从头开始的迭代器
void quicklistRewindTail(const quicklist *quicklist, quicklistIter **iter); // 创建一个从尾开始的迭代器
int quicklistNext(quicklistIter *iter, quicklistEntry *node); // 迭代器获取下一个元素
void quicklistReleaseIterator(quicklistIter *iter); // 释放迭代器

删除操作需要先找到要删除的元素在哪个 ziplist 中,然后调用 ziplist 的删除函数删除该元素。如果删除后导致 ziplist 空了,就把整个 ziplist 节点从链表中删除。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void quicklistDelEntry(quicklistIter *iter, quicklistEntry *entry) { // 删除迭代器指向的元素
    quicklistNode *prev = entry->node->prev;
    quicklistNode *next = entry->node->next;
    int deleted_node = quicklistDelIndex((quicklist *)entry->quicklist,
                                         entry->node, &entry->zi);

    /* after delete, the zi is now invalid for any future usage. */
    iter->zi = NULL;

    /* If current node is deleted, we must update iterator node and offset. */
    if (deleted_node) {
        if (iter->direction == AL_START_HEAD) {
            iter->current = next;
            iter->offset = 0;
        } else if (iter->direction == AL_START_TAIL) {
            iter->current = prev;
            iter->offset = -1;
        }
    }
}
REDIS_STATIC int quicklistDelIndex(quicklist *quicklist,  // 删除指定范围内的元素
                                   quicklistNode *node, unsigned char **p) {
    int gone = 0;

    node->zl = ziplistDelete(node->zl, p);
    node->count--;
    if (node->count == 0) {
        gone = 1;
        __quicklistDelNode(quicklist, node);
    } else {
        quicklistNodeUpdateSz(node);
    }
    quicklist->count--;
    /* If we deleted the node, the original node is no longer valid */
    return gone ? 1 : 0;
}

除了上面介绍的操作,quicklist 还有一些其它的操作,例如:

1
2
3
4
quicklistRotate(quicklist *quicklist); // 将尾部的元素移动到头部
quicklistDup(quicklist *orig); // 复制一个 quicklist
quicklistRelease(quicklist *quicklist); // 释放一个 quicklist
quicklistCompare(unsigned char *p1, unsigned char *p2, int p2_len); // 比较两个元素是否相等

quicklist 是 Redis 为了优化 list 的内存和性能而设计的一种数据结构,它结合了 ziplist 和 linkedlist 的优点,既节省了内存空间,又降低了更新复杂度。quicklist 的操作也比较简单,主要是对 ziplist 的封装和管理。quicklist 是 Redis 中的一种重要的数据结构,值得我们深入学习和理解。

  • compress 表示 quicklist 的压缩深度,0 表示所有节点都不压缩,否则就表示从两端开始有多少个节点不压缩。例如,compress 为 1 表示从两端开始,有 1 个节点不做 LZF 压缩。LZF 是种无损压缩算法。Redis 为了节省内存空间,会将 quicklist 的节点用 LZF 压缩后存储,但这里不是全部压缩,可以配置 compress 的值。

2、压缩列表(ziplist) 压缩列表(ziplist)是 Redis 中一种紧凑的、内存优化的列表编码方式,适用于存储较小的列表,或者列表中的元素都是较小的整数或字符串。压缩列表以连续的内存块的形式存储数据,每个节点可以包含一个或多个元素,这使得压缩列表在内存使用效率上有一定优势。

压缩列表是一块连续的内存空间 (像内存连续的数组,但每个元素长度不同),一个 ziplist 可以包含多个节点(entry),元素之间紧挨着存储,没有任何冗余空隙。 压缩列表的本质就是一个数组,只不过是增加了 列表长度zlbytes、尾部偏移量zltail、列表元素个数zllen 以及 列表结束标识zlend,这有利于快速的寻找列表的首、尾节点,压缩列表将表中每一项存放在前后连续的地址空间内,每一项因占用的空间不同,而采用变长编码,由于内存是连续分配的,所以遍历速度很快。

当 Lists 列表数据量比较少的时候,且存储的数据轻量的(如小整数值、短字符串)时候, Redis 就会通过压缩列表来进行底层实现。

链表的结构元素:

  • zlbytes:表示压缩列表的长度(包括所有的字节)
  • zltail:表示压缩列表尾部的指针(偏移量)
  • zllen:表示压缩列表中节点(Entry)的个数
  • entry:存储区,可以包含多个节点,每个节点可以存放整数或者字符串
  • zlend:表示列表结束

如果查找定位首个元素或最后1个元素,可以通过表头 zlbyteszltail_offset 元素快速获取,复杂度是 O(1)。但是查找其它元素时,就没有这么高效了,只能逐个查找下去,比如 entryN 的复杂度就是 O(N)。

压缩列表(ziplist)的特点:

  • 压缩列表可以保存多个元素在一个节点中,因此在元素较小的情况下,它可以节省内存;
  • 压缩列表支持快速的元素访问,因为可以通过索引直接访问元素;
  • 压缩列表适用于列表较小且元素较小的情况;

3、链表(linkedlist) Redis 中另一种列表的内部编码方式是 链表(linkedlist) ,它更适合存储大型列表或者元素大小不一致的列表。

链表(linkedlist) 是标准的双向链表,它的Node 节点包含 prevnext 指针,分别指向后继与前驱节点,因此从双向链表中的任意一个节点开始都可以很方便地访问其前驱与后继节点。这种结构使得链表在插入和删除元素时具有较高的效率。

LinkedList 可以进行双向遍历;添加删除元素快 O(1),查找元素慢 O(n),高效实现了 LPUSH 、RPOP、RPOPLPUSH,但由于需要为每个节点分配额外的内存空间,所以会浪费一定的内存空间。这种编码方式适用于元素数量较多或者元素较大的场景。

LinkedList 结构为链表提供了表头指针 head、表尾指针 tail,以及节点数量计算 len。下图展示一个由 list 结构和三个 listNode 节点组成的链表:

Redis 的linkedlist链表实现的特性可以总结如下:

  • 双端:链表节点带有 prev 和 next 指针,获取某个节点的前一节点和后一节点的复杂度都是 O(1);
  • 无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问以 NULL 为终点;
  • 表头指针/表尾指针:通过 list 结构的 head 指针和 tail 指针,获取链表的表头节点和表尾节点的复杂度为 O(1);
  • 链表长度计数器:通过 list 结构的 len 属性来对 list 的链表节点进行计数,获取节点数量的复杂度为O(1);
  • 多态:链表节点使用 void* 指针来保存节点值,并通过 list 结构的 dup、free、match 三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。
  • 使用链表的附加空间相对太高,因为 64bit 系统中指针是 8 个字节,所以 prev 和 next 指针需要占据 16 个字节,且链表节点在内存中单独分配,会加剧内存的碎片化,影响内存管理效率

总之,Redis 根据列表的大小和元素的大小自动选择使用压缩列表或链表来进行编码,以平衡内存使用和操作效率。在选择数据结构和命令时,需要考虑数据的规模和操作的性能需求。

二、Lists类型相关命令

2.1 Redis List 相关命令简介

以下是关于 Redis List 相关命令的简介表,包括命令、作用以及时间复杂度:

命令 作用 时间复杂度
LPUSH 从列表左侧插入一个或多个元素 O(N) (N 为插入元素数量)
RPUSH 从列表右侧插入一个或多个元素 O(N) (N 为插入元素数量)
LPUSHX 如果列表存在,从左侧插入一个或多个元素,否则不执行操作 O(1)
RPUSHX 如果列表存在,从右侧插入一个或多个元素,否则不执行操作 O(1)
LPOP 从列表左侧删除并返回一个元素 O(1)
RPOP 从列表右侧删除并返回一个元素 O(1)
BLPOP 从左侧删除并返回元素,如果列表为空则阻塞,带有超时参数 O(1) 或阻塞等待
BRPOP 从右侧删除并返回元素,如果列表为空则阻塞,带有超时参数 O(1) 或阻塞等待
LRANGE 获取指定范围内的元素列表 O(Slice Size)
LINDEX 获取指定位置的元素 O(N) (N 为索引位置)
LINSERT 在指定元素前或后插入新元素 O(N) (N 为列表长度)
LLEN 获取列表的长度 O(1)
LREM 根据参数 count 的值,移除列表中与参数 element 相等的元素 O(N)
LSET 将列表 key 下标为 index 的元素的值设置为 element O(N)
LTRIM 对一个列表进行修剪(trim),让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除 O(N)
RPOPLPUSH 在一个原子时间内,执行两个动作,将列表 source 中的最后一个元素(尾元素)弹出,返回给客户端并将 source 弹出的元素插入到列表 destination的表头 O(1)
BRPOPLPUSH 是 RPOPLPUSH 的阻塞版本,当指定列表 source 不为空时,BRPOPLPUSH 的表现和 RPOPLPUSH 一样。 O(1)

注意:时间复杂度中的 “N” 表示操作的复杂度与列表的长度或插入元素的数量成线性关系,而不是固定的常数时间。在实际使用中,需要根据数据规模和性能要求选择适当的命令。

2.2 LPUSH 和 RPUSH、LPUSHX 和 RPUSHX 命令

1、LPUSH 和 RPUSH 命令 LPUSH 命令的作用是将一个或多个元素从左侧(头部插)插入到 list 中; RPUSH 命令的作用则是将一个或多个元素从右侧(尾部插)插入到 list 中; 命令语法:

1
2
127.0.0.1:6379> LPUSH key element [element1 ...]
127.0.0.1:6379> RPUSH key element [element1 ...]
  • 如果 key 不存在,一个空列表会被创建并执行 LPUSH/RPUSH 操作;
  • 当 key 存在,但不是列表类型时,返回类型不匹配的错误信息;

命令参数:

  • element:要插入列表的元素值,如果有多个 element 值,那么各个 element 值按从左到右的顺序依次插入到 表头/表尾。

Tips: 多个 element 值是2.4版本及之后才支持的,在2.4版本之前只接受一个 element 值。

命令返回值:

  • LPUSH/RPUSH 命令返回执行 LPUSH/RPUSH 命令后列表的长度。

2、LPUSHX 和 RPUSHX 命令 LPUSHX 命令的作用是当 key 存在时,将⼀个或者多个元素从左侧放⼊(头部插)到 list 中,不存在则直接返回; RPUSHX 命令的作用是当 key 存在时,将⼀个或者多个元素从右侧放⼊(尾部插)到 list 中,不存在则直接返回; 命令语法:

1
2
127.0.0.1:6379> LPUSHX key element [element1 ...]
127.0.0.1:6379> RPUSHX key element [element1 ...]

LPUSH/RPUSHX 命令返回有 3 种情况:

  • 指定 key 不存在时,返回 0;
  • 指定 key 存在且是列表类型时,返回操作后表的长度;
  • 指定 key 不是列表类型时,返回类型不匹配的错误信息;

2.3 LPOP 和 RPOP、BLPOP 和 BRPOP 命令

1、LPOP 和 RPOP 命令 LPOP 命令的作用是从左侧删除一个元素(头删),并返回删除的值; RPOP 命令的作用是从右侧删除一个元素(尾删),并返回删除的值; 命令语法:

1
2
127.0.0.1:6379> LPOP key
127.0.0.1:6379> RPOP key

LPOP/RPOP 命令返回有3种情况:

  • 当指定的列表 key 存在时,返回列表的 头元素/尾元素;
  • 当指定的列表 key 不存在时,返回 nil;
  • 当指定的 key 不是列表时,返回类型不匹配的错误信息;

2、BLPOP 和 BRPOP BLPOP/BRPOP 命令是 LPOP/RPOP 命令的阻塞版本,BLPOP/BRPOP 命令需要指定一个超时时间,当指定列表 key 内没有任何元素可供获取时,连接将被 BLPOP/BRPOP 命令阻塞,直到等待超时 或 超时前有人向 key 中插入元素使得被阻塞 BLPOP/BRPOP 命令得以唤醒执行。 命令语法:

1
2
127.0.0.1:6379> BLPOP key [key1 ...] timeout  # 命令操作发现是 从左到右
127.0.0.1:6379> BRPOP key [key1 ...] timeout  # 命令操作发现是 从右到左

BLPOP/BRPOP 是列表的阻塞式弹出的原语(primitive)。可以用于消息队列场景,可以指定监测多个消息队列,直到有任意一个消息队列中有待处理消息时,阻塞返回。

命令参数:

  • key:指定的列表 key,当指定多个 key 参数时,按照参数 key 的先后顺序依次检查各个列表,并弹出第一个非空列表 key 的头元素;
  • timeout:超时参数,接受一个以秒为单位的数字作为值,若超时参数设为0,其表示无超时时间限制(即无限期阻塞等待数据的到来),直到 key 内有元素;

BLPOP/BRPOP 命令返回的数据形式为数组形式,主要有2种情况:

  • 当指定的 key 全部为空且已经超时,返回 nil;
  • 当指定的 key 中 从 BLPOP/BRPOP 命令操作方向开始、除为空的 key 以外,先出现 不为空的 key 是列表时(不管后面的key 中是否有 不是列表的非空key),立即返回 该列表 key 头/尾 元素列表(其中返回的第一个元素是被弹出元素所属的 key,第二个元素是被弹出的列表元素);
  • 当指定的 key 中 从 BLPOP/BRPOP 命令操作方向开始、除为空的 key 以外,先出现 不为空的 key 不是列表时(不管后面的key 中是否有 是列表的非空key),立即返回类型不匹配的错误信息;
  • 当指定的 key 为非列表时,立即返回命令与类型不匹配的信息;

非阻塞行为 当 BLPOP/BRPOP 被调用时,如果指定 key 内至少有一个非空列表,那么弹出第一个非空列表的头元素,并和被弹出元素所属的列表的名字一起,组成结果返回给调用者。

当存在多个指定的 key 时,BLPOP/BRPOP 按指定 key 参数排列的先后顺序,依次检查各个列表。

阻塞行为 如果所有指定的 key 都不存在元素,那么 BLPOP/BRPOP 命令将阻塞连接,直到等待超时,或有另一个客户端对指定 key 的任意一个执行 LPUSH 或 RPUSH 命令为止。

Tips: BLPOP/BRPOP 命令实际不会阻塞 Redis 服务,只是把执行该命令的一方进行了阻塞,不会影响其它命令服务。

相同的 key 被多个客户端同时阻塞 相同的 key 可以被多个客户端同时阻塞。

不同的客户端被放进一个队列中,按 “先阻塞先服务” 原则,顺序地为 key 执行 BLPOP 命令。

在 MULTI/EXEC 事务中的 BLPOP/BRPOP BLPOP/BRPOP 命令可以用于 pipline 中,但把它用在 MULTI/EXEC 块当中没有意义。因为这要求整个服务器被阻塞以保证块执行时的原子性,该行为阻止了其它客户端执行 LPUSH 或 RPUSH 命令。

因此,一个被包裹在 MULTI/EXEC 块内的 BLPOP/BRPOP 命令,行为表现得就像 BLPOP/BRPOP 一样,对空列表返回 nil,对非空列表弹出列表元素,不进行任何阻塞操作。

2.4 LRANGE、LINDEX、LINSERT、LLEN、LREM 命令

1、LRANGE LRANGE命令的作用是获取从 start 到 stop 区间的所有元素,区间左闭右闭,并且指定的位置可以是负数,表示倒数第几个。 命令语法:

1
127.0.0.1:6379> LRANGE key start stop

命令参数

  • start:下标的起始位置,以0为底,0表示列表的第一个元素,1表示列表的第二个元素,以此类推,也可以是负数,以-1表示列表的最后一个元素,-2表示列表的倒数第二个元素,以此类推;
  • stop:下标的结束位置,下标数字的意义与 start 相同,stop 位置的元素是包含的,即元素的指定区间是闭区间,这和一些编程语言的范围值不包含结束位置不一样,需要注意;

下标参数超出范围: 超出范围的下标值不会引起错误

  • 若 start 下标比列表的最大下标 end 还要大,那么 LRANGE 返回一个空列表;
  • 若 stop 下标比列表的最大下标 end 还要大,那么将 stop 的值设置为 end;

命令返回值:

  • LRANGE 命令返回一个在指定区间的列表;
  • 若 指定的key 不是列表,返回命令与类型不匹配的信息;

2、LINDEX LINDEX 命令命令返回列表 key 中,下标为 index 的元素(从左边表头开始第 index 位置的元素); 命令语法:

1
127.0.0.1:6379> LINDEX key index

命令参数:

  • index:下标的位置,以0为底,0表示列表的第一个元素,1表示列表的第二个元素,以此类推,也可以是负数,以-1表示列表的最后一个元素,-2表示列表的倒数第二个元素,以此类推;

LINDEX 命令返回值有3种情况:

  • 当 index 位置存在元素时,返回列表中下标为 index 的元素;
  • 当 index 参数的值不在列表的区间范围内 或 列表为空,返回 nil;
  • 当指定的 key 不是列表时,返回类型不匹配的错误信息;

3、LINSERT LINSERT 命令的作用是将元素插入到列表 key 当中,位于值 pivot元素 之前或之后。当指定的 pivot元素 不存在于列表中时,不执行任何操作。当列表不存在时,被视为空列表,不执行任何操作。如果 key 不是列表类型,返回一个错误。 命令语法:

1
127.0.0.1:6379> LINSERT key [BEFORE|AFTER] pivot element

说明:

  • BEFORE 表示插入到 pivot 元素之前;
  • AFTER 表示插入到 pivot 元素之后。

4、LLEN LLEN 命令返回指定列表的长度,当指定的列表 key 不存在时,该命令返回 0,被解释为空列表,当指定的 key 的值不是一个列表类型,则会返回类型不匹配的错误信息。 命令语法:

1
127.0.0.1:6379> LLEN key

LLEN 命令返回主要有 3 种情况:

  • 当指定的列表 key 不存在时,被解释为空列表,返回 0;
  • 指定 key 存在且为列表类型,返回列表长度;
  • 指定 key 不是列表类型,返回类型不匹配的错误信息;

5、LREM LREM 命令表示根据参数 count 的值,移除列表中与参数 element 相等的元素。 命令语法:

1
127.0.0.1:6379> LREM key count element

命令参数: LREM 命令主要的参数是 count,count 的值可以是以下几种:

  • count > 0:从表头开始向表尾搜索,移除与 element 相等的元素,数量为 count。
  • count < 0:从表尾开始向表头搜索,移除与 element 相等的元素,数量为 count 的绝对值。
  • count = 0:移除表中所有与 element 相等的值。

命令返回值:

  • LREM 命令返回被移除元素的数量,因为不存在的 key 被视作空表,所以当 key 不存在时,LREM 命令总是返回0。

2.5 RPOPLPUSH 和 BRPOPLPUSH 命令

RPOPLPUSH 命令在一个原子时间内,执行两个动作:

  • 一个是将列表 source 中的最后一个元素(尾元素)弹出,并返回给客户端;
  • 另一个是将 source 弹出的元素插入到列表 destination,作为 destination 列表的的头元素;

命令语法:

1
127.0.0.1:6379> RPOPLPUSH source destination

命令参数:

  • source:弹出尾元素的指定列表 key。
  • destination:插入头元素的指定列表 key。

RPOPLPUSH 命令返回有3种情况:

  • 当指定的 source 列表为空(key 不存在),不管 destination 如何(为空、为非空列表 key 或是 为 非空且非列表key)都返回 nil;
  • 当指定的 source 列表中存在可返回的元素(key 非空且为列表)时,如果 destination 为空 或 为非空列表 key,则返回改(弹出)source 的尾元素, 并同时将弹出的 source 的尾元素 压入 destination 列表的头部(列表不存在就创建);如果 destination 为 非空且非列表key, 则返回类型不匹配的错误信息;
  • 当指定的 source 为 非空且非列表key时,返回类型不匹配的错误信息;

BRPOPLPUSH 是 RPOPLPUSH 的阻塞版本,当指定列表 source 不为空时,BRPOPLPUSH 的表现和 RPOPLPUSH 一样。当列表 source 为空时,BRPOPLPUSH 命令将阻塞连接,直到等待超时,或有另一个客户端对 source 执行 LPUSH 或 RPUSH 命令为止。

命令语法:

1
127.0.0.1:6379> BRPOPLPUSH source destination timeout

命令参数:

  • source:弹出尾元素的指定列表 key。
  • destination:插入头元素的指定列表 key。
  • timeout:阻塞超时时间,单位为秒。

RPOPLPUSH 命令返回有3种情况:

  • 当指定的 source 列表为空(key 不存在),且已经超时,不管 destination 如何(为空、为非空列表 key 或是 为 非空且非列表key)都返回 nil;
  • 当指定的 source 列表中存在可返回的元素(key 非空且为列表,包括key 为空但在超时返回前其它链接向 source key push 元素的情况)时,如果 destination 为空 或 为非空列表 key,则返回改(弹出)source 的尾元素, 并同时将弹出的 source 的尾元素 压入 destination 列表的头部(列表不存在就创建);如果 destination 为 非空且非列表key,则返回类型不匹配的错误信息;
  • 当指定的 source 为 非空且非列表key时,返回类型不匹配的错误信息;

2.6 LEST 和 LTRIM 命令

LSET 命令将列表 key 下标为 index 的元素的值设置为 element。

命令语法:

1
127.0.0.1:6379> LSET key index element

命令参数:

  • index:列表的下标,当 index 参数超出范围,或对一个空列表(key 不存在)进行 LSET 操作时,返回一个错误。

命令返回值:

  • 操作成功返回 OK,否则返回错误信息。

LTRIM 命令对一个列表进行修剪(trim),让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。

命令语法:

1
127.0.0.1:6379> LTRIM key start stop

当 key 不是列表类型时,返回一个错误。

LTRIM 命令通常和 LPUSH 命令或 RPUSH 命令配合使用。

命令参数:

  • start:下标的起始位置,以0为底,0表示列表的第一个元素,1表示列表的第二个元素,以此类推,也可以是负数,以-1表示列表的最后一个元素,-2表示列表的倒数第二个元素,以此类推。
  • stop:下标的结束位置,下标数字的意义与 start 相同。

下标超出范围:超出范围的下标值不会引起错误

  • 如果 start 下标比列表的最大下标 end(LLEN list 减去 1)还要大,或者 start>stop,LTRIM 返回一个空列表(因为 LTRIM 已经将整个列表清空)。
  • 如果 stop 下标比 end 下标还要大,Redis 将 stop 的值设置为 end。

命令返回值:

  • 命令执行成功时,返回 OK。

三、Lists 应用场景

3.1 消息队列

Redis 可以使用 lpush + brpop 命令组合实现经典的 阻塞式生产者-消费者模型 队列,生产者客户端使用 lpush 从列表左侧插入元素,多个消费者客户端使用 brpop 命令阻塞式地从队列中 “争抢” 队首元素。通过多个客户端来保证消费的负载均衡和高可用性。

Redis 阻塞消息队列模型

分频道的消息队列

3.2 微博列表

在博客类系统中,每个用户都有属于自己的微博列表,一般现需要分页展示文章列表。此时可以考虑使用列表类型,因为列表不但是有序的,同时支持按照索引范围获取元素。相关操作步骤如下:

  1. 每篇微博使用哈希结构存储,例如微博中 3 个属性:title、timestamp、content:
1
2
3
hmset mblog:1 title xx timestamp 1476536196 content xxxxx
...
hmset mblog:n title xx timestamp 1476536196 content xxxxx
  1. 向用户的微博列表中添加微博,使用 user::mblogs 作为微博的键:
1
2
3
lpush user:1:mblogs mblog:1 mblog:3
...
lpush user:k:mblogs mblog:9
  1. 分页获取用户的微博列表,例如获取用户1 的前 10 篇微博:
1
2
3
4
keylist = lrange user:1:mblogs 0 9
for key in keylist {
|hgetall key
}

此外,此方案在实际中可能存在两个问题:

  • 1 + n 问题,即如果每次分页获取的微博个数较多,需要执行多次 hgetall 操作,此时可以考虑使用 pipeline(流水线)模式批量提交命令,或者微博不采用哈希类型,而是使用序列化的字符串类型,使用 mget 获取。
  • 分页获取文章时,lrange 在列表两端表现较好,获取列表中间的元素表现较差,此时可以考虑将列表做拆分。