Redis 数据结构之 List 详细解析

Redis 数据结构详解:List 列表篇

在 Redis 的五大基础数据结构中,List(列表) 是一种非常灵活的有序字符串集合,它既可以充当栈和队列,也能实现消息队列、时间线等业务场景。本文将带你从底层特性、核心命令、内部编码到实战场景,全面吃透 Redis List。


一、List 列表是什么?

List 是 Redis 中存储多个有序字符串的序列,每个元素称为 element,一个列表最多可以存储 2 32 − 1 2^{32}-1 232−1 个元素。它的核心特性可以概括为三点:

  1. 有序性 :列表中的元素是按插入顺序排列的,我们可以通过下标(正索引 / 负索引)获取指定元素,例如获取第 5 个元素用 index 4,或直接用负索引 lindex \-1 获取最后一个元素。

  2. 元素可重复:列表中允许存在相同的值,比如["a", "b", "a"] 这样的结构是完全合法的。

  3. 操作灵活性:支持从两端(左 / 右)插入、弹出元素,也支持按范围截取、按索引修改,能同时适配栈、队列等多种数据结构的使用需求。

List 的基础操作示意图如下:


二、List 核心命令详解

List 的命令可以分为基础操作阻塞操作两大类,我们按功能逐一拆解说明。

1. 基础操作命令

(1)添加元素:LPUSH / LPUSHX / RPUSH / RPUSHX

这组命令用于向列表中插入元素,区别在于插入方向和是否仅在列表存在时操作。

命令 作用 语法 时间复杂度
LPUSH 从列表左侧(表头)插入一个或多个元素 LPUSH key element [element \.\.\.\] 单个元素 O ( 1 ) O(1) O(1),多个元素 O ( N ) O(N) O(N)
LPUSHX 仅当列表存在时,从左侧插入元素 LPUSHX key element [element \.\.\.\] 单个元素 O ( 1 ) O(1) O(1),多个元素 O ( N ) O(N) O(N)
RPUSH 从列表右侧(表尾)插入一个或多个元素 RPUSH key element [element \.\.\.\] 单个元素 O ( 1 ) O(1) O(1),多个元素 O ( N ) O(N) O(N)
RPUSHX 仅当列表存在时,从右侧插入元素 RPUSHX key element [element \.\.\.\] 单个元素 O ( 1 ) O(1) O(1),多个元素 O ( N ) O(N) O(N)

示例:

redis 复制代码
# 从左侧插入元素
127.0.0.1:6379> LPUSH mylist "world"
(integer) 1
127.0.0.1:6379> LPUSH mylist "hello"
(integer) 2
# 查看列表所有元素
127.0.0.1:6379> LRANGE mylist 0 -1
1) "hello"
2) "world"
(2)获取元素:LRANGE / LINDEX / LLEN

这组命令用于读取列表中的元素信息,不修改列表本身。

  • LRANGE key start stop :获取列表中从 startstop 索引区间的所有元素(左闭右闭),支持负索引(\-1 表示最后一个元素)。

  • LINDEX key index :获取列表中指定索引位置的元素,索引超出范围返回 nil

  • LLEN key :获取列表的长度,时间复杂度为 O ( 1 ) O(1) O(1)。

示例:

redis 复制代码
# 继续上面的示例
127.0.0.1:6379> LRANGE mylist 0 -1
1) "hello"
2) "world"
127.0.0.1:6379> LINDEX mylist 0
"hello"
127.0.0.1:6379> LINDEX mylist -1
"world"
127.0.0.1:6379> LLEN mylist
(integer) 2
(3)插入 / 修改 / 删除元素:LINSERT / LREM / LTRIM / LSET

这组命令用于修改列表的结构或元素值。

  • LINSERT key BEFORE|AFTER pivot element :在列表中指定元素 pivot 的前 / 后插入新元素。

  • LREM key count value :删除列表中前 count 个值为 value 的元素(count\>0 从左删,count\<0 从右删,count=0 删除所有匹配元素)。

  • LTRIM key start stop :截取列表中 startstop 区间的元素,其余元素全部删除,常用于实现固定长度列表。

  • LSET key index value:修改列表中指定索引位置的元素值,索引超出范围会报错。

示例:

redis 复制代码
# 在 "world" 前插入 "there"
127.0.0.1:6379> LINSERT mylist BEFORE "world" "there"
(integer) 3
127.0.0.1:6379> LRANGE mylist 0 -1
1) "hello"
2) "there"
3) "world"
# 修改索引1的元素为 "hi"
127.0.0.1:6379> LSET mylist 1 "hi"
OK
127.0.0.1:6379> LRANGE mylist 0 -1
1) "hello"
2) "hi"
3) "world"

2. 阻塞操作命令:BLPOP / BRPOP

BLPOPBRPOPLPOPRPOP 的阻塞版本,它们的核心区别在于:

  • 非阻塞版(LPOP/RPOP):如果列表为空,直接返回 nil

  • 阻塞版(BLPOP/BRPOP):如果列表为空,会阻塞等待指定时间,直到有新元素加入或超时才返回结果。

命令语法
redis 复制代码
BLPOP key [key ...] timeout
BRPOP key [key ...] timeout
  • key:要监听的列表键,可以同时监听多个;

  • timeout:阻塞等待的超时时间(单位:秒),0 表示永久阻塞。

核心特性
  1. 多键监听:可以同时监听多个列表,按顺序检查列表,哪个列表有元素就从哪个列表弹出。

  2. 客户端公平性:多个客户端同时阻塞等待同一个列表时,遵循 "先到先得" 原则,先执行命令的客户端会优先获取元素。

  3. 超时机制 :超时时间到了仍无元素加入,返回 nil

