常见的数据类型和应用场景

我们都知道,redis 中有五种常见的数据类型,分别是:String(字符串)、Hash(哈希)、List(列表)、Set(集合)、Zset(有序集合)。

不过呢,以上这五种都是比较常见的,还有几种不常见的,比如:BitMap、HyperLog、GEO、Stream。这几种不常见的都是后来随着 Redis 的版本更新迭代才出现的。

Redis 的这些数据类型还是挺重要的。因为在面试的时候,面试官可能会问我们 比如 String 数据类型有什么典型的应用场景吗?

所以,我们在学习这些数据类型的时候,还要注意每种数据类型的应用场景是什么。下面,我们就按顺序来学习一下 Redis 中所有的数据类型吧。

String

Redis 的 String 类型是 Redis 最基础的数据类型之一,也是最常用的数据结构。它在 Redis 中是一个简单的键值对存储,值可以是字符串、数字、二进制数据等。

定义

在 Redis 中,String 类型是一个键值对(key-value)的映射。每个键(key)对应一个字符串(value)。String 类型的值可以是任何字节序列,包括文本、二进制数据、数字等。

  • key:是 Redis 中的唯一标识符,可以是任意的字节序列,最大为 512MB。
  • value:可以是一个普通的字符串、二进制数据或数字,大小最大可以达到 512MB。

Redis 的 String 类型是最基本的数据类型,也是所有其他数据结构的基础,这是因为哈希、列表、集合等都可以通过 String 来存储。

内部实现

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

Redis 在处理 String 类型时,如果值是一个整数,它会采用整数编码进行存储。这种方式能最大化地节省内存,并提升性能。因为 Redis 会直接将整数值存储在内存中,而不需要进行额外的字符串转换或内存分配。

特点:

  • 当 String 类型的值是整数时,Redis 会将其存储为一个 整数值,而不是一个字符串。
  • 这种存储方式非常紧凑,因此可以节省大量内存,尤其是在处理大量数字时。
  • 采用这种方式时,Redis 对整数值进行存储和操作非常高效。

如果 String 类型的值是一个普通的字符串(即文本、二进制数据等),Redis 会使用 简单动态字符串(SDS) 来存储。

SDS 是 Redis 自定义的一种字符串类型,它比 C 语言的 char * 类型更高效。SDS 通过将字符串长度和容量信息嵌入到字符串数据的前面,避免了 C 字符串常见的空字符终止符问题,并且提供了更高效的字符串操作。

SDS的内存结构是这样的:

字段 描述
len 字符串的实际长度(不包括空字符)。
alloc SDS 分配的内存大小,表示 SDS 当前预分配的空间。
flags 标识 SDS 类型的额外标志。
buf 存储字符串的缓冲区,即实际存储字符数据的地方。

SDS 的优点也是显而易见的:

  • 避免了 C 字符串的空字符终止符:SDS 字符串没有使用空字符来标识字符串结尾,因此能够直接保存字符串的长度,避免了操作过程中的低效。
  • 预分配空间,避免频繁内存分配:SDS 会预分配一定的空间,在字符串长度增加时,只有在超过当前容量时才进行扩展。这样可以减少内存的分配次数。
  • 高效的字符串操作:支持快速的长度查询(通过 Len 字段),时间复杂度是O(1),因此操作字符串时可以避免遍历整个字符串。
  • 二进制安全:SDS 不依赖于字符编码,可以存储任意二进制数据。

常用指令

以下是一些常见的指令:

  • SET key value:设置键值对,如果键已经存在,更新其值。
SET mykey "hello"
Bash
  • GET key:获取指定键的值。如果键不存在,返回 null
GET mykey
Bash
  • DEL key:删除指定的键。如果键不存在,返回 0
DEL mykey
Bash
  • MSET key1 value1 key2 value2 …:一次设置多个键值对。
MSET key1 "value1" key2 "value2"
Bash
  • MGET key1 key2 …:一次获取多个键的值。
MGET key1 key2
Bash
  • INCR key:将指定键的值加 1。如果键不存在,会创建并初始化为 0。
INCR mykey
Bash
  • DECR key:将指定键的值减 1。
DECR mykey
Bash
  • APPEND key value:将指定值追加到已有的字符串值后面。
APPEND mykey " world"
Bash
  • GETSET key value:先获取键的值,再设置该键的新值。
GETSET mykey "new_value"
Bash
  • STRLEN key:获取指定键的字符串长度。
STRLEN mykey
Bash
  • SETNX key value:仅当键不存在时,才设置键值。
SETNX mykey "hello"
Bash

应用场景

缓存对象

  1. 直接缓存整个对象的 JSON

直接缓存整个对象的 JSON 是指将对象转换为一个 JSON 格式的字符串并存储在 Redis 中。

比如有一个用户对象:

{
  "user_id": 123,
  "name": "Alice",
  "email": "alice@example.com",
  "age": 30
}
Plain

可以通过 JSON 序列化将其转换为一个 JSON 字符串:

'{"user_id": 123, "name": "Alice", "email": "alice@example.com", "age": 30}'
Plain

然后使用 SET 命令将整个 JSON 字符串进行存储:

SET user:123 '{"user_id": 123, "name": "Alice", "email": "alice@example.com", "age": 30}'
Plain

这种情况适用于对象较小、变化不频繁或者读取操作远远多于更新操作的场景。

  1. 将 key 进行分离

将对象的各个属性拆分成独立的 Redis key,并使用 Redis 的 MSET 和 MGET 批量操作来存取这些属性值。

有一个用户对象,想将其分解为多个 Redis 键:

{
  "user_id": 123,
  "name": "Alice",
  "email": "alice@example.com",
  "age": 30
}
Plain

你可以将每个字段分别存储为不同的键:

SET user:123:name "Alice"
SET user:123:email "alice@example.com"
SET user:123:age 30
Plain

当需要批量获取一个对象的多个属性时,可以使用 MGET 来高效地获取:

MGET user:123:name user:123:email user:123:age
Plain

这种方法适用于对象属性变化频繁、对象较大且只需要频繁访问部分属性的场景。

常规计数

每篇文章的阅读量可以通过 Redis String 来存储,每次有用户阅读该文章时,就对对应的计数值进行加一操作。这个方式非常适合 高并发 的场景,因为 Redis 支持 原子性增量,并且 Redis 在内存中操作数据,非常高效。

设置计数键:为每篇文章定义一个唯一的 Redis key。这个 key 可以使用文章的 ID 来命名,比如 article:{id}:views ,其中 {id} 是文章的 ID。

