Skip to content

1. 数据类型

数据类型和数据结构的关系

1.1 String

String 是最基本的 key-value 结构,key 是唯一标识,value 是具体的值,value其实不仅是字符串, 也可以是数字(整数或浮点数),value 最多可以容纳的数据长度是 512M

1.1.1 内部实现

String 类型的底层的数据结构实现主要是 intSDS(简单动态字符串)。

之所以没有使用 C 语言的字符串表示,因为 SDS 相比于 C 的原生字符串:

  • SDS 不仅可以保存文本数据,还可以保存二进制数据

因为 SDS 使用 len 属性的值而不是空字符来判定字符串是否结束,并且 SDS 所有的 API 都会以处理二进制的方式来处理 SDS 存放在buf[]数组里的数据。

  • SDS 获取字符串长度的时间复杂度是 O(1)

因为 C 语言的字符串并不记录自身长度,所以获取长度的复杂度为 O(n);而 SDS 结构里用 len 属性记录了字符串的长度,所以复杂度为 O(1)。

  • Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出

因为 SDS 在拼接字符串之前会检查 SDS 空间是否满足要求,若空间不足会自动扩容。

字符串对象的内部编码(encoding)有 3 种 :int、raw和 embstr。根据保存内容的类型和长度决定使用哪种编码和保存方式。

  • 如果一个字符串对象保存的是整数值,并且可用long类型表示,那么字符串对象会将整数值保存着字符串对象结构的ptr属性中(将void* 转换成 long) ,并将字符串对象的编码设置为int

  • 如果字符串对象保存的是一个字符串,并且长度小于 32 字节(根据 Redis 版本决定),那么字符串对象将使用一个 SDS 来保存字符串,编码设置为embstr,这个编码是专门用于保存短字符串的一种优化编码方式。

  • 如果字符串对象保存的是一个字符串,并且长度大于 32 字节(根据 Redis 版本决定),那么字符串对象将使用一个 SDS 来保存字符串,编码设置为raw

embstr 编码和 raw 编码的边界在 redis 不同版本中是不一样的:

  • redis 2.+ 是 32 字节
  • redis 3.0-4.0 是 39 字节
  • redis 5.0 是 44 字节

embstr会通过一次内存分配函数来分配一块连续的内存空间来保存redisObject和SDS,而raw编码会通过调用两次内存分配函数来分别分配两块空间来保存redisObject和SDS。Redis这样做会有很多好处:

  • embstr编码将创建字符串对象所需的内存分配次数从raw的两次减为一次
  • 释放embstr编码的字符串对象也只调用一次内存是否函数
  • embstr编码的字符串对象的所有数据保存着一块连续的内存中,可以更好地利用 CPU 缓存提升性能

embstr 也有缺点的:

  • 如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,所以embstr编码的字符串对象实际上是只读的,redis没有为embstr编码的字符串对象编写任何相应的修改程序。当我们对embstr编码的字符串对象执行任何修改命令(例如append)时,程序会先将对象的编码从embstr转换成raw,然后再执行修改命令。

1.1.2 应用场景

  1. 缓存对象

使用 String 来缓存对象有两种方式:

直接缓存整个对象的 JSON,命令例子: SET user:1 '{"name":"xiaolin", "age":18}'。 采用将 key 进行分离为 user:ID:属性,采用 MSET 存储,用 MGET 获取各属性值,命令例子: MSET user:1:name xiaolin user:1:age 18 user:2:name xiaomei user:2:age 20

  1. 常规计数

因为 Redis 处理命令是单线程,所以执行命令的过程是原子的。因此 String 数据类型适合计数场景。

  1. 分布式锁

SET 命令有个 NX 参数可以实现「key不存在才插入」,可以用它来实现分布式锁:

  • 如果 key 不存在,则显示插入成功,可以用来表示加锁成功;
  • 如果 key 存在,则会显示插入失败,可以用来表示加锁失败。
  • 一般而言,还会对分布式锁加上过期时间

SET lock_key unique_value NX PX 10000
PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。

而解锁的过程就是将 lock_key 键删除,但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。

Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。

 // 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

这样就通过使用 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁和解锁。

  1. 共享 Session 信息

