Redis Stream 是 Redis 5.0 引入的日志型数据结构,除了作为轻量级消息队列使用,它还支持按消息 ID 进行范围读取。XREVRANGE 正是在这一能力上提供的反向查询命令,用来从最新消息向历史消息方向翻页。它和 XRANGE 共用同一套 Stream 底层存储,但扫描方向相反,因而参数位置、边界语义和典型使用场景都有差异。

XREVRANGE 命令的语法与参数拆解
XREVRANGE 的基本语法为 XREVRANGE key end start [COUNT count]。与 XRANGE key start end 相比,最大的不同在于 start 和 end 的位置发生了交换。XRANGE 按消息 ID 从小到大读取,因此参数顺序是从起始 ID 到结束 ID;而 XREVRANGE 按消息 ID 从大到小读取,所以第一个范围参数应当是较大的 ID,也就是 end,第二个才是较小的 start。这个设计并不是随意为之,它保证了调用者在书写范围时始终遵循命令的扫描方向。
在 XREVRANGE 中,+ 表示 Stream 中可能存在的最大 ID,- 表示最小 ID。例如执行 XREVRANGE mystream + -,就是要求从最新一条消息开始,一直扫描到最早一条消息。实际返回结果会严格按照消息 ID 倒序排列,最新消息排在最前,最早消息排在最后。如果只想看最新两条,可以写成 XREVRANGE mystream + - COUNT 2,COUNT 会限制返回的消息条数。
# 添加几条测试消息 XADD mystream * type login user 1001 XADD mystream * type view user 1002 XADD mystream * type logout user 1001 # 从最新到最早返回两条消息 XREVRANGE mystream + - COUNT 2
除了 + 和 - 这两个特殊边界之外,XREVRANGE 也支持精确 ID 和半开区间。Redis Stream 的消息 ID 格式为 毫秒时间戳-序列号,例如 1700000000000-0。如果希望排除某个精确 ID,可以在 ID 前加上左圆括号,例如 (1700000000000-0,表示不包含这个 ID。这种写法在倒序分页时非常关键,因为翻页游标通常需要排除上一页已经读到的最后一条消息。
XREVRANGE 与 XRANGE 的核心差异与适用场景
理解 XREVRANGE 和 XRANGE 的差异,不能只停留在“结果顺序相反”这一层。两者虽然共享同一份 Stream 数据,但扫描逻辑和参数语义都不同。XRANGE 适合从旧到新读取,例如需要顺序回放完整事件流;XREVRANGE 则适合从新到旧回溯,例如先展示最新聊天记录、最近登录日志或最新的系统告警。在实际业务中,用户几乎总是先看最新内容,再根据滚动操作向前翻页,因此 XREVRANGE 的使用频率通常比 XRANGE 更高。
| 对比项 | XRANGE | XREVRANGE |
|---|---|---|
| 读取方向 | 旧消息到新消息 | 新消息到旧消息 |
| 参数顺序 | key start end | key end start |
| 典型边界 | - + | + - |
| 结果排序 | 按 ID 递增 | 按 ID 递减 |
| 典型用途 | 顺序回放、全量遍历 | 最新消息、倒序分页 |
从底层来看,Redis Stream 使用 listpack 或 radix tree 组织消息节点,每个节点内维护了按 ID 排序的消息列表。XREVRANGE 在扫描时会反向遍历这些节点,再在节点内部反向读取消息,整个过程不需要额外排序。它的时间复杂度与扫描范围相关,通常可以理解为 O(N),其中 N 是实际返回或扫描的消息数量。正因为如此,加上 COUNT 限制对控制单次查询耗时非常重要。
适用场景方面,XREVRANGE 非常适合消息队列中的“查看最近 N 条记录”功能。例如客服系统需要展示用户最近的咨询记录,运维平台需要展示主机最近产生的告警,社交应用需要展示群聊最新消息。这些场景的共同点是数据只增不减,而且用户对最新数据的关心程度远高于历史数据。
实战:用 XREVRANGE 构建最新消息查询与游标分页
假设有一个订单事件流,每次订单状态变更都会通过 XADD 写入 Stream。为了展示最近三条事件,可以直接执行 XREVRANGE orders + - COUNT 3。返回结果中每条消息包含 ID 和字段值,开发人员可以按照数组顺序渲染页面,第一项就是最新事件,无需再在应用层做反转。
如果数据量较大,一次性把最新消息全部取出并不现实,此时需要基于消息 ID 做游标分页。倒序分页的思路是:先获取最新一页,记录本页最后一条消息的 ID;然后下一页查询时,把这个 ID 作为 end,并在前面加上左圆括号,表示排除该 ID 本身。例如第一次查询返回的最后一条消息 ID 为 1700000000123-0,第二次查询就可以写成 XREVRANGE orders (1700000000123-0 - COUNT 3。这样既不会重复读取边界消息,也不会漏掉更早的消息。
# 第一页:获取最新三条 XREVRANGE orders + - COUNT 3 # 假设返回的最后一条 ID 为 1700000000123-0 # 第二页:排除该 ID,继续向更早方向查询 XREVRANGE orders (1700000000123-0 - COUNT 3
如果既要按时间范围过滤,又要反向读取,可以直接用时间戳构造消息 ID。例如要查询某个时间段内的日志,可以指定 end 为该时间段的结束时间戳加 -0,start 为开始时间戳加 -0。命令形如 XREVRANGE logs 1700000005000-0 1700000000000-0。这种方式适合排障场景,先根据错误发生时间框定范围,再从最后一条日志向前回溯上下文。
在编写客户端代码时,需要注意 Redis 客户端对 XREVRANGE 的参数封装。以 Python 的 redis-py 为例,命令名对应为 xrevrange,参数顺序与 Redis 命令一致,先传 name,再传 end 和 start,最后可以跟 count。不要把 start 和 end 的习惯顺序直接套用过来,否则在客户端层面就会返回空列表。
import redis
client = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)
# 获取最新 3 条消息
messages = client.xrevrange("orders", max="+", min="-", count=3)
for message_id, fields in messages:
print(message_id, fields)
常见误区与性能排查
使用 XREVRANGE 最容易踩的坑就是把参数顺序写成 XREVRANGE key - +。因为大家已经习惯了 XRANGE 的参数顺序,会下意识地把小 ID 写在前面。实际执行这种命令时,Redis 会从最小 ID 扫描到最大 ID,然后试图反向返回,结果通常为空或不符合预期。因此,如果发现 XREVRANGE 查不到数据,第一件事就是检查 end 和 start 是否写反。
另一个常见问题是对 COUNT 的理解。COUNT 并不是指定扫描范围,而是限制最终返回的消息数量。如果流中有大量消息,且没有设置 COUNT,XREVRANGE 可能会一次性返回全部消息,造成客户端内存压力和网络阻塞。在生产环境中,任何面向用户的查询都应该加上合理的 COUNT 值。需要注意的是,COUNT 0 不会返回消息,因此如果要查询全部数据,要么省略 COUNT,要么显式设置足够大的值,但不建议在数据量未知时这样做。
当 XREVRANGE 查询变慢时,可以先通过 XLEN 查看 Stream 总长度,通过 TYPE key 确认键类型是否为 stream。如果确认键类型正确但查询仍然很慢,通常是因为扫描范围过大。此时需要缩小时间范围,或者使用消费者组与 XREADGROUP 进行持续消费,而不是反复用范围查询全量扫描。对于已经确认不再需要的历史消息,可以调用 XTRIM 控制 Stream 长度,从源头降低反向扫描的成本。
# 查看键类型 TYPE mystream # 查看当前 Stream 长度 XLEN mystream # 保留最近 10000 条消息 XTRIM mystream MAXLEN 10000
还需要留意的是,XREVRANGE 返回的是消息 ID 递减的数据,而消息 ID 中既包含毫秒时间戳,又包含同一毫秒内的序号。当业务使用 XADD key * 自动生成 ID 时,极少数情况下同一毫秒内会写入多条消息,此时序号部分会递增。反向查询时,同一毫秒内的消息会按照序号从大到小返回,这与写入顺序相反。如果业务对同一毫秒内的顺序有严格依赖,应该使用显式 ID 或者在应用层进行二次排序。
总体来看,XREVRANGE 是 Redis Stream 中非常实用的反向范围查询命令。掌握它的参数顺序、边界规则和分页方式,可以高效地实现最新消息列表、倒序日志查看等常见功能。配合 COUNT 和左圆括号排除语法,它能够在数据量持续增长的流式场景中保持稳定、可控的查询表现。
Redis XREVRANGEStream消息队列反向范围查询修改时间:2026-08-29 04:19:43