如果文章的 ID 为 123 ,我们可以使用如下 Redis key:

article:123:views
Plain

使用 INCR 命令进行计数:INCR 命令用于 整数值的自增,并且是 原子性操作,即使在并发情况下也能保证正确性。每次用户阅读文章时,我们使用 INCR 命令对对应的文章阅读量进行加一操作。

用户每次阅读文章时执行以下命令:

INCR article:123:views
Plain

这会将 article:123:views 的值加一。如果该键不存在,Redis 会自动初始化为 0,然后加一。

获取当前文章的阅读量:使用 GET 命令读取对应的 Redis 键的值。

获取 article:123:views 的当前值:

GET article:123:views
Plain

设置过期时间:你可以使用 EXPIRE 命令为文章的阅读量设置过期时间。

如果你希望在 24 小时内计算阅读量,24 小时后数据自动清除,可以这样做:

EXPIRE article:123:views 86400  # 设置过期时间为 86400 秒(即 24 小时)
Plain

增量计数优化:如果需要对多个文章进行计数,可以利用 MSET 命令同时为多个文章进行阅读量更新:

MSET article:123:views 100 article:124:views 200
Plain

分布式锁

在分布式环境中,多个客户端可能会尝试同时获取一个共享资源的锁。为了确保只有一个客户端能够成功获取锁,我们可以使用 String 类型来实现一个分布式锁。

实现方式是使用 SET 命令设置一个键,表示该资源已经被锁定。通过 NX 参数,确保只有当该键不存在时才能成功设置,即当资源没有被锁定时,才能获取锁。通过 EX 参数设置锁的过期时间,避免死锁。 当客户端完成任务后,通过删除锁的键来释放锁。

我们来分析一下这个命令:

SET lock_key unique_value NX EX 30
Plain
  • lock_key:锁的键,可以使用资源的标识符(比如文章 ID、商品 ID)作为键的一部分。
  • unique_value:锁的值,一般可以是一个唯一的值,比如客户端的标识(例如 UUID)。确保同一个客户端不会在同一时刻获取多个锁。
  • NX:仅在键不存在时设置锁(即只有没有获取锁的其他客户端时才可以获取锁)。
  • EX 30:设置锁的过期时间为 30 秒,防止因某些原因客户端崩溃或者无法释放锁而导致死锁。

上述的这条命令是用来加锁的,那么如何实现解锁呢?

实际上,解锁的过程就是将 lock_key 这个键删除的过程。但是在删除的时候,需要先判断 unique_value 是否为客户端,如果是,则可以删除,否则就无法删除。也就是说,这个锁是谁加的,谁才有权利解锁。

解锁的过程是先判断,再删除,所以需要用到 Lua 脚本来实现原子性:

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

通过使用 String 的 SET 命令和 Lua 脚本,就实现了分布式锁。

共享 Session 信息

在 单体应用架构 中,通常将 Session 信息保存到服务器端,来存储用户的登录状态。当每次用户发起请求时,服务器通过 Session ID 来识别该用户的会话状态,从而知道该用户是否已登录,以及用户的相关信息(如权限、角色等) 。

但是在分布式系统中,每个服务器的内存是独立的,Session 信息只能在当前服务器上有效。如果用户的请求被路由到不同的服务器,新的服务器就无法获取该用户之前的会话数据,就会出现重复登陆的问题。

为了解决这个问题,我们可以使用 Redis 来集中存储 Session 信息。集中式存储的优势, 就是在分布式环境中,多个应用服务器可以通过访问 Redis 来获取和更新会话信息,无论请求被路由到哪个服务器,都会得到相同的会话数据,这样就解决了多个服务器之间数据无法共享的问题。

List

List 提供了一个简单的线性表形式,允许我们将多个元素按顺序存储在一个列表中。在 Redis 中,List 类型非常适合用于实现队列、栈等数据结构,并且提供了非常高效的插入、删除等操作。List 的最大长度是 2^32 – 1 。

定义

Redis 中的 List 类型是一个简单的双向链表,允许我们以顺序的方式存储多个元素。每个 List 都是一个由多个元素组成的集合,且可以在两端进行插入和删除操作,它支持按顺序读取和操作数据。

  • :List 通过一个键来进行访问的,每个 List 都有一个唯一的键。
  • :是一个有序的字符串列表,可以包含任何有效的 Redis 字符串。

特点:

  • 双向链表:实现基于双向链表,允许在两端进行高效的插入和删除操作。
  • 顺序存储:元素按照插入顺序排列,从左到右(LIFO 或 FIFO)。

内部实现

List 类型是使用双向链表或者压缩列表来实现的。

双向链表的内部存储结构包括节点和指针:

  • 节点:每个 List 中的元素都是一个链表节点。每个节点包含一个值和两个指针,分别指向前一个节点和后一个节点。
  • 指针:通过前后指针,Redis 可以在 List 的两端进行快速的插入和删除操作。

如果列表中的元素数量小于 512 个并且每个元素的字节大小都小于 64 ,那么 List 就会使用压缩列表来存储数据。否则,List 就会使用双向链表。

在 redis 3.2 版本之后,出现了 quicklist,由它来替代双向链表和压缩列表。

quicklist** **是双向链表和压缩列表的结合体,它将多个压缩列表串联起来,既保证了插入和删除操作的高效性,又减少了内存的占用。

常用指令

下面是一些常见的指令:

插入和删除操作

  • LPUSH key value [value …]:将一个或多个元素插入到列表的左端(头部)。如果列表不存在,创建一个新列表。
LPUSH mylist "apple"
LPUSH mylist "banana"
Bash
  • RPUSH key value [value …]:将一个或多个元素插入到列表的右端(尾部)。如果列表不存在,创建一个新列表。
RPUSH mylist "orange"
RPUSH mylist "grape"
Bash
  • LPOP key:移除并返回列表的第一个元素(左端)。如果列表为空,返回 nil
LPOP mylist
Bash
  • RPOP key:移除并返回列表的最后一个元素(右端)。如果列表为空,返回 nil
RPOP mylist
Bash
  • LREM key count value:从列表中删除指定的元素。如果 count 为正,删除从左边开始的元素;如果 count 为负,删除从右边开始的元素;如果 count 为 0,删除所有与 value 匹配的元素。
LREM mylist 2 "apple"
Bash
  • LINSERT key BEFORE|AFTER pivot value:在列表的某个元素之前或之后插入新元素。
LINSERT mylist BEFORE "banana" "pear"
Bash