阻塞 vs 非阻塞对比
场景 非阻塞版(LPOP) 阻塞版(BLPOP)
列表不为空 直接弹出元素,返回结果 直接弹出元素,返回结果
列表为空 立即返回 nil 阻塞等待,直到有元素加入或超时

示意图如下:


三、List 内部编码实现

Redis 为了兼顾性能和内存占用,为 List 设计了两种内部编码,会根据列表的元素数量和元素长度自动切换:

1. ziplist(压缩列表)

当列表同时满足以下两个条件时,使用 ziplist 作为内部编码:

  • 列表中元素数量小于 list\-max\-ziplist\-entries(默认 512);

  • 每个元素的长度都小于 list\-max\-ziplist\-value(默认 64 字节)。

ziplist 是一种紧凑的连续内存结构,通过减少额外指针和元数据来节省内存,适合存储少量、短字符串元素。

2. linkedlist(双向链表)

当列表不满足 ziplist 的条件时,Redis 会自动切换为 linkedlist 编码:

  • 元素数量超过 512 个;

  • 任意一个元素长度超过 64 字节。

linkedlist 是标准的双向链表结构,每个节点包含前驱 / 后继指针和元素值,虽然内存占用比 ziplist 高,但插入、删除元素的时间复杂度为 O ( 1 ) O(1) O(1),适合处理大量元素或长字符串元素。

示例:

redis 复制代码
# 查看小列表的编码
127.0.0.1:6379> LPUSH smalllist "a" "b" "c"
(integer) 3
127.0.0.1:6379> OBJECT ENCODING smalllist
"ziplist"

# 查看长字符串列表的编码
127.0.0.1:6379> LPUSH biglist "a very long string that exceeds 64 bytes..."
(integer) 1
127.0.0.1:6379> OBJECT ENCODING biglist
"linkedlist"

四、List 典型使用场景

List 的特性决定了它非常适合处理 "有序、需频繁两端操作" 的业务场景,以下是最常见的两个场景:

1. 阻塞式消息队列(生产者 - 消费者模型)

利用 LPUSH(生产者写入)和 BRPOP(消费者读取)可以实现经典的阻塞式消息队列:

  • 生产者 :使用 LPUSH 将消息写入列表;

  • 消费者 :使用 BRPOP 阻塞等待消息,有消息时立即处理,无消息时阻塞等待,避免轮询空列表消耗资源。

示意图如下:

如果需要实现多消费者的负载均衡,可以同时监听多个列表键,实现 "发布 - 订阅" 模式的扩展。

2. 社交平台时间线(Timeline)

微博、朋友圈这类社交平台的时间线功能,本质上就是基于 List 实现的:

  1. 存储单条动态:每条微博 / 朋友圈作为一条数据,存入 Hash 结构中;

  2. 构建时间线列表 :用户发布动态时,用 LPUSH 将动态 ID 写入用户的时间线列表(user:1:blogs),保证最新的动态在列表最前面;

  3. 分页查询 :使用 LRANGE user:1:blogs 0 9 分页获取动态 ID,再批量查询 Hash 中的动态详情。

这种实现方式既保证了时间线的有序性,又支持高效的分页查询,是 List 最经典的实战场景之一。


五、List 命令时间复杂度总结

为了方便大家快速回顾,这里整理了 List 所有核心命令的时间复杂度:

操作类型 命令 时间复杂度
添加 LPUSH/RPUSH/LPUSHX/RPUSHX 单个元素 O ( 1 ) O(1) O(1),多个元素 O ( N ) O(N) O(N)
查找 LRANGE O ( N ) O(N) O(N)(N 为返回元素个数)
LINDEX/LLEN O ( 1 ) O(1) O(1)
修改 LINSERT/LSET O ( N ) O(N) O(N)(N 为列表长度)
删除 LREM/LTRIM O ( N ) O(N) O(N)(N 为列表长度)
阻塞操作 BLPOP/BRPOP O ( 1 ) O(1) O(1)

List 作为 Redis 中唯一的有序序列结构,凭借灵活的操作和高效的性能,在消息队列、时间线等场景中有着不可替代的作用。理解它的底层编码和命令特性,才能在业务中选择最合适的实现方式。

相关推荐
影sir1 小时前
STL容器——list类
c++·链表·stl·list
如君愿1 小时前
考研复习 Day 35 | 习题--计算机网络 第七章 网络安全(上)、数据结构 排序算法(上)
数据结构·计算机网络·考研·课后习题
手握风云-1 小时前
Redis:不只是缓存那么简单(九)
redis·缓存
gQ85v10Db1 小时前
Redis分布式锁进阶第三十二篇
数据库·redis·分布式
xu_ws2 小时前
redis的io多路复用和Java NIO的区别
java·redis·nio
wmm_会飞的@鱼2 小时前
FlexSim-基于SLP方法的A汽车企业总装车间布局优化
前端·数据结构·数据库·python·数学建模·汽车
Severus_black2 小时前
【初阶数据结构】链式二叉树(BinaryTreeNode)与递归
c语言·数据结构·链表
鱼子星_2 小时前
最短路问题【图论】
数据结构·算法·贪心算法·动态规划·图论
Devin~Y2 小时前
大厂Java面试实录:Spring Boot微服务 + Redis/Kafka + Prometheus/Jaeger + RAG/Agent(小Y水货版)
java·spring boot·redis·spring cloud·kafka·prometheus·jaeger