通常我们在开发后台管理系统时,会使用 Session 来保存用户的会话(登录)状态,这些 Session 信息会被保存在服务器端,但这只适用于单系统应用,如果是分布式系统此模式将不再适用。

因为布式系统每次会把请求随机分配到不同的服务器。对于分布式系统来说可以使用同一个 Redis 存储 Session 来解决这个问题:

1.2 List

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

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

1.2.1 内部实现

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

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

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

常用命令:

# 将一个或多个值value插入到key列表的表头(最左边),最后的值在最前面
LPUSH key value [value ...] 
# 将一个或多个值value插入到key列表的表尾(最右边)
RPUSH key value [value ...]
# 移除并返回key列表的头元素
LPOP key     
# 移除并返回key列表的尾元素
RPOP key 

# 返回列表key中指定区间内的元素,区间以偏移量start和stop指定,从0开始
LRANGE key start stop

# 从key列表表头弹出一个元素,没有就阻塞timeout秒,如果timeout=0则一直阻塞
BLPOP key [key ...] timeout
# 从key列表表尾弹出一个元素,没有就阻塞timeout秒,如果timeout=0则一直阻塞
BRPOP key [key ...] timeout

1.2.2 应用场景

  1. 消息队列

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

如何满足消息保序需求

List 作为消息队列保存消息的话,就已经能满足消息保序的需求了。

list消息队列

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

为了解决这个问题,Redis 提供了 BRPOP 命令。BRPOP 命令也称为阻塞式读取,客户端在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据。

如何处理重复的消息

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

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

但是 List 并不会为每个消息生成 ID 号,所以我们需要自行为每个消息生成一个全局唯一 ID

如何保证消息可靠性

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

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

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

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

Stream 同样能够满足消息队列的三大需求,而且它还支持「消费组」形式的消息读取。

1.3 Hash

hash 和 string 对象存储结构的区别: hash

1.3.1 内部实现

Hash 类型的底层数据结构是由压缩列表或哈希表实现的:

  • 如果哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值,可由 hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;
  • 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的 底层数据结构。

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。

1.3.2 应用场景

  1. 缓存对象

一般对象用 String + Json 存储,对象中某些频繁变化的属性可以考虑抽出来用 Hash 类型存储。

1.4 Set

Set 类型是一个无序并唯一的键值集合。除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。

1.4.1 内部实现