查询操作

  • LRANGE key start stop:获取列表指定范围内的元素,范围从 startstop(包含)。索引从 0 开始,负数表示从列表尾部开始(-1 是最后一个元素)。
LRANGE mylist 0 2  # 获取前3个元素
Bash
  • LLEN key:获取列表的长度。
LLEN mylist
Bash
  • LINDEX key index:获取列表中指定索引位置的元素。如果索引超出范围,返回 nil
LINDEX mylist 1
Bash
  • LSET key index value:将列表中指定索引位置的元素设置为新的值。如果索引超出范围,返回错误。
LSET mylist 0 "kiwi"
Bash

其他操作

  • BRPOP key [key …] timeout:阻塞式地移除并返回列表的最后一个元素。如果列表为空,会阻塞直到超时。
BRPOP mylist 5  # 阻塞 5 秒,等待有元素时返回
Bash
  • BLPOP key [key …] timeout:阻塞式地移除并返回列表的第一个元素。
BLPOP mylist 5  # 阻塞 5 秒,等待有元素时返回
Bash

应用场景

消息队列

List 经常被用来实现消息队列,通过使用 LPUSH 和 RPOP 或者 RPUSH 和 LPOP 操作,我们可以实现一个简单的消息队列。

  • 生产者将任务添加到队列的头部,也就是 LPUSH。
  • 消费者从队列的尾部取出任务,也就是 RPOP。
# 生产者
LPUSH task_queue "task1"
LPUSH task_queue "task2"

# 消费者
RPOP task_queue  # 获取并删除 task1
RPOP task_queue  # 获取并删除 task2
Bash

通过这两个命令,我们就可以实现一个简单的消息队列。

但是,一个可靠的消息队列,必须要具备消息保序、处理重复消息和保证消息可靠性这三个特点。那我们就来看看应该如何实现。

1、消息保序

其实我们使用 LPUSH 和 RPOP 这两个命令,就已经能够保证消息是有序的了。但是,还有一个潜在的问题。

那就是当生产者往 List 中添加消息时,List 并不会主动的通知消费者此时有新的消息进来了,所有消费者就需要在队列的另一端不断地调用 RPOP 命令,来确认是否有新的消息进来。如果有,就可以读取到;如果没有,就会返回一个空值。

这样的话,在读取不到值的时候,就会造成 CPU 不必要的消耗,性能就比较差。那这种情况就可以使用 BRPOP 来实现消息的阻塞式读取。

BRPOP 是 Redis 提供的一个阻塞式命令,专门用于从列表中读取并移除元素。如果列表为空,BRPOP 会让消费者进入阻塞状态,直到列表中有新元素可以返回,或者超时为止。这样就能有效避免轮询和不必要的请求,减少系统负担。

2、处理重复消息

为了确保消费者能够判断和避免重复消息的处理,通常需要以下两个方面的要求:

  1. 每个消息都有一个全局的 ID

每个消息都必须有一个唯一 ID,List 并不会生成 ID,所以需要我们来生成,比如可以用 UUID 生成一个。这个全局的 ID 用于确保每个消息是唯一的,并且能够在消费者端进行唯一标识和追踪。全局 ID 通常是消息的元数据的一部分,在生产者发送消息时附带在每个消息中。

  1. 消费者需要记录已经处理过的消息的 ID

每当消费者处理一个消息时,可以将该消息的 ID 添加到一个集合中。然后,消费者在处理新消息之前,会首先检查消息 ID 是否已经存在于该集合中。如果存在,则说明这个消息已经被处理过,消费者将跳过该消息,从而避免重复处理。

3、保证消息的可靠性

采用一种 消息备份 机制,常见的做法是在 读取消息之后,将其备份到另一个列表,以确保消息在处理过程中能够被恢复或追踪。这样即使在处理过程中出现异常或者消费失败,备份的消息仍然存在,可以重新尝试处理。

步骤是这样的:

  • 创建两个队列,一个用来存储待处理的消息的队列 task_queue, 另一个用来存储消息的备份的队列backup_queue。
  • 消费者使用 BRPOP 从 task_queue 中阻塞式读取消息。
  • 消费者处理消息之前,先将该消息存入备份队列 backup_queue 。
  • 消费者处理消息后,如果处理成功,可以继续从 task_queue 中删除消息。
  • ****如果消费者在处理过程中出现异常,可以从备份队列中恢复未处理的消息。

Hash

定义

哈希数据类型是一个键值对(field – value)集合,它将多个键值对存储在一个键中,就像是一个小型的字典。每个哈希键可以包含多个字段(field),每个字段都有一个对应的值(value)。这样就使得哈希类型特别适合用于存储和管理对象,因为可以将对象的各个属性作为字段,属性值作为对应的值存储在哈希中。

内部实现

Redis 的哈希数据类型内部采用两种数据结构来实现:压缩列表(ziplist)和哈希表(hashtable)。

  • 压缩列表(ziplist):当哈希中包含的键值对数量小于 512 ,也就是参数 hash-max-ziplist-entries,并且每个字段和值的长度都小于 64 字节,也就是参数 hash-max-ziplist-value ,Redis 会使用压缩列表来存储哈希数据。压缩列表是一种紧凑的存储结构,它将多个元素紧凑地存储在一起,通过特殊的编码方式来节省内存空间。
  • 哈希表(hashtable):如果不满足上述条件,Redis 会切换到哈希表来存储。哈希表是一种基于哈希算法的数据结构,它能够提供快速的查找、插入和删除操作。Redis 的哈希表实现采用链地址法来处理哈希冲突,确保在高并发情况下依然能够高效地处理数据。

常用命令

  • HSET key field value:向指定的哈希键 key 中设置一个字段 field 及其对应的值 value 。
HSET user:1 name "Alice",将user:1这个哈希中的name字段设置为"Alice"
Bash
  • HGET key field:从指定的哈希键 key 中获取字段 field 的值。
HGET user:1 name,会返回user:1哈希中name字段的值"Alice"
Bash
  • HGETALL key:返回指定哈希键 key 的所有字段和值。
HGETALL user:1,会返回类似name "Alice" age "25" email "alice@example.com"这样的结果。
Bash
  • HDEL key field [field…]:删除指定哈希键 key 中的一个或多个字段。
HDEL user:1 age,会删除user:1哈希中的age字段。
Bash
  • HEXISTS key field:检查指定哈希键key中是否存在字段field。如果存在返回1,不存在返回0。
HEXISTS user:1 name,若user:1哈希中存在name字段,就返回1。
Bash

应用场景

缓存对象

