XADD:追加新元素到流的末尾

用户可以通过执行 XADD 命令,将一个带有指定 ID 以及包含指定键值对的元素追加到流的末尾:

XADD stream id field value [field value ...]

如果给定的流不存在,那么 Redis 会先创建一个空白的流,然后将给定的元素追加到流中。

流中的每个元素可以包含一个或任意多个键值对,并且同一个流中的不同元素可以包含不同数量的键值对,比如其中一个元素可以包含 3 个键值对,而另一个元素则可以包含 5 个键值对,诸如此类。需要注意的是,与散列以无序方式存储键值对的做法不同,流元素会以有序方式存储用户给定的键值对:用户在创建元素时以什么顺序给定键值对,它们在被取出的时候就是什么顺序。

流元素的ID

流元素的 ID 由毫秒时间(millisecond)和顺序编号(sequcen number)两部分组成,其中使用 UNIX 时间戳表示的毫秒时间用于标识与元素相关联的时间,而以 0 为起始值的顺序编号则用于区分同一时间内产生的多个不同元素。因为毫秒时间和顺序编号都使用 64 位的非负整数表示,所以整个流 ID 的总长为 128 位,而 Redis 在接受流 ID 输入以及展示流 ID 的时候都会使用连字符-分割这两个部分。

image 2025 01 05 11 18 28 201
Figure 1. 图10-2 流元素ID示例

图 10-2 展示了一个流元素 ID 示例。在这个示例中,元素 ID 的毫秒时间部分为 1100000000000,而顺序编号部分则为 12345。如果我们使用这个 ID 作为输入调用以下 XADD 命令,那么 Redis 将把包含键值对 k1 和 v1 的新元素追加到流 s1 的末尾:

redis> XADD s1 1100000000000-12345 k1 v1
1100000000000-12345

XADD 命令在成功执行时将返回新元素的 ID 作为结果。在这个例子中,被返回的 ID 就是我们刚刚输入的 ID。

不完整的流ID

用户在输入流 ID 的时候,除了可以给出带有毫秒时间和顺序编号的完整流 ID 之外,还可以给出只包含毫秒时间的不完整流 ID:在这种情况下,Redis 会自动将 ID 的顺序编号部分设置为 0。

举个例子,Redis 命令在执行以下两个 XADD 命令的时候,就会将用户给定的不完整 ID1000000000000 和 2000000000000 分别补全为 1000000000000-0 和 2000000000000-0,然后再执行相应的追加操作,以下命令返回的流元素 ID 证明了这一点。

redis> XADD temp-stream 1000000000000 k1 v1
1000000000000-0
redis> XADD temp-stream 2000000000000 k2 v2
2000000000000-0

流元素ID的限制

因为同一个流中的每个元素 ID 都用于指定特定的一个元素,所以这些 ID 必须是各不相同的,换句话说,同一个流中的不同元素是不允许使用相同 ID 的。

比如,如果我们尝试使用流 s1 中已有的 ID 1100000000000-12345 向流中添加新元素,那么 Redis 将返回一个错误:

redis> XADD s1 1100000000000-12345 k2 v2
(error) ERR The ID specified in XADD is equal or smaller than the target stream top item

除了不允许使用相同的 ID 之外,Redis 还要求新元素的 ID 必须比流中所有已有元素的 ID 都要大。具体来说,Redis 会记住每个流已有元素的最大 ID,并在用户尝试向流里面添加新元素的时候,使用新元素的 ID 与流目前最大的 ID 进行对比:

  • 如果新 ID 的毫秒时间部分比最大 ID 的毫秒时间部分要大,那么允许添加新元素。

  • 如果新 ID 的毫秒时间部分与最大 ID 的毫秒时间部分相同,那么对比两个 ID 的顺序编号部分,如果新 ID 的顺序编号部分比最大 ID 的顺序编号部分要大,那么允许添加新元素。

相反,不符合上述两种情况的添加操作将会被拒绝,并返回一个错误。

举个例子,如果我们在流 s1 已经拥有 ID 为 1100000000000-12345 的元素的情况下,尝试添加 ID 为 1000000000000-12345 的元素或者 ID 为 1100000000000-100 的元素,那么 Redis 将返回一个错误:

redis> XADD s1 1000000000000-12345 k2 v2 -- 毫秒时间部分太小
(error) ERR The ID specified in XADD is equal or smaller than the target stream
top item
redis> XADD s1 1100000000000-100 k2 v2 -- 毫秒时间相同,但顺序编号部分太小
(error) ERR The ID specified in XADD is equal or smaller than the target stream
top item

