• 【Redis】五大常见的数据类型之 List


    前言

    我们都知道 Redis 提供了丰富的数据类型,常见的有五种:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)

    今天我们就来详细的聊聊 Redis 这五大常见的数据类型之一 List

    结构类型结构存储的值结构读写能力
    List一个链表,链表上的每个节点都包含一个字符串;对链表的两端进行 pushpop 操作;
    根据值查找或删除元素;

    应用场景:消息队列等。

     

    概述简介

    List 列表是简单的字符串列表,按照插入顺序排序,可以从头部或尾部向 List 列表添加元素。

    列表的最大长度为 2^32 - 1,也即每个列表支持超过 40 亿个元素。
     

    内部实现

    List 类型的底层数据结构是由双端链表或压缩列表实现的:

    • 如果列表的元素个数小于 512 个(默认值,可由 list-max-ziplist-entries 配置),列表每个元素的值都小于 64 字节(默认值,可由 list-max-ziplist-value 配置),Redis 会使用压缩列表作为 List 类型的底层数据结构;
    • 如果列表的元素不满足上面的条件,Redis 会使用双端链表作为 List 类型的底层数据结构;

    但是在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双端链表和压缩列表
     

    常用命令

    在 redis 里面,我们可以把 List 玩成栈、队列等;

    # 将一个或多个值 value 插入到 key 列表的表头(最左边),最后的值在最前面
    # LPUSH key element [element ...]
    127.0.0.1:6379> LPUSH list a b c
    (integer) 3
    
    # 将一个或多个值 value 插入到 key 列表的表尾(最右边)
    # RPUSH key element [element ...]
    127.0.0.1:6379> RPUSH list d f e
    (integer) 6
    
    # 移除并返回 key 列表的头元素
    # LPOP key [count]
    127.0.0.1:6379> LPOP list
    "c"
    127.0.0.1:6379> LPOP list 2
    1) "b"
    2) "a"
    
    # 移除并返回 key 列表的尾元素
    # RPOP key [count]
    127.0.0.1:6379> RPOP list
    "e"
    127.0.0.1:6379> RPOP list 2
    1) "f"
    2) "d"
    
    # 返回列表 key 中指定区间内的元素,区间以偏移量 start 和 stop 指定,从0开始
    # LRANGE key start stop
    127.0.0.1:6379> LRANGE list 0 3
    1) "a"
    2) "b"
    3) "c"
    4) "d"
    
    # 从 key 列表表头弹出一个元素,没有就阻塞 timeout 秒,如果 timeout=0 则一直阻塞
    # BLPOP key [key ...] timeout
    127.0.0.1:6379> BLPOP list 0
    1) "list"
    2) "b"
    127.0.0.1:6379> BLPOP noList 0
    # 阻塞中...
    
    # 从 key 列表表尾弹出一个元素,没有就阻塞 timeout 秒,如果 timeout=0 则一直阻塞
    # BRPOP key [key ...] timeout
    127.0.0.1:6379> BRPOP list 0
    1) "list"
    2) "f"
    127.0.0.1:6379> BRPOP noList 1
    (nil)
    (1.03s)
    
    # 通过下标获得 key 列表中的某一个值
    # LINDEX key index
    127.0.0.1:6379> LINDEX list 2
    "e"
    127.0.0.1:6379> LINDEX list 6
    (nil)
    
    # 返回 key 列表的长度
    # LLEN key
    127.0.0.1:6379> LLEN list
    (integer) 3
    
    # 移除 key 列表中的某元素,指定个数,精确匹配
    # LREM key count element
    127.0.0.1:6379> LREM list 1 d
    (integer) 1
    
    # 截取 key 列表中指定范围的元素
    # LTRIM key start stop
    127.0.0.1:6379> LRANGE list 0 -1
    1) "s"
    2) "i"
    3) "d"
    4) "1"
    5) "0"
    6) "t"
    127.0.0.1:6379> LTRIM list 3 5
    OK
    127.0.0.1:6379> LRANGE list 0 -1
    1) "1"
    2) "0"
    3) "t"
    
    # 移除 source 列表的最后一个元素,将该元素移动到 destination 列表去
    # RPOPLPUSH source destination
    127.0.0.1:6379> LRANGE list 0 -1
    1) "1"
    2) "0"
    3) "t"
    127.0.0.1:6379> RPOPLPUSH list l1
    "t"
    127.0.0.1:6379> LRANGE l1 0 -1
    1) "t"
    
    # 将 key 列表中的指定下标的值更新成新值
    # LSET key index element
    127.0.0.1:6379> EXISTS list
    (integer) 0
    127.0.0.1:6379> lset list 0 item	
    (error) ERR no such key
    127.0.0.1:6379> lpush list value1
    (integer) 1
    127.0.0.1:6379> LRANGE list 0 -1
    1) "value1"
    127.0.0.1:6379> lset list 0 item
    OK
    127.0.0.1:6379> LRANGE list 0 -1
    1) "item"
    127.0.0.1:6379> lset list 1 other
    (error) ERR index out of range
    
    # 将 value 插入到 key 列表中的某个元素前面或后面
    # LINSERT key BEFORE|AFTER pivot element
    127.0.0.1:6379> RPUSH mylist hello sid10t
    (integer) 2
    127.0.0.1:6379> LINSERT mylist before sid10t world
    (integer) 3
    127.0.0.1:6379> LRANGE mylist 0 -1
    1) "hello"
    2) "world"
    3) "sid10t"
    
    • 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
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122

     

    应用场景

    消息队列

    消息队列在存取消息时,必须要满足三个需求,分别是消息保序、处理重复的消息和保证消息可靠性

    Redis 的 ListStream 两种数据类型,就可以满足消息队列的这三个需求。我们先来了解下基于 List 的消息队列实现方法,在之后的博文中再细说 Stream

    1、如何满足消息保序需求?

    List 本身就是按先进先出的顺序对数据进行存取的,所以,如果使用 List 作为消息队列保存消息的话,就已经能满足消息保序的需求了。

    List 可以使用 LPUSH + RPOP 或者 RPUSH+LPOP 命令实现消息队列。

    • 生产者使用 LPUSH key value[value...] 将消息插入到队列的头部,如果 key 不存在则会创建一个空的队列再插入消息。
    • 消费者使用 RPOP key 依次读取队列的消息,先进先出。

    不过,在消费者读取数据时,有一个潜在的性能风险点。

    在生产者往 List 中写入数据时,List 并不会主动地通知消费者有新消息写入,如果消费者想要及时处理消息,就需要在程序中不停地调用 RPOP 命令(比如使用一个 while True 循环)。如果有新消息写入,RPOP 命令就会返回结果,否则,RPOP 命令返回空值,再继续循环。

    所以,即使没有新消息写入 List,消费者也要不停地调用 RPOP 命令,这就会导致消费者程序的 CPU 一直消耗在执行 RPOP 命令上,带来不必要的性能损失。

    为了解决这个问题,Redis提供了 BRPOP 命令。BRPOP 命令也称为阻塞式读取,客户端在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据。和消费者程序自己不停地调用 RPOP 命令相比,这种方式能节省 CPU 开销。

    2、如何处理重复的消息?

    消费者要实现重复消息的判断,需要 2 个方面的要求:

    • 每个消息都有一个全局的 ID。
    • 消费者要记录已经处理过的消息的 ID。当收到一条消息后,消费者程序就可以对比收到的消息 ID 和记录的已处理过的消息 ID,来判断当前收到的消息有没有经过处理。如果已经处理过,那么,消费者程序就不再进行处理了。

    但是 List 并不会为每个消息生成 ID 号,所以我们需要自行为每个消息生成一个全局唯一 ID,生成之后,我们在用 LPUSH 命令把消息插入 List 时,需要在消息中包含这个全局唯一 ID。

    例如,我们执行以下命令,就把一条全局 ID 为 111000102、库存量为 99 的消息插入了消息队列:

    127.0.0.1:6379> LPUSH mq "111000102:stock:99"
    (integer) 1
    
    • 1
    • 2

    3、如何保证消息可靠性?

    当消费者程序从 List 中读取一条消息后,List 就不会再留存这条消息了。所以,如果消费者程序在处理消息的过程出现了故障或宕机,就会导致消息没有处理完成,那么,消费者程序再次启动后,就没法再次从 List 中读取消息了。

    为了留存消息,List 类型提供了 BRPOPLPUSH 命令,这个命令的作用是让消费者程序从一个 List 中读取消息,同时,Redis 会把这个消息再插入到另一个 List(可以叫作备份 List)留存

    这样一来,如果消费者程序读了消息但没能正常处理,等它重启后,就可以从备份 List 中重新读取消息并进行处理了。

    好了,到这里可以知道基于 List 类型的消息队列,满足消息队列的三大需求(消息保序、处理重复的消息和保证消息可靠性)。

    • 消息保序:使用 LPUSH + RPOP
    • 阻塞读取:使用 BRPOP
    • 重复消息处理:生产者自行实现全局唯一 ID;
    • 消息的可靠性:使用 BRPOPLPUSH

    List 作为消息队列有什么缺陷?

    List 不支持多个消费者消费同一条消息,因为一旦消费者拉取一条消息后,这条消息就从 List 中删除了,无法被其它消费者再次消费。

    要实现一条消息可以被多个消费者消费,那么就要将多个消费者组成一个消费组,使得多个消费者可以消费同一条消息,但是 List 类型并不支持消费组的实现

    这就要说起 Redis 从 5.0 版本开始提供的 Stream 数据类型了,Stream 同样能够满足消息队列的三大需求,而且它还支持「消费组」形式的消息读取。
     

    后记

    Redis 五大常见数据类型之一的 List 就先讲到这里了,后续还会有其他类型的讲解呢,敬请关注!

    参考资料:

  • 相关阅读:
    安全风险 - 检测设备是否为模拟器
    Python连接MySQL、PostgreSQL数据库
    $display-SystemVerilog
    nSum问题解题套路4.5-4.6
    Fetch与Axios数据请求
    RedissonCach的源码流程
    多维下numpy实现torch下的log_softmax
    Kafka为什么性能这么快?4大核心原因详解
    halcon知识:矩阵专题【01】
    代码随想录算法训练营第九天|二叉树(截止到合并二叉树)
  • 原文地址:https://blog.csdn.net/weixin_46263782/article/details/127131698