在 Web 应用中,经常需要缓存用户信息、商品信息等对象。使用 Redis 的哈希数据类型可以将对象的各个属性存储在一个哈希中,通过一个键来管理整个对象。

比如我想缓存一本书:

# 设置书名
HSET book:1 title "The Great Gatsby"
# 设置作者
HSET book:1 author "F. Scott Fitzgerald"
# 设置出版年份
HSET book:1 publication_year 1925
Bash

然后获取这些信息:

# 获取书名
HGET book:1 title
# 获取作者
HGET book:1 author
# 获取出版年份
HGET book:1 publication_year
Bash

其实 String 也可以用来缓存对象。如果使用 String 缓存对象,通常是将整个对象序列化为一个字符串,比如 JSON 格式的字符串,然后存储在 String 类型的 value 中。

那我们应该如何选择呢?

对象结构简单且变化频率低,就用 String 类型;需要对对象的部分字段单独操作并且变化频率高,就用 Hash 类型。

购物车功能

购物车通常包含以下几个部分:

  • 商品ID(item_id)
  • 商品的数量(quantity)
  • 商品的单价(price)

可以把每个购物车的商品信息存储在一个唯一的 key 下,每个商品作为一个字段( field ),而商品的属性(如数量、单价)作为对应字段的值( value )。

假设我们用 user_id 作为哈希的 key ,每个商品的 item_id 作为哈希的 field,商品的数量和单价作为字段的 value 。当用户将商品添加到购物车时,我们使用 HSET 操作来设置商品的数量和价格。

HSET cart:user:1 item:101 quantity 3 price 20
Bash

如果用户选择增加或减少商品的数量,可以使用 HINCRBY 来增减数量。

HINCRBY cart:user:1 item:101 quantity 1 
Bash

这个命令将用户 1 的购物车中 item:101 商品的数量增加 1。

总之,Hash 中的 HSET 、HINCRBY 、HDEL 和 HGETALL 这些命令,使得实现添加商品、修改数量、删除商品和查询购物车等功能变得非常容易。

Set

定义

Set 是一个无序的字符串集合。它有以下三个特点:

  • 无序性:元素没有顺序,是随机存储的。
  • 唯一性:元素是唯一的,不允许有重复元素。如果插入相同的元素,Redis 会自动忽略重复项。
  • 支持集合操作:可以进行集合操作,如交集、并集、差集等,可以方便地进行集合运算。

一个集合最多可以存储 2^32-1 个元素。

内部实现

Set 类型在内部实现上实际上有两种不同的实现方式:哈希表 和 **整数集合 **。

  • 如果全部是整数,并且元素数量小于 512 个,Redis 会使用 整数集合 来存储数据。
  • 如果不满足上述条件,Redis 就会使用 **哈希表 **来实现 Set 。

常用命令

以下是一些最常用的命令:

  • SADD:向 Set 中添加一个或多个元素。如果元素已经存在,Redis 会忽略该元素。
SADD key member [member ...]
Plain
  • SREM:从 Set 中移除一个或多个元素。如果元素不存在,Redis 会忽略该元素。
SREM key member [member ...]
Plain
  • SMEMBERS:获取 Set 中所有的元素。
SMEMBERS key
Plain
  • SISMEMBER:检查某个元素是否存在于 Set 中。
SISMEMBER key member
Plain
  • SCARD:获取 Set 中元素的数量(即 Set 的大小)。
SCARD key
Plain
  • SPOP:从 Set 中随机移除并返回一个元素。如果 Set 为空,则返回 nil
SPOP key
Plain
  • SDIFF:返回一个或多个 Set 之间的差集。
SDIFF key [key ...]
Plain
  • SUNION:返回一个或多个 Set 之间的并集。
SUNION key [key ...]
Plain
  • SINTER:返回一个或多个 Set 之间的交集。
SINTER key [key ...]
Plain

应用场景

1、去重

去重用户操作:存储某用户访问的不同页面 ID,保证每个页面仅计数一次。

SADD user:1234:visited_pages "page1" "page2" "page3"
Bash

去重投票或者点赞:在投票或点赞系统中,确保每个用户只能投票或点赞一次,避免重复投票。

SADD article:5678:likes "user1" "user2" "user3"
Bash

2、标签系统

存储用户兴趣标签:存储某个用户的兴趣标签,并且保证兴趣标签是唯一的。

SADD user:1234:tags "sports" "music" "technology"
Bash

3、社交网络

存储用户的好友列表:记录用户的好友,保证列表中没有重复的用户。

SADD user:1234:friends "user1" "user2" "user3"
Bash

存储用户的粉丝列表:记录某用户的粉丝,确保每个粉丝只能出现一次。

SADD user:5678:followers "user1" "user3"
Bash

计算共同好友:通过 SINTER 命令,可以计算两个用户之间的共同好友。

SINTER user:1234:friends user:5678:friends
Bash

4、实现集合运算

用户兴趣的交集:查找两个用户的共同兴趣标签。

SINTER user:1234:tags user:5678:tags
Bash

5、限制条件

一次性优惠券:记录已经领取某个优惠券的用户,因为对于一次性使用的优惠券,要避免用户重复领取。

SADD coupon:1234:used "user1" "user3"
Bash

Zset

定义

Redis 中的是一个 不重复 的字符串集合,每个元素都关联一个 分数(score)。Zset 中的元素是按照分数的升序排列的,分数相同的元素会根据字典序排列。

  • 不重复性:元素是唯一的,不能插入重复的元素。
  • 分数:每个元素都有一个关联的分数,分数用于对元素进行排序。
  • 排序:自动按照分数从小到大排列元素。

简单来说,Zset 就是一个有序的集合,元素有顺序,并且每个元素都有一个与之关联的分数,分数决定了排序的顺序。

内部实现

Zset 的内部实现有两种:跳表 和 **压缩列表 **。

  • 如果元素数量较少(少于 128 个元素),且这些元素是简单的整数类型或短字符串,Redis 会使用压缩列表。
  • 如果元素较多,或者元素包含了很多复杂的字符串或对象(不是简单的整数或小字符串),Redis 会选择跳表。

常用命令

  • ZADD:向 Zset 中添加一个或多个元素。如果元素已经存在,更新该元素的分数。
ZADD key score member [score member ...]
Bash
  • ZRANGE:返回 Zset 中指定区间内的元素(按分数升序排序)。可以使用 WITHSCORES 选项同时返回元素的分数。
ZRANGE key start stop [WITHSCORES]
Bash
  • ZREVRANGE:与 ZRANGE 类似,但按分数降序排列返回元素。