Set 类型的底层数据结构是由哈希表或整数集合实现的:

  • 如果集合中的元素都是整数且元素个数小于 512 (默认值,set-maxintset-entries配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;
  • 如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。

常用命令:

# 交集运算
SINTER key [key ...]
# 将交集结果存入新集合destination中
SINTERSTORE destination key [key ...]

# 并集运算
SUNION key [key ...]
# 将并集结果存入新集合destination中
SUNIONSTORE destination key [key ...]

# 差集运算
SDIFF key [key ...]
# 将差集结果存入新集合destination中
SDIFFSTORE destination key [key ...]

1.4.2 应用场景

Set 类型比较适合用来数据去重和保障数据的唯一性,还可以用来统计多个集合的交集、错集和并集等,当我们存储的数据是无序并且需要去重的情况下,比较适合使用集合类型进行存储。

但是要提醒你一下,这里有一个潜在的风险。Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞。

在主从集群中,为了避免主库因为 Set 做聚合计算(交集、差集、并集)时导致主库被阻塞,我们可以选择一个从库完成聚合统计,或者把数据返回给客户端,由客户端来完成聚合统计。

1.5 Zset

Zset 类型(有序集合类型)相比于 Set 类型多了一个排序属性 score(分值)。对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成的,一个是有序结合的元素值,一个是排序值。默认升序排列。

1.5.1 内部实现

Zset 类型的底层数据结构是由压缩列表或跳表实现的:

  • 如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;
  • 如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。

1.5.2 应用场景

在面对需要展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页显示,可以优先考虑使用 Sorted Set。

Warning

不要在分数不一致的 SortSet 集合中去使用 ZRANGEBYLEX 和 ZREVRANGEBYLEX 指令,因为获取的结果会不准确。

# 返回指定成员区间内的成员,按字典正序排列, 分数必须相同。
ZRANGEBYLEX key min max [LIMIT offset count]
# 返回指定成员区间内的成员,按字典倒序排列, 分数必须相同
ZREVRANGEBYLEX key max min [LIMIT offset count]

zrangebylex,文本排序

1.6 Bitmap

Bitmap,即位图,是一串连续的二进制数组(0 和 1),可以通过偏移量(offset)定位元素。BitMap 通过最小的单位 bit 来进行0|1的设置,表示某个元素的值或者状态,时间复杂度为 O(1)。特别适合一些数据量大且使用二值统计的场景

# 设置值,其中value只能是 0 和 1
SETBIT key offset value

# 获取值
GETBIT key offset

# 获取指定范围内值为 1 的个数
# start 和 end 以字节为单位
BITCOUNT key start end

# BitMap间的运算, O(N)
# destkey 计算的结果,会存储在该key中
# key1 … keyn 参与运算的key,可以有多个,空格分割,not运算只能一个key
# 当 BITOP 处理不同长度的字符串时,较短的那个字符串所缺少的部分会被看作 0。返回值是保存到 destkey 的字符串的长度(以字节byte为单位),和输入 key 中最长的字符串长度相等。
BITOP <AND | OR | XOR | NOT> destkey key [key ...]

# 返回指定key中第一次出现指定value(0/1)的位置
BITPOS [key] [value]

1.6.1 内部实现

Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。

String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态,你可以把 Bitmap 看作是一个 bit 数组。

1.6.2 应用场景

Bitmap 类型非常适合二值状态统计的场景,这里的二值状态就是指集合元素的取值就只有 0 和 1 两种,在记录海量数据时,Bitmap 能够有效地节省内存空间。

  1. 签到统计
  2. 判断用户登陆态
  3. 连续签到用户总数

把每天的日期作为 Bitmap 的 key,userId 作为 offset,若是打卡则将 offset 位置的 bit 设置成 1。

key 对应的集合的每个 bit 位的数据则是一个用户在该日期的打卡记录。 对这些 key 进行与运算之后执行 BITCOUNT 统计即可。

1.7 HyperLogLog

HyperLogLog 是 Redis 2.8.9 版本新增的数据类型,是一种用于「统计基数」的数据集合类型,基数统计就是指统计一个集合中不重复的元素个数。但要注意,HyperLogLog 是统计规则是基于概率完成的,不是非常准确,标准误算率是 0.81%。

所以,简单来说 HyperLogLog 提供不精确的去重计数。

HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的内存空间总是固定的、并且是很小的。

在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数,和元素越多就越耗费内存的 Set 和 Hash 类型相比,HyperLogLog 就非常节省空间。

# 添加指定元素到 HyperLogLog 中
PFADD key element [element ...]

# 返回给定 HyperLogLog 的基数估算值。
PFCOUNT key [key ...]

# 将多个 HyperLogLog 合并为一个 HyperLogLog
PFMERGE destkey sourcekey [sourcekey ...]

1.7.1 内部实现

HyperLogLog

1.7.2 应用场景

  1. 百万级网页 UV 计数

1.8 GEO

主要用于存储地理位置信息,并对存储的信息进行操作。

# 存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中。
GEOADD key longitude latitude member [longitude latitude member ...]

# 从给定的 key 里返回所有指定名称(member)的位置(经度和纬度),不存在的返回 nil。
GEOPOS key member [member ...]

# 返回两个给定位置之间的距离。
GEODIST key member1 member2 [m|km|ft|mi]

# 根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]

1.8.1 内部实现

GEO 本身并没有设计新的底层数据结构,而是直接使用了 Sorted Set 集合类型。

GEO 类型使用 GeoHash 编码方法实现了经纬度到 Sorted Set 中元素权重分数的转换,这其中的两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为 Sorted Set 元素的权重分数。

这样一来,我们就可以把经纬度保存到 Sorted Set 中,利用 Sorted Set 提供的“按权重进行有序范围查找”的特性,实现 LBS 服务(Location-Based Service,基于位置信息服务)中频繁使用的“搜索附近”的需求。

1.8.2 应用场景

各种和位置相关的场景。

  1. 打车

1.9 Stream

Stream 是 Redis 5.0 版本新增加的数据类型,Redis 专门为消息队列设计的数据类型。

在 Stream 没出来之前,消息队列的实现方式都有着各自的缺陷,例如:

  • 发布订阅模式,不能持久化也就无法可靠的保存消息,并且对于离线重连的客户端不能读取历史消息的缺陷;
  • List 实现消息队列的方式不能重复消费,一个消息消费完就会被删除,而且生产者需要自行实现全局唯一 ID。

Stream 支持消息的持久化、支持自动生成全局唯一 ID、支持 ack 确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠。

XADD:插入消息,保证有序,可以自动生成全局唯一 ID;
XLEN:查询消息长度;
XREAD:用于读取消息,可以按 ID 读取数据;
XDEL:根据消息 ID 删除消息;
DEL:删除整个 Stream;
XRANGE:读取区间消息
XREADGROUP:按消费组形式读取消息;
XPENDING 和 XACK:
XPENDING 命令可以用来查询每个消费组内所有消费者「已读取、但尚未确认」的消息;
XACK 命令用于向消息队列确认消息处理已完成;

1.9.1 内部实现

1.9.2 应用场景

消息队列。

生产者插入消息:

> xadd mq * name ignorantshr
"1696668141040-0"
> xadd mq * name ccm
"1696668350668-0"

插入成功后会返回全局唯一的 ID:"1696668141040-0"。消息的全局唯一 ID 由两部分组成:

  • 第一部分“1696668141040”是数据插入时,以毫秒为单位计算的当前服务器时间;
  • 第二部分表示插入消息在当前毫秒内的消息序号,这是从 0 开始编号的。例如,“1696668141040-0”就表示在“1696668141040”毫秒内的第 1 条消息。

消费者读取消息:

> xread streams mq 1696668141000-0
1) 1) "mq"
   2) 1) 1) "1696668141040-0"
         2) 1) "name"
            2) "ignorantshr"
      2) 1) "1696668350668-0"
         2) 1) "name"
            2) "ccm"
