两种排序,一是插入序、一是按分数排序。

Redis 列表与有序集合,一字之差用途不同

Redis 列表保存允许重复值的有序序列;有序集合保存唯一成员,并按数值分数排序。真正有用的区别不是谁总是更快或更省内存,而是应用需要按位置处理序列、维持唯一成员、查询分数范围,还是需要另一种数据模型才能表达的组合语义。 先定义什么算不同条目 使用 RPUSH 时,列表按追加顺序保留值,也保留重复项。随后 LPOP 移除最旧值,RPOP 则移除最新值。换一种推入与弹出端的组合,顺序也会变化,因此不能不看生产者命令就断言列表天然是先进先出。多个工作进程并行执行时,移除顺序也不保证业务完成顺序。 ZADD 为每个成员关联一个分数。再次加入同名成员,是更新分数,不是增加第二条记录。默认返回值统计新增成员数量,所以返回零也可能是成功更新。如果直接拿重复的事件载荷当成员名,多次出现会合并成同一个成员;当每次出现都代表独立任务时,这样建模就会丢掉业务区别。 分数允许并列,同分时按成员字节排序,不按到达顺序,也不是本地化文字排序;REV 同时反转同分成员的先后。排行榜可能接受这个规则,时间索引则可能需要独立事件标识和明确的并列处理方式。分数使用浮点数,因此还要选择能够保留所需数值精度的单位,不能把排序正确等同于任意精度都能表达。 位置和分数选择的是不同内容 列表不只支持两端访问。LINDEX 可以读取一个位置,LRANGE 返回包含边界的位置区间。到达内部位置需要遍历;两端访问便宜,不代表返回大量元素也无需付出相应成本。对只读取头尾、长度受控的短列表来说,保留一个小窗口完全可能是适合的方案。 ZRANGE 默认解释为从零开始的排名位置,结束位置也包含在内。BYSCORE 才把两个边界解释为数值分数,在分数前加左圆括号则排除该边界。官方给出的范围读取复杂度是 O(log N + M),其中 N 为成员数,M 为返回数量。因为有索引,就认为大结果集或很深的偏移扫描没有成本,是不成立的。 持续变化的两种结构都不会让按排名分页变成快照。插入或分数更新可能移动后续页的位置,分开的请求就可能重复或遗漏。选择容器之前应先决定读取契约。内存开销也没有普遍相近的保证:底层表示、成员数量、载荷长度和配置都会改变结果。容量是决策因素时,应测量具有代表性的数据,而不是根据类型名字推断。 坏掉的样子 时间戳分数不是成员的过期截止时间。ZREMRANGEBYSCORE 显式删除数值区间,LTRIM 显式保留按位置选择的窗口;两者都不会因为时间过去或有人读取而自动清理。追加与清理如果分成两个命令,就是两个操作,保留逻辑仍然需要自己的触发与并发策略。 有序集合也不是完整的延迟任务调度器。ZPOPMIN 会立即移除最低分成员,即使分数表达的是未来时刻。先查到期成员、再用另一条命令删除,可能让多个工作进程选到同一任务。原子认领、处理确认以及遗弃任务恢复都是额外要求,不会因为分数放了时间戳就自然具备。 列表弹出同样发生在业务处理完成之前。移除后消费者失败,若没有合适的处理协议,任务就可能丢失。需要序列操作时选列表,需要按分数索引唯一成员时选有序集合;不能从任何一种排序方式推导出送达保证。数据结构选择与业务处理协议应分别判断,避免用容器替代完整语义。 快速验证 需要 POSIX 环境、C 编译器、make、curl、tar,以及 Python 3.10 或更新版本。以下步骤已在 macOS、Python 3.14.4、Redis 8.10.1 和 redis-py 8.1.0 上执行。它们使用 libc 构建 Redis 核心,并把客户端安装到临时虚拟环境,不构建可选模块,也不做系统级安装。 ...

一月 4, 2028 · 3 分钟 · XB Field Notes
一个常驻有序、以分数决定先后次序的集合。

有序集合按分数维持成员次序

Redis 有序集合保存唯一的字节串成员,每个成员关联一个数值分数。分数决定排序,但分数本身可以重复。这个模型可以用于排行榜和时间索引,却不会把分数自动解释成过期时间。成员是否存在、处于哪个名次、什么时候清理,是需要分别定义边界的三件事,不能因为数据有序就假定这些动作已经一并完成。 成员不能重复,分数可以相同 ZADD 在同一条命令内插入新成员,或者更新已有成员的分数。重复使用同一个成员名不会产生第二条记录。默认返回值统计新增成员数,所以返回零也可能表示分数已经成功更新。加上 CH 后,返回值才统计新增成员以及确实改变了分数的更新;不能把原分数不变的重复写入算作一次实际变化。 NX 只允许新增成员,XX 只允许更新已有成员。GT 限制替换后的分数必须更高,但单独使用它仍然允许插入新成员。如果契约是“只提高已有成员的分数”,就需要同时指定 XX 和 GT,并检查执行结果,而不是把命令没有语法错误等同于排行榜发生了变化。是否允许新成员入榜和是否允许降分,是两项不同的条件。 ZRANGE 默认按分数从低到高排列,同分时再按成员的二进制字节字典序排序,不是按插入先后,也不是按本地语言的排序规则。REV 会同时反转分数顺序和同分成员顺序。名次从零开始,按名次查询的两端都包含在结果内,因此从零到九最多得到十个成员。ZRANK 和 ZREVRANK 分别报告对应方向上的位置。 名次和分数是两套坐标 按名次查询选择的是位置,ZRANGE BYSCORE 选择的是数值范围。分数边界默认包含自身,加左括号前缀后才排除该端点;按分数查询再使用 REV 时,需要先给上界、再给下界。无论采用哪一种坐标,查询都不会预留成员,也不会冻结后续分页。两次正常读取之间插入一个成员,就可能让后面的所有名次发生偏移。 分数使用双精度浮点数。从负的二的五十三次方到正的二的五十三次方,这个闭区间内的整数都能被精确表示;超过范围后,相邻整数就可能映射到同一个分数。因此,如果要求每一纳秒都可区分,就不能直接把原始纪元纳秒时间当分数。精确标识可以放在成员数据中,分数采用什么单位、允许多大范围,应由业务精度要求明确决定。 坏掉的样子 按分数查询只是读取。存入时间戳,再查询最近一段窗口,不会自动删除更老的成员,也不会创建成员 TTL。ZREMRANGEBYSCORE 才会显式删除指定分数区间,键级过期则会删除整个集合。应用必须安排清理触发点,并明确截止分数本身是保留还是删除;如果没有清理动作,旧成员完全可能持续积累。 有序访问也不意味着返回多少结果都耗费相同时间。ZRANGE 的文档复杂度为 O(log N + M),其中 M 是返回成员数;按分数范围删除的成本同样取决于实际删除数量。超大响应和集中清理依然占用执行时间,不能用“已经排好序”忽略它们。分页不是快照,单条 ZADD 的原子性也不会把另外几条读取、判断和写入自动合成一个事务。 快速验证 准备带有 C 编译器、make、curl、tar 和 Python 3.10 或更新版本的 POSIX 环境。本次在 macOS、Python 3.14.4、Redis 8.10.1 和 redis-py 8.1.0 上实测。下面只编译使用 libc 的 Redis 核心,并在临时虚拟环境中安装 Python 客户端,不执行系统级安装,也不构建可选模块。 ...

三月 21, 2027 · 3 分钟 · XB Field Notes