ZREVRANGE key start stop [WITHSCORES]
Bash
  • ZSCORE:返回某个元素的分数。
ZSCORE key member
Bash
  • ZINCRBY:增加某个元素的分数。如果该元素不存在,则会创建该元素并设置分数。
ZINCRBY key increment member
Bash
  • ZREM:从 Zset 中移除一个或多个元素。
ZREM key member [member ...]
Bash
  • ZRANK 和 ZREVRANK:返回元素在 Zset 中的排名(按分数升序排序)。ZRANK 返回元素的排名,ZREVRANK 返回按分数降序排序时的排名。
ZRANK key member
ZREVRANK key member
Bash

应用场景

游戏排行榜

在一个游戏中,玩家的分数可以通过 Zset 存储,玩家的 ID 为元素,分数为分数值,Zset 会自动根据分数对玩家进行排序。

ZADD leaderboard 100 "player1" 200 "player2" 150 "player3"
ZREVRANGE leaderboard 0 2 WITHSCORES  # 获取分数最高的前 3 名玩家
Plain

社交平台的热度排行

在一个社交平台中,可以使用 Zset 来存储帖子或话题的热度,热度分数可以根据点赞数、评论数等来计算。Zset 会根据热度分数自动排序,方便获取最热的帖子或话题。

ZADD hot_posts 500 "post1" 300 "post2" 800 "post3"
ZREVRANGE hot_posts 0 2 WITHSCORES  # 获取热度最高的前 3 个帖子
Plain

姓名排序

假设要根据 姓名的字典顺序 对一组姓名进行排序,可以将 姓名的 ASCII 码(或者其字典顺序) 作为 分数(score),而将 姓名本身 作为 元素(member) 存储在 Zset 中。这样,Zset 就会根据 字典顺序 排序姓名。

有一组姓名:Alice 、Bob 、Charlie,按姓名的字典顺序来排序:

ZADD namebook 1 "Alice" 2 "Bob" 3 "Charlie"
Plain

这里将字母的顺序(数字)作为分数,Zset 会自动根据这些分数对姓名进行排序。

按字典顺序升序排序(从 A 到 Z):

ZRANGE namebook 0 -1 WITHSCORES
Plain

这个命令会按字典顺序返回所有姓名,WITHSCORES 会显示每个姓名的分数。

返回结果:

1) "Alice"
2) "1"
3) "Bob"
4) "2"
5) "Charlie"
6) "3"
Plain

BitMap

定义

BitMap 并不是一个独立的数据类型,而是一个 特殊的操作模式,它允许你将 Redis 的 字符串(String) 类型当作位数组来使用。也就是说,BitMap 实际上是利用 Redis 的 String 类型来进行位操作的,它允许你对单个字节的单个位进行操作。因为 bit 是计算机中最小的单位,所以用它来存储就会非常节省空间。

BitMap 有以下三个特点:

  • 每一位(bit) 可以存储一个 布尔值,即 0 或 1 。
  • 大小是可变的,可以通过 Redis 的命令来设置和获取不同位置的位。
  • 通过字符串类型来实现的,每个字节存储多个二进制位,因此非常高效地利用内存。

每个位都可以通过索引访问,索引从 0 开始。

内部实现

BitMap 使用的是 字符串(String) 类型的特殊操作。每个字符串可以用来表示一个 位数组,每个字符都存储一个或多个二进制位(byte)。因此,BitMap 的存储结构与 Redis 的 字符串 数据类型共享相同的内部存储机制。BitMap 就相当于是一个 bit 数组。

常用命令

  • SETBIT:设置指定位置的位为 1 或 0。
SETBIT key offset value
Plain
  • GETBIT:获取指定位置的位值(0 或 1)。
GETBIT key offset
Plain
  • BITCOUNT:计算给定的 BitMap 中有多少个 1(即已设置为 true 的位)。
BITCOUNT key [start end]
Plain
  • BITOP:执行位运算(如 AND、OR、XOR)操作,结果存储在新的 BitMap 键中。
BITOP operation destkey key1 [key2 ...]
Plain
  • BITFIELD:执行多个位操作并返回结果,支持读取和修改多个位字段。
BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment]
Plain

应用场景

签到统计

将每个用户的签到状态用一个 来表示,1 表示用户已经签到,0 表示用户未签到。

我们将用户的 ID 映射到位的位置上,用户 ID 为 1001 的用户的签到状态存储在第 1001 位,ID 为 1002 的用户的签到状态存储在第 1002 位,以此类推。

比如用户 1001 今天已经签到:

SETBIT signin:2025 1001 1  # 用户 ID 1001 在 2025 年的签到状态设置为 1
Plain

这里的 signin:2025 是存储 2025 年签到状态的 BitMap 键,1001 是用户 ID,1 表示该用户已签到。

使用 GETBIT 命令查询某个用户的签到状态。

比如检查用户 1001 是否已签到:

GETBIT signin:2025 1001  # 返回 1 表示已签到,返回 0 表示未签到
Plain

使用 BITCOUNT 命令可以统计 BitMap 中为 1 的位数,即统计已签到的用户数量。

比如统计 2025 年所有已签到的用户数量:

BITCOUNT signin:2025  # 返回已签到的用户总数
Plain

判断用户登录态

同样的,每个位代表一个用户的登录状态,将用户的 ID 映射到位的位置上。当用户登录时,设置对应位为 1;当用户登出时,设置对应位为 0。

用户 1001 登录:

SETBIT user_login 1001 1  # 用户 ID 1001 登录,设置为 1
Plain

当用户 1001 登出:

SETBIT user_login 1001 0  # 用户 ID 1001 登出,设置为 0
Plain

查询用户 1001 是否已登录:

GETBIT user_login 1001  # 返回 1 表示已登录,返回 0 表示未登录
Plain

统计所有已登录的用户数:

BITCOUNT user_login  # 返回已登录的用户数量
Plain

HyperLogLog

定义

HyperLogLog 是一种概率算法,主要用于估算基数,即某个数据集合中的不同元素的数量。它与传统的精确计数方法不同,使用了一种统计方法,通过存储少量的数据来估算大规模集合中的不同元素数量。

HyperLogLog 是通过概率统计的方法来实现的,因此它不需要存储所有的元素,而只需要使用少量内存来估算结果。每个 HyperLogLog 键只需要花费 12 KB 的内存,就可以计算将近 2^64 个不同元素的基数。但是,它一定程度的误差,通常误差在 0.81% 左右。

内部实现