读取消息的时候需要指定一个消息 id,从这个 id 的下一个消息开始返回数据。这个 id 不需要在 stream 中存在。

如果想要实现阻塞读(当没有数据时,阻塞住),可以调用 XRAED 时设定 BLOCK 配置项,单位是毫秒:

# 命令最后的“$”符号表示读取最新的消息
> XREAD BLOCK 10000 STREAMS mymq $
(nil)
(10.00s)

负载均衡

XGROUP 创建消费组

# 创建一个名为 group1 的消费组,0-0 表示从第一条消息开始读取。
> XGROUP CREATE mq group1 0-0
OK
# 创建一个名为 group2 的消费组,0-0 表示从第一条消息开始读取。
> XGROUP CREATE mq group2 0-0
OK

消费组 group1 内的消费者 consumer1 从 mymq 消息队列中读取所有消息的命令如下:

> XREADGROUP GROUP group1 consumer1 STREAMS mq >
1) 1) "mq"
   2) 1) 1) "1696668141040-0"
         2) 1) "name"
            2) "ignorantshr"
      2) 1) "1696668350668-0"
         2) 1) "name"
            2) "ccm"

消息队列中的消息一旦被消费组里的一个消费者读取了,就不能再被该消费组内的其他消费者读取了,即同一个消费组里的消费者不能消费同一条消息

> XREADGROUP GROUP group1 consumer2 STREAMS mq >
(nil)

但是,不同消费组的消费者可以消费同一条消息(但是有前提条件,创建消息组的时候,不同消费组指定了相同位置开始读取消息)。

> XREADGROUP GROUP group2 consumer1 COUNT 1 STREAMS mq >
1) 1) "mq"
   2) 1) 1) "1696668141040-0"
         2) 1) "name"
            2) "ignorantshr"

确认机制

Streams 会自动使用内部队列(也称为 PENDING List)留存消费组里每个消费者读取的消息,直到消费者使用 XACK 命令通知 Streams“消息已经处理完成”。

如果消费者没有成功处理消息,它就不会给 Streams 发送 XACK 命令,消息仍然会留存。此时,消费者可以在重启后,用 XPENDING 命令查看已读取、但尚未确认处理完成的消息。

> XPENDING mq group1
1) (integer) 2
2) "1696668141040-0"
3) "1696668350668-0"
4) 1) 1) "consumer1"
      2) "2"

查看 group2 里 consumer1 已从 mymq 消息队列中读取了哪些消息

