美文网首页分布式缓存(redis)
深入剖析Redis系列(七) - Redis数据结构之列表

深入剖析Redis系列(七) - Redis数据结构之列表

作者: 零壹技术栈 | 来源:发表于2019-01-31 22:32 被阅读15次

    前言

    列表list)类型是用来存储多个 有序字符串。在 Redis 中,可以对列表的 两端 进行 插入push)和 弹出pop)操作,还可以获取 指定范围元素列表、获取 指定索引下标元素 等。

    image

    列表 是一种比较 灵活数据结构,它可以充当 队列 的角色,在实际开发上有很多应用场景。

    如图所示,abcde 五个元素 从左到右 组成了一个 有序的列表,列表中的每个字符串称为 元素element),一个列表最多可以存储 2 ^ 32 - 1 个元素。

    • 列表的 插入弹出 操作
    image
    • 列表的 获取截取删除 操作
    image

    正文

    1. 相关命令

    下面将按照对 列表5操作类型 对命令进行介绍:

    image

    1.1. 添加命令

    1.1.1. 从右边插入元素

    rpush key value [value ...]

    下面代码 从右向左 插入元素 cba

    127.0.0.1:6379> rpush listkey c b a
    (integer) 3
    

    lrange 0 -1 命令可以 从左到右 获取列表的 所有元素

    127.0.0.1:6379> lrange listkey 0 -1
    1) "c"
    2) "b"
    3) "a"
    

    1.1.2. 从左边插入元素

    lpush key value [value ...]

    使用方法和 rpush 相同,只不过从 左侧插入,这里不再赘述。

    1.1.3. 向某个元素前或者后插入元素

    linsert key before|after pivot value

    linsert 命令会从 列表 中找到 第一个 等于 pivot 的元素,在其 before)或者 after)插入一个新的元素 value,例如下面操作会在列表的 元素 b 前插入 redis

    127.0.0.1:6379> linsert listkey before b redis
    (integer) 4
    

    返回结果为 4,代表当前 列表长度,当前列表变为:

    127.0.0.1:6379> lrange listkey 0 -1
    1) "c"
    2) "redis"
    3) "b"
    4) "a"
    

    1.2. 查询命令

    1.2.1. 获取指定范围内的元素列表

    lrange key start stop

    lrange 操作会获取列表 指定索引 范围所有的元素。

    索引下标 有两个特点:

    • 其一,索引下标 从左到右 分别是 0N-1,但是 从右到左 分别是 -1-N

    • 其二,lrange 中的 end 选项包含了 自身,这个和很多编程语言不包含 end 不太相同。

    从左到右 获取列表的第 2 到第 4 个元素,可以执行如下操作:

    127.0.0.1:6379> lrange listkey 1 3
    1) "redis"
    2) "b"
    3) "a"
    

    从右到左 获取列表的第 1 到第 3 个元素,可以执行如下操作:

    127.0.0.1:6379> lrange listkey -3 -1
    1) "redis"
    2) "b"
    3) "a"
    

    1.2.2. 获取列表指定索引下标的元素

    lindex key index

    例如当前列表 最后一个 元素为 a

    127.0.0.1:6379> lindex listkey -1
    "a"
    

    1.2.3. 获取列表长度

    llen key

    例如,下面示例 当前列表长度4

    127.0.0.1:6379> llen listkey
    (integer) 4
    

    1.3. 删除命令

    1.3.1. 从列表左侧弹出元素

    lpop key

    如下操作将 列表 最左侧的元素 c 弹出,弹出后 列表 变为 redisba

    127.0.0.1:6379> lpop listkey
    "c"
    127.0.0.1:6379> lrange listkey 0 -1
    1) "redis"
    2) "b"
    3) "a"
    

    1.3.2. 从列表右侧弹出元素

    rpop key

    它的使用方法和 lpop 是一样的,只不过从列表 右侧 弹出元元素。

    127.0.0.1:6379> lpop listkey
    "a"
    127.0.0.1:6379> lrange listkey 0 -1
    1) "c"
    2) "redis"
    3) "b"
    

    1.3.3. 删除指定元素

    lrem key count value

    lrem 命令会从 列表 中找到 等于 value 的元素进行 删除,根据 count 的不同分为三种情况:

    • count > 0从左到右,删除最多 count 个元素。

    • count < 0从右到左,删除最多 count 绝对值 个元素。

    • count = 0删除所有

    例如向列表 从左向右 插入 5a,那么当前 列表 变为 “a a a a a redis b a”,下面操作将从列表 左边 开始删除 4 个为 a 的元素:

    127.0.0.1:6379> lrem listkey 4 a
    (integer) 4
    127.0.0.1:6379> lrange listkey 0 -1
    1) "a"
    2) "redis"
    3) "b"
    4) "a"
    

    1.3.4. 按照索引范围修剪列表

    127.0.0.1:6379> ltrim listkey 1 3
    OK
    127.0.0.1:6379> lrange listkey 0 -1
    1) "redis"
    2) "b"
    3) "a"
    

    1.4. 修改命令

    1.4.1. 修改指定索引下标的元素

    修改 指定索引下标 的元素:

    lset key index newValue

    下面操作会将列表 listkey 中的第 3 个元素设置为 mysql

    127.0.0.1:6379> lset listkey 2 mysql
    OK
    127.0.0.1:6379> lrange listkey 0 -1
    1) "redis"
    2) "b"
    3) "mysql"
    

    1.5. 阻塞操作命令

    阻塞式弹出 操作的命令如下:

    blpop key [key ...] timeout
    brpop key [key ...] timeout

    blpopbrpoplpoprpop阻塞版本,它们除了 弹出方向 不同,使用方法 基本相同,所以下面以 brpop 命令进行说明, brpop 命令包含两个参数:

    • key[key...]:一个列表的 多个键

    • timeout阻塞 时间(单位:)。

    对于 timeout 参数,要氛围 列表为空不为空 两种情况:

    • 列表为空

    如果 timeout = 3,那么 客户端 要等到 3 秒后返回,如果 timeout = 0,那么 客户端 一直 阻塞 等下去:

    127.0.0.1:6379> brpop list:test 3
    (nil)
    (3.10s)
    127.0.0.1:6379> brpop list:test 0
    ...阻塞...
    

    如果此期间添加了数据 element1,客户端 立即返回

    127.0.0.1:6379> brpop list:test 3
    1) "list:test"
    2) "element1"
    (2.06s)
    
    • 列表不为空:客户端会 立即返回
    127.0.0.1:6379> brpop list:test 0
    1) "list:test"
    2) "element1"
    

    在使用 brpop 时,有以下两点需要注意:

    • 其一,如果是 多个键,那么 brpop从左至右 遍历键,一旦有 一个键弹出元素,客户端 立即返回
    127.0.0.1:6379> brpop list:1 list:2 list:3 0
    ..阻塞..
    

    此时另一个 客户端 分别向 list:2list:3 插入元素:

    client-lpush> lpush list:2 element2
    (integer) 1
    client-lpush> lpush list:3 element3
    (integer) 1
    

    客户端 会立即返回 list:2 中的 element2,因为 list:2 最先有 可以弹出 的元素。

    127.0.0.1:6379> brpop list:1 list:2 list:3 0
    1) "list:2"
    2) "element2"
    
    • 其二,如果 多个客户端同一个键 执行 brpop,那么 最先执行 brpop 命令的 客户端 可以 获取 到弹出的值。

    按先后顺序在 3 个客户端执行 brpop 命令:

    • 客户端1:
    client-1> brpop list:test 0
    ...阻塞...
    
    • 客户端2:
    client-2> brpop list:test 0
    ...阻塞...
    
    • 客户端3:
    client-3> brpop list:test 0
    ...阻塞...
    

    此时另一个 客户端 lpush 一个元素到 list:test 列表中:

    client-lpush> lpush list:test element
    (integer) 1
    

    那么 客户端 1 会获取到元素,因为 客户端 1 最先执行 brpop 命令,而 客户端 2客户端 3 会继续 阻塞

    client> brpop list:test 0
    1) "list:test"
    2) "element"
    

    有关 列表基础命令 已经介绍完了,下表是相关命令的 时间复杂度

    image

    2. 内部编码

    列表类型的 内部编码 有两种:

    2.1. ziplist(压缩列表)

    当列表的元素个数 小于 list-max-ziplist-entries 配置(默认 512 个),同时列表中 每个元素 的值都 小于 list-max-ziplist-value 配置时(默认 64 字节),Redis 会选用 ziplist 来作为 列表内部实现 来减少内存的使用。

    2.2. linkedlist(链表)

    列表类型 无法满足 ziplist 的条件时, Redis 会使用 linkedlist 作为 列表内部实现

    2.3. 编码转换

    下面的示例演示了 列表类型内部编码,以及相应的变化。

    • 当元素 个数较少没有大元素 时,内部编码ziplist
    127.0.0.1:6379> rpush listkey e1 e2 e3
    (integer) 3
    127.0.0.1:6379> object encoding listkey
    "ziplist"
    
    • 当元素个数超过 512 个,内部编码 变为 linkedlist
    127.0.0.1:6379> rpush listkey e4 e5 ... e512 e513
    (integer) 513
    127.0.0.1:6379> object encoding listkey
    "linkedlist"
    
    • 当某个元素超过 64 字节内部编码 也会变为 linkedlist
    127.0.0.1:6379> rpush listkey "one string is bigger than 64 byte..."
    (integer) 4
    127.0.0.1:6379> object encoding listkey
    "linkedlist"
    

    Redis3.2 版本提供了 quicklist 内部编码,简单地说它是以一个 ziplist节点linkedlist,它结合了 ziplistlinkedlist 两者的优势,为 列表类型 提供了一种更为优秀的 内部编码 实现,它的设计原理可以参考 Redis 的另一个作者 Matt Stancliff 的博客 redis-quicklist

    3. 应用场景

    3.1. 消息队列

    通过 Redislpush + brpop 命令组合,即可实现 阻塞队列。如图所示:

    image

    生产者客户端 使用 lrpush 从列表 左侧插入元素多个消费者客户端 使用 brpop 命令 阻塞式“抢” 列表 尾部 的元素,多个客户端 保证了消费的 负载均衡高可用性

    3.2. 文章列表

    每个 用户 有属于自己的 文章列表,现需要 分页 展示文章列表。此时可以考虑使用 列表,因为列表不但是 有序的,同时支持 按照索引范围 获取元素。

    • 每篇文章使用 哈希结构 存储,例如每篇文章有 3 个属性 titletimestampcontent
    hmset acticle:1 title xx timestamp 1476536196 content xxxx
    hmset acticle:2 title yy timestamp 1476536196 content yyyy
    ...
    hmset acticle:k title kk timestamp 1476512536 content kkkk
    
    • 向用户文章列表 添加文章user:{id}:articles 作为用户文章列表的
    lpush user:1:acticles article:1 article:3 article:5
    lpush user:2:acticles article:2 article:4 article:6
    ...
    lpush user:k:acticles article:7 article:8
    
    • 分页 获取 用户文章列表,例如下面 伪代码 获取用户 id=1 的前 10 篇文章:
    articles = lrange user:1:articles 0 9
    for article in {articles}
        hgetall {article}
    

    使用 列表 类型 保存获取 文章列表会存在两个问题:

    • 第一:如果每次 分页 获取的 文章个数较多,需要执行多次 hgetall 操作,此时可以考虑使用 Pipeline 进行 批量获取,或者考虑将文章数据 序列化为字符串 类型,使用 mget 批量获取

    • 第二分页 获取 文章列表 时, lrange 命令在列表 两端性能较好,但是如果 列表较大,获取列表 中间范围 的元素 性能会变差。此时可以考虑将列表做 二级拆分,或者使用 Redis 3.2quicklist 内部编码实现,它结合 ziplistlinkedlist 的特点,获取列表 中间范围 的元素时也可以 高效完成

    3.3. 其他场景

    实际上列表的使用场景很多,具体可以参考如下:

    命令组合 对应数据结构
    lpush + lpop Stack(栈)
    lpush + rpop Queue(队列)
    lpush + ltrim Capped Collection(有限集合)
    lpush + brpop Message Queue(消息队列)

    小结

    本文介绍了 Redis 中的 列表 的 一些 基本命令内部编码适用场景。通过组合不同 命令,可以把 列表 转换为不同的 数据结构 使用。

    参考

    《Redis 开发与运维》


    欢迎关注技术公众号: 零壹技术栈

    零壹技术栈

    本帐号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。

    相关文章

      网友评论

        本文标题:深入剖析Redis系列(七) - Redis数据结构之列表

        本文链接:https://www.haomeiwen.com/subject/dfzlsqtx.html