HyperLogLog 主要基于哈希函数和桶来实现基数估算。核心原理是通过哈希算法将每个元素映射到一个桶中,并通过计算哈希值的前导零的数量来估算集合中的不同元素的数量。

整个过程非常复杂,简单来描述的话可以分为这四步:

  1. 哈希映射:每个元素通过一个哈希函数映射到一个较大的空间中。Redis 使用的是 MurmurHash 或 SHA1 等哈希函数来生成元素的哈希值。
  2. :HyperLogLog 使用一个 桶数组,每个桶存储一个值,表示哈希值前导零的数量。每次新的元素加入时,哈希值的前导零的数量会更新对应桶的值。
  3. 前导零统计:对每个元素进行哈希后,查看其哈希值中的前导零的数量。然后将该数量记录到桶中,桶的值是该元素哈希值前导零数量的最大值。通过统计所有桶的最大值,HyperLogLog 可以估算出集合的基数。
  4. 估算基数:当所有元素都映射到桶中后,HyperLogLog 根据桶的最大前导零数量来估算基数。该算法可以通过数学公式计算得到一个接近实际值的基数。

常用命令

HyperLogLog 的命令,就三个:

  • PFADD:将一个或多个元素添加到 HyperLogLog 中。每个元素会通过哈希映射到桶中,用于估算基数。
PFADD key element [element ...]
Plain
  • PFCOUNT:返回 HyperLogLog 中不同元素的估算数量,即基数。
PFCOUNT key [key ...]
Plain
  • PFMERGE:将多个 HyperLogLog 合并为一个新的 HyperLogLog。这个命令用于将不同的数据源中的 HyperLogLog 基数进行合并,以便一起计算基数。
PFMERGE destkey sourcekey [sourcekey ...]
Plain

应用场景

UV 计数

HyperLogLog 可以用于估算某个网站、API 或应用的 唯一访问者数。每个用户或访问者可以视为一个独特的元素,使用 HyperLogLog 来估算不同用户或 IP 地址的数量。

比如我想估算网站的独立访客数:

PFADD unique_visitors "user1" "user2" "user3"  # 每次访问时增加一个用户
PFCOUNT unique_visitors  # 估算独立访问者数
Plain

GEO

定义

Redis 中的 GEO 数据类型允许你存储一组地理坐标(经度和纬度)。每个地点或物体都由 经度纬度名称 三部分组成。在 Redis 中,这些地理数据可以通过一系列 命令 来存储、查询、计算距离、查找范围内的地点等。

通过 GEO 类型,Redis 可以执行地理空间查询(如范围查询、距离计算等)。

内部实现

GEO 是基于 Geohash(地理哈希)和跳表实现的。Geohash 是一种空间索引技术,将地理坐标(经纬度)转换为一个短字符串。它还将经纬度空间分割成不同的网格,并将这些网格编码成字符串。跳表的作用就是来存储每个地点的 Geohash 值。

常用命令

  • GEOADD:将一个或多个地理位置添加到指定的 GEO 数据集合中,每个位置包含名称、经度和纬度。
GEOADD key longitude latitude member [longitude latitude member ...]
Plain
  • GEODIST:计算两个地点之间的距离,支持不同的单位(如米、千米、英里等)。
GEODIST key member1 member2 [unit]
Plain
  • GEOPOS:获取一个或多个地点的经纬度坐标。
GEOPOS key member [member ...]
Plain
  • GEORADIUS:返回给定半径范围内的所有地点,可以通过经纬度和半径进行范围查询。
GEORADIUS key longitude latitude radius [m|km|ft|mi] [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]
Plain

比如,获取半径 50 千米内的地点,查询位置为 Palermo :

GEORADIUS locations 13.361389 38.115556 50 km WITHCOORD
Plain
  • GEORADIUSBYMEMBER:与 GEORADIUS 类似,但是查询是基于某个现有地点(而不是经纬度)来进行的。
GEORADIUSBYMEMBER key member radius [m|km|ft|mi] [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]
Plain

比如,获取 Palermo 半径 50 千米内的所有地点:

GEORADIUSBYMEMBER locations "Palermo" 50 km WITHCOORD
Plain
  • GEOLIST:返回指定的地理位置列表。
GEOLIST key member [member ...]
Plain

应用场景

商店/门店定位

GEO 数据类型可以用于存储和查询多个商店或门店的位置,用户可以通过其经纬度查询周围的门店,类似于在电子商务或商场应用中的门店查询。

用户在应用中输入自己的位置,通过 GEORADIUS 查找附近 5 公里内的所有商店:

GEORADIUS stores 12.971598 77.594566 5 km WITHCOORD  # 查询半径 5 公里的商店
Plain

外卖配送

外卖配送服务通常需要根据用户位置来计算配送范围,Redis 的 GEO 数据类型可以用来高效地查询用户位置周围的可配送商店或餐厅。

使用 GEORADIUSBYMEMBER 命令查找用户位置附近的商家,然后计算配送费用和时间:

GEORADIUSBYMEMBER restaurants "user_location" 3 km WITHCOORD
Plain

打车服务

在打车应用中,GEO 类型可以用于存储所有出租车的位置,并实时获取附近的出租车供用户选择。

用户查询附近的可用出租车:

GEORADIUS taxis 13.361389 38.115556 3 km WITHCOORD  # 查询用户半径3公里内的出租车
Plain

社交网络中的地理标记

在社交平台中,用户可以根据地理位置发布带有位置信息的动态,其他用户可以基于这些位置信息进行查找,类似于“签到”功能。

根据用户的当前位置,查找附近的朋友或动态:

GEORADIUS users_location 13.361389 38.115556 5 km WITHCOORD  # 查找 5 公里内的朋友
Plain

地理广告定位

地理广告定位是许多广告系统中常见的场景。商家可以根据用户所在位置推送附近的广告内容。

根据用户当前位置,推送附近的广告商家:

GEORADIUS ad_location 12.971598 77.594566 10 km WITHCOORD  # 查询10公里内的广告商家
Plain

旅游推荐系统

GEO 存储景点位置,并根据用户当前所在位置推荐附近的景点或活动。

用户在旅游应用中输入当前经纬度,获取 20 公里内的所有景点:

GEORADIUS landmarks 13.361389 38.115556 20 km WITHCOORD  # 查询附近景点
Plain

Stream

介绍

Stream 是 Redis 5.0 版本新增加的数据类型,在 Stream 没有出现之前,消息队列的实现方式有很多问题,比如:

  • 用 List 实现的消息队列不能重复消费。
  • 在发布订阅模式下,不能可靠的保存消息。