> XPENDING mq group2 - + 10 consumer1
> XPENDING mq group2 - + 10 consumer1
1) 1) "1696668141040-0"
   2) "consumer1"
   3) (integer) 536420
   4) (integer) 1
2) 1) "1696668350668-0"
   2) "consumer1"
   3) (integer) 536420
   4) (integer) 1

XACK 命令通知 Streams,然后这条消息就会被删除:

> XACK mq group2 1696668141040-0
(integer) 1
> XPENDING mq group2 - + 10 consumer1
1) 1) "1696668350668-0"
   2) "consumer1"
   3) (integer) 664535
   4) (integer) 1

小结

  • 消息保序:XADD/XREAD
  • 阻塞读取:XREAD block
  • 重复消息处理:Stream 在使用 XADD 命令,会自动生成全局唯一 ID;
  • 消息可靠性:内部使用 PENDING List 自动保存消息,使用 XPENDING 命令查看消费组已经读取但是未被确认的消息,消费者使用 XACK 确认消息;
  • 支持消费组形式消费数据

1.9.3 与专业的消息队列比较

专业的消息队列,必须要做到两大块:

  • 消息不丢。
  • 消息可堆积。

消息不丢

生产者、队列中间件、消费者,要保证三个环节都不能丢失数据。

  • Redis 生产者会不会丢消息?生产者会不会丢消息,取决于生产者对于异常情况的处理是否合理。从消息被生产出来,然后提交给 MQ 的过程中,只要能正常收到(MQ 中间件)的 ack 确认响应,就表示发送成功,所以只要处理好返回值和异常,如果返回异常则进行消息重发,那么这个阶段是不会出现消息丢失的。
  • Redis 消费者会不会丢消息?不会,因为 Stream(MQ 中间件)会自动使用内部队列(也称为 PENDING List)留存消费组里每个消费者读取的消息,但是未被确认的消息。消费者可以在重启后,用 XPENDING 命令查看已读取、但尚未确认处理完成的消息。等到消费者执行完业务逻辑后,再发送消费确认 XACK 命令,也能保证消息的不丢失。
  • Redis 消息中间件会不会丢消息?会,Redis 在以下 2 个场景下,都会导致数据丢失:
    • AOF 持久化配置为每秒写盘,但这个写盘过程是异步的,Redis 宕机时会存在数据丢失的可能
    • 主从复制也是异步的,主从切换时,也存在丢失数据的可能。

像 RabbitMQ 或 Kafka 这类专业的队列中间件,在使用时是部署一个集群,生产者在发布消息时,队列中间件通常会写「多个节点」,也就是有多个副本,这样一来,即便其中一个节点挂了,也能保证集群的数据不丢失。

消息可堆积

Redis 的数据都存储在内存中,这就意味着一旦发生消息积压,则会导致 Redis 的内存持续增长,如果超过机器内存上限,就会面临被 OOM 的风险。

所以 Redis 的 Stream 提供了可以指定队列最大长度的功能,就是为了避免这种情况发生。

当指定队列最大长度时,队列长度超过上限后,旧消息会被删除,只保留固定长度的新消息。这么来看,Stream 在消息积压时,如果指定了最大长度,还是有可能丢失消息的。

但 Kafka、RabbitMQ 专业的消息队列它们的数据都是存储在磁盘上,当消息积压时,无非就是多占用一些磁盘空间。

1.9.4 发布订阅机制的缺点

发布订阅机制存在以下缺点,都是跟丢失数据有关:

  • 发布/订阅机制没有基于任何数据类型实现,所以不具备「数据持久化」的能力,也就是发布/订阅机制的相关操作,不会写入到 RDB 和 AOF 中,当 Redis 宕机重启,发布/订阅机制的数据也会全部丢失。
  • 发布订阅模式是“发后既忘”的工作模式,如果有订阅者离线重连之后不能消费之前的历史消息。
  • 当消费端有一定的消息积压时,也就是生产者发送的消息,消费者消费不过来时,如果超过 32M 或者是 60s 内持续保持在 8M 以上,消费端会被强行断开,这个参数是在配置文件中设置的,默认值是 client-output-buffer-limit pubsub 32mb 8mb 60

所以,发布/订阅机制只适合即时通讯的场景,比如构建哨兵集群的场景采用了发布/订阅机制。