与此相反,如果我们向流中添加 ID 为 1200000000000-0 的新元素,那么 Redis 将正确执行命令,因为新给定的 ID 比之前的最大 ID 1100000000000-12345 要大:

redis> XADD s1 1200000000000-0 k2 v2
1200000000000-0

在成功执行 XADD 命令之后,流的最大元素 ID 也会随之更新。比如,在执行上述命令之后,流 s1 的最大元素 ID 就从原来的 1100000000000-12345 更新到了 1200000000000-0。

只执行追加操作的流

通过将元素 ID 与时间进行关联,并强制要求新元素的 ID 必须大于旧元素的 ID,Redis 从逻辑上将流变成了一种只执行追加操作(append only)的数据结构,这种特性对于使用流实现消息队列和事件系统的用户来说是非常重要的:用户可以确信,新的消息和事件只会出现在已有消息和事件之后,就像现实世界里新事件总是发生在已有事件之后一样,一切都是有序进行的。

此外,只能将新元素添加到末尾而不允许在数据结构的 “中间” 添加新元素,这也是流与列表以及有序集合之间的一个显著区别。

自动生成元素ID

正如 10.1.3 节所介绍的那样,Redis 流对元素 ID 的要求非常严格,并且还会拒绝不符合规则的 ID。为了方便用户执行添加操作,Redis 为 XADD 命令的 id 参数设定了一个特殊值 *:当用户将符号 * 用作 id 参数的值时,Redis 将自动为新添加的元素生成一个可用的新 ID。

具体来说,自动生成的新 ID 会将 Redis 所在宿主机器当前毫秒格式的 UNIX 时间戳用作 ID 的毫秒时间,并根据当前已有 ID 的最大顺序编号来设置新 ID 的顺序编号部分:

  • 如果在当前毫秒之内还没有出现过任何 ID,那么新 ID 的顺序编号将被设置为 0。比如,如果当前时间为 1530200000000ms,并且在这一毫秒之内没有任何现存的 ID,那么新元素的 ID 将被设置为 1530200000000-0。

  • 如果在当前毫秒内已经存在其他 ID,那么这些 ID 中顺序编号最大的那个加上 1 就是新 ID 的顺序编号。比如,如果在 1530200000000ms 内,现存最大的顺序编号为 10086,那么新 ID 的顺序编号将是 10086 加 1 的结果 10087,而完整的新 ID 则是 1530200000000-10087。

以下是一个使用特殊值 * 的 XADD 命令执行示例:

redis> XADD s1 * k1 v1
1530513983956-0

注意,此处命令返回的 1530513983956-0 就是 Redis 为新元素自动生成的新 ID。如前所述,如果我们在同一毫秒内同时执行多个 XADD 命令,那么新 ID 还会带有相同的毫秒时间以及不同的顺序编号:

redis> XADD s1 * k2 v2
1530514186159-0
redis> XADD s1 * k3 v3
1530514186159-1
redis> XADD s1 * k4 v4
1530514186159-2

防止时钟错误

如果用户使用了 * 作为 ID 参数的值,但是宿主机器的当前时间比流中已有最大 ID 的毫秒时间要小,那么 Redis 将使用该 ID 的毫秒时间来作为新 ID 的毫秒时间,以此来避免机器时间倒流产生错误。

限制流的长度

除了上面提到的各项参数之外,XADD 命令还提供了 MAXLEN 选项,让用户可以在添加新元素的同时删除旧元素,以此来限制流的长度:

XADD stream [MAXLEN len] id field value [field value ...]

在将新元素追加到流的末尾之后,XADD 命令就会按照 MAXLEN 选项指定的长度,按照先进先出规则移除超出长度限制的元素。

image 2025 01 05 11 54 50 799
Figure 2. 图10-3 执行XADD命令之前的mini-stream

举个例子,对于图10-3所示的流,如果我们执行以下命令:

redis> XADD mini-stream MAXLEN 3 * k4 v4
1400000000000-0

那么 XADD 命令将把包含键值对 k4 和 v4 的新元素添加到流 mini-stream 的末尾,并从流中移除一个旧元素,以此来保证流的长度不超过 MAXLEN 选项指定的长度 3。对于这个命令来说,ID 为 1100000000000-0 的元素将被移除,因为这个元素位于流的最开始位置,并且它也是存在于流中时间最长的元素,所以它会首先被移除。图10-4 展示了执行 XADD 命令之后的 mini-stream。

image 2025 01 05 11 56 12 258
Figure 3. 图10-4 执行XADD命令之后的mini-stream

其他信息

  • 复杂度:O(log(N)),其中 N 为流目前包含的元素数量。

  • 版本要求:XADD 命令从 Redis 5.0.0 版本开始可用。