基于出现的问题,Stream 就出现了。

Stream 是一种日志结构化的队列,用于管理一组数据流。每个 Stream 都由多个消息组成,每个消息由唯一ID 和多个字段-值对组成。

Stream 的核心特性有以下这些:

  • 顺序存储:每个消息在 Stream 中都有一个唯一的 ID,这个 ID 是由 Redis 自动生成的,ID 具有时间戳顺序。
  • 消息持久化:消息被写入 Stream 后,默认会持久化到磁盘(取决于 Redis 的持久化配置)。
  • 消费者组支持:Stream 支持消费者组,允许多个消费者共享消费消息。
  • 高效的消息消费:支持通过消费者组进行消息消费,支持 消息确认消息ACK,确保消息的可靠消费。
  • 支持历史数据查询:Stream 支持按时间顺序查询消息,可以实现 实时流处理历史数据回溯

内部实现

Stream 内部是基于 双端链表 和 **字典 **实现的。每条消息是链表中的一个节点,节点存储消息的 ID 和字段-值对。

Stream 数据结构的组成:

  1. 消息 ID:每条消息都有一个唯一的 ID,格式为 时间戳 + 序列号,例如:1609459200000-0 。时间戳表示消息的创建时间,序列号确保同一时间戳下的消息按顺序排列。
  2. 消息字段和值:每条消息可以包含多个字段和值,类似于哈希表的键值对。
  3. 消费者组:支持消费者组,每个消费者组可以有多个消费者。每个消费者负责从队列中读取消息,并通过 消息确认机制(ACK) 确保消息被可靠消费。
  4. 双端链表:使用双端链表存储消息,可以高效地从两端添加和删除消息。
  5. 字典:用于管理消费者组的状态(例如消息的ACK状态)。

常用命令

  • XADD:向 Stream 中添加消息。消息包括一个唯一的 ID 和一组字段-值对。
XADD key [ID] field1 value1 [field2 value2 ...]
Plain

向名为 mystream 的 Stream 中添加一条消息:

XADD mystream * name Alice age 30
Plain

这里 * 表示让 Redis 自动生成一个消息 ID。

  • XRANGE:从 Stream 中查询指定范围的消息。可以指定消息 ID 范围,或者查询所有消息。
XRANGE key [start] [end] [COUNT count]
Plain

查询 mystream 中的所有消息:

XRANGE mystream - +
Plain
  • XREAD:从 Stream 中读取消息。可以从多个 Stream 中同时读取消息,支持阻塞读取。
XREAD [BLOCK milliseconds] [COUNT count] STREAMS key [key ...] [ID ID ...]
Plain

阻塞读取 mystream 中的最新消息:

XREAD BLOCK 0 STREAMS mystream >
Plain
  • XGROUP:用于管理消费者组,允许多个消费者共享消费消息。

创建消费者组 mygroup,基于 mystream:

XGROUP CREATE mystream mygroup $
Plain
  • XREADGROUP:消费者组读取消息。每个消费者从消费者组的消息队列中读取消息,并根据需要进行 ACK 确认。
XREADGROUP GROUP group consumer [BLOCK milliseconds] [COUNT count] STREAMS key [key ...] [ID ID ...]
Plain

消费者 consumer1 从消费者组 mygroup 中读取消息:

XREADGROUP GROUP mygroup consumer1 STREAMS mystream >
Plain
  • XACK:确认消息已经被消费者成功处理。通过 ACK,Redis 可以确保消息不会被重复消费。
XACK key group ID [ID ...]
Plain

确认消费者组 mygroup 中的某条消息已被处理:

XACK mystream mygroup 1609459200000-0
Plain
  • XTRIM:修剪 Stream,删除已处理的旧消息,保持 Stream 的大小。
XTRIM key MAXLEN ~ count
Plain

保持 mystream 中的消息数不超过 1000:

XTRIM mystream MAXLEN ~ 1000
Plain

应用场景

消息队列

生产者使用 XADD 命令将消息插入到 Stream 中。该命令允许生产者为每条消息指定多个字段和字段值(即消息的内容)。

有一个生产者向 Redis Stream mystream 插入消息,消息内容为 “user_id”: 123, “action”: “login” 。

XADD mystream * user_id 123 action login
Plain
  • mystream:Stream 的名称。
  • *:表示 Redis 自动生成消息的 ID(时间戳-序列号)。
  • user_id 123 action login:消息的字段和值,即消息的内容。

在执行 XADD 时,Redis 会为每个消息生成一个 唯一的 ID,作为消息的标识。

比如插入成功之后,可能会返回这样的一个 ID 。

"1624410901000-0"
Plain

消息 ID 是由 Redis 自动生成的,格式为 时间戳-序列号。拿上面这个 ID 来说,其中 1624410901000 是时间戳,0 是序列号。时间戳是当前时间(毫秒级),序列号的含义就是从几开始编号的。

消费者通过使用 XREAD 命令从一个或多个 Stream 中读取消息。

假设有一个名为 mystream 的 Stream,消费者可以使用以下命令来从该 Stream 读取消息:

XREAD STREAMS mystream 0
Plain

这将从 mystream Stream 中从 最早的消息(ID 为 0)开始读取所有消息。

XREAD 返回的数据格式是这样的:

1) 1) "mystream"
   2) 1) "1624410901000-0"
      2) 1) "user_id"
         2) "123"
      3) 1) "action"
         2) "login"
Plain

XREAD 可以和 BLOCK 进行配合实现消息的阻塞式读取。

阻塞读取的基本逻辑是这样的:

  1. 如果消费者有新消息可以读取,XREAD 会立刻返回这些消息。
  2. 如果当前没有新消息,XREAD 会根据 BLOCK 参数的设置阻塞等待:
    • 如果 BLOCK 设置为 大于 0 的毫秒数,消费者会等待最多指定的时间(例如 5000 毫秒即 5 秒)来等待新消息。
    • 如果 BLOCK 设置为 0,则表示永久阻塞,直到有消息。
  3. 如果超时且没有新消息,XREAD 会返回空结果。
  4. 如果有新消息,XREAD 会返回从指定 ID 开始的消息。

比如你有一个名为 mystream 的 Stream,消费者希望从中读取消息,并希望在没有新消息时阻塞 5 秒钟。如果 5 秒内有新消息,返回新消息;如果没有新消息,则返回空。就可以使用这个命令:

XREAD BLOCK 5000 STREAMS mystream 0
Plain

0 代表从消息队列中的第一个消息开始读取。

基于 XADD 和 XREAD 这两个命令,我们就实现了一个简易的消息队列:

