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

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 列表既是队列,也能当栈

Redis 列表是有顺序的字节值序列,可以从两端插入和取出。选择哪一端写、哪一端读,决定了它表现为队列还是栈。阻塞操作只是等待数据到达,并不自动提供分布式锁、消息确认或完整的任务处理协议。这些能力必须根据业务需要另外明确,不能从“支持阻塞”直接推导出来。 先把两端的约定说清楚 RPUSH 配合 LPOP 按先进先出顺序移除元素,早进入的元素先离开;RPUSH 配合 RPOP 则是后进先出。一条 LPUSH 带多个值时,会依照参数顺序逐个往左插入,因此传入 a、b、c 后,从左到右看到的是 c、b、a。设计队列时要验证生产者真正执行的命令,而不是只看图里画了哪一端。 列表允许重复值,相同消息插入两次就占两个位置,不会自动完成去重。当两次投递必须能够区分时,应使用不同的任务标识。LRANGE 的起止位置都包含在返回范围里,-1 表示最后一个元素。最后一个元素被移除时,整个列表键也会消失;对不存在的列表弹出会返回空结果,而不是创建一个空列表占位。 两端操作的单元素成本较低,但返回很大的范围或一次删除许多元素,仍然需要相应的工作量。LTRIM 可以保留最近一段窗口,不过用它裁剪待处理任务,就等于主动丢弃被裁掉的条目。生产者写入和随后的单独裁剪是两条操作,不能当成原子提交。保留哪一段应由数据用途决定,不能把日志窗口策略直接套到任务队列上。 等待的是连接,不是整个服务器 BLPOP 在数据到来或等待超时前阻塞当前连接,其他连接仍可正常执行,生产者也能照常投递。在 redis-py 中,成功结果包含键名和取出的值,超时返回 None;超时参数为零则表示无限等待。客户端的套接字超时需要容纳预期的服务器等待时间及响应时间,否则可能先在客户端超时。 消费者没有业务工作时,阻塞调用仍会占用连接。因此应给消费者设置有界的连接预算,并避免让生产者与正在等待的消费者共用同一条连接。还要区分移除顺序与完成顺序:FIFO 保证先移走较早的元素,多个工作者却可能先完成后来的任务。仅凭阻塞等待,无法保证某个业务资源只有一个持有者。 坏掉的样子 LPOP 和 BLPOP 都在业务处理完成之前把任务从列表里移除。消费者刚取到任务就崩溃,或者服务器弹出后响应丢失,该任务都已经不在待处理列表中。Redis 不会自动把它恢复回来,也不要求消费者确认完成。单独配置持久化,并不能修补这类应用层交接窗口。 Redis 6.2 开始提供 LMOVE,可以在一条原子命令中把任务从待处理列表转移到处理中列表;BLMOVE 是对应的阻塞版本。成功处理后,可通过 LREM 按唯一任务标识移除一个条目,作为应用约定的确认。对于长时间没有完成的任务,应用仍需定义恢复策略,因为列表不会自动为每个元素维护租约、时间戳或重试次数。 如果业务副作用已经完成,但确认操作失败,恢复时就可能重复执行。因此唯一任务标识、幂等处理和迟到工作者的处置策略都很重要,两个列表之间的移动不等于恰好执行一次。下面只把一个已知任务明确放回待处理列表,用来验证机制,不把手动操作冒充自动故障检测或生产级恢复调度器。 ...

四月 18, 2027 · 3 分钟 · XB Field Notes