其实,上述这些功能,用 List 也能实现。但是,Stream 中还有一些特有的功能。

Stream 可以通过 XGROUP CREATE 命令来为某个 Stream 创建一个消费者组,并指定一个消费起始点(通常是流中的第一个消息)。

比如有一个名为 mystream 的 Stream,我想为它创建一个名为 mygroup 的消费组。

XGROUP CREATE mystream mygroup 0
Plain

这样就创建了一个消费组 mygroup ,并指定从 mystream 中的第一个消息开始消费。

如果想从 Stream 中最新的未消费消息开始消费,可以这样写:

XGROUP CREATE mystream mygroup $
Plain

消费者读取消息组则可以使用 XREADGROUP 命令。消费组的机制 确保每个消息只被一个消费者消费。

XREADGROUP GROUP group_name consumer_name STREAMS stream_name [ID id1] [COUNT count] [BLOCK milliseconds]
Plain
  • group_name:消费组的名称。
  • consumer_name:消费者的标识。
  • stream_name:指定要读取的 Stream 的名称。
  • ID id1:指定从哪个消息 ID 开始读取。如果第一次读取,可以指定从最后未消费的消息开始读取(用 >)。
  • COUNT count:可选参数,指定每次读取的消息数量。
  • BLOCK milliseconds:可选参数,指定阻塞时间(毫秒)。如果没有新消息时,消费者将阻塞等待。

消费组的一个核心目的就是实现 负载均衡。当一个消息队列中有大量消息需要消费时,单个消费者可能会成为瓶颈,处理消息的速度会受限于该消费者的处理能力。而消费组的出现可以让多个消费者共同消费消息,实现了消息的 并行消费,从而提高了系统的吞吐量。

现在,我们来看 Stream 中的一个很重要的概念,叫做 PENDING List ,也就是内部队列。 它包含了被消费者读取但未进行确认的消息。每当消费者读取消息时,Redis 会将消息记录在 PENDING List 中。当消费者处理完消息并发送确认( XACK )时,消息就会从 PENDING List 中被移除。

比如,消费者 consumer1 从 Stream mystream 中读取消息:

XREADGROUP GROUP mygroup consumer1 STREAMS mystream >
Plain

Redis 会将这些消息分配给 consumer1,并将它们添加到 PENDING List 中,表示这些消息被 consumer1 读取了,但尚未确认。

把消息添加到 PENDING List 时,会包含消息 ID ,消费者名称以及未确认的消息数量。

1)  1) "1624410901000-0"
    2) "consumer1"
    3) (integer) 1
Plain

当消费者 consumer1 处理完该消息并调用 XACK 命令进行确认时,Redis 会将该消息从 PENDING List 中移除。

XACK mystream mygroup 1624410901000-0
Plain

确认之后,PENDING List 中就不再包含该消息。

既然用 redis 中的 stream 可以实现消息队列,用专业的中间件比如 RabbitMQ 也可以实现消息队列,那么这两种不同方式实现的消息队列有什么不同呢?

我们要知道,如果想实现一个合格的消息队列,必须要做到消息不会丢失并且消息是可以堆积的。

而且,一个消息队列的组成无非就是三部分:生产者、中间件和消费者。

如果想保证消息不会丢失,那就只需要保证这三个环节都不会丢失消息不就行了嘛。

我们按照顺序来看一看这三个环节在 Stream 中到底会不会丢消息。

  • 从生产者来说,如果生产者向中间件发送消息的时候能正常收到 ack 响应,那么就说明这条消息没有丢失。当然,在实际的情况中,可能会出现一些异常情况,但是可以让生产者进行重复发送。也就是,只要没收到 ack 那就重新发送,这样的话,就可以保证生产者不会丢消息。
  • 从中间件来说, Stream 中的消息默认是保存在内存中的,因此如果 Redis 崩溃了,内存中的数据就会丢失,就会导致消息丢失。那可能有小伙伴会说,redis 不是有持久化吗?是,redis 确实有持久化,但是持久化的过程是异步的呀,当然会丢数据的。并且在主从复制中,如果主从节点需要切换,也是有可能丢数据的。
  • 从消费者来说,我们刚刚已经提到了 PENDING List。消费者读取消息后需要通过 XACK 命令确认消息。只有确认了的消息才会从 PENDING List 中移除,确保消息不会被重复消费。如果消息没有被确认,Redis 会将这些未确认的消息保留在 PENDING List 中,等待重新分配给其他消费者。

通过上面的分析,我们能够知道,redis 是无法保证中间件不会丢数据的。

那 redis Stream 中的消息可以堆积吗?

如果 消费者 从 Redis Stream 中读取消息的速度低于 生产者 写入消息的速度,消息会在 Stream 中 堆积,即消息数量会持续增加。redis 是基于内存的,如果消息持续堆积可能导致 Redis 占用过多内存,甚至可能导致内存溢出。

基于以上这个问题,Redis Stream 允许使用 MAXLEN 配置来控制 Stream 中消息的最大数量。如果消息数量超出了设定的最大值,Redis 会丢弃最旧的消息,从而避免消息堆积。但是这样就会导致消息丢失。

总之,redis 可能会丢消息,并且在消息堆积的时候,会导致内存紧张。

如果业务场景比较简单,允许部分消息丢失并且消息堆积的可能性很小,是可以使用 redis stream 来作为消息队列的。但是如果业务场景比较复杂,不允许消息丢失,那还是老老实实用专业的消息队列中间件。

总结

这篇文章我们讲了 redis 中的九大数据类型,其中有五种比较常见的,分别是 String、List、Hash、Set、Zset。

  • String 主要用于 缓存对象、常规计数、分布式锁和共享 session 信息。
  • List 主要用于消息队列。
  • Hash 主要用于缓存对象和购物车功能。
  • Set 主要用于去重、标签系统、集合运算、社交网络。
  • Zset 主要用于游戏排行榜、社交平台的热度排行、姓名排序。

这五种数据类型是比较基础的,在面试的时候被问到的频率也是最高的。

随着 redis 版本的更新,又出现了四种新的数据类型,分别是 BitMap、HyperLogLog、GEO、Stream。

  • BitMap 主要用于签到统计、判断用户登录态。
  • HyperLogLog 主要用于 UV 计数。
  • GEO 主要用于 商店定位、外卖配送、打车服务、地理广告定位、旅游系统。
  • Stream 主要用于实现消息队列,适用于简单的业务场景。

好了,这篇文章就讲到这里,感谢观看。

发表评论

后才能评论