ZADD 是 Redis 有序集合类型中最核心的写入命令,它负责将成员及其分数写入一个由跳表和哈希表共同支撑的数据结构。理解这个命令不能只停留在语法层面,因为同一个命令在成员不存在、成员已存在、有条件更新、批量写入等不同情况下,底层行为差异很大。本文将围绕命令参数、分数更新规则以及典型应用场景展开,帮助读者准确掌握 ZADD 的使用方式。

一、命令语法与参数拆解
ZADD 的基本语法如下,选项部分可以按需组合,但需要注意某些选项之间存在互斥关系。score 参数表示成员对应的分值,member 参数表示成员名称,一次可以同时写入多组 score 和 member。
ZADD key [NX|XX] [GT|LT] [CH] [INCR] score member [score member ...]
最基础的用法是向一个有序集合中添加多个成员,Redis 会按照 score 从低到高自动排序。例如下面这条命令一次性添加三名玩家到排行榜中,返回值为 3,表示新增了三个成员。
ZADD leaderboard 100 player1 95 player2 88 player3
如果再次执行 ZADD leaderboard 105 player2,由于 player2 已经存在,默认行为是更新它的分数而不是新增成员,因此返回值会变成 0。这个返回值只统计真正新增的成员数量,不包括发生分数更新的成员。想要同时统计更新数量,就需要使用 CH 选项。
二、存在性选项与分数条件选项
存在性选项包括 NX 和 XX。NX 表示只在成员不存在时才执行插入,如果成员已经存在则忽略本次操作;XX 则表示只在成员已经存在时才执行更新,如果成员不存在则不做任何事。下面两条命令分别展示了 NX 和 XX 的行为。
ZADD leaderboard NX 100 player1 ZADD leaderboard XX 120 player1
第一条命令在 player1 不存在时会将分数设为 100,如果 player1 已经存在则不会修改分数。第二条命令则要求 player1 必须存在,存在时才会把分数更新为 120。这两个选项适合处理幂等写入和只更新不新增的场景。
GT 和 LT 是 Redis 较新版本引入的条件选项,用于控制分数更新的方向。GT 表示仅当新分数大于当前分数时更新,LT 表示仅当新分数小于当前分数时更新。它们可以与 NX 或 XX 搭配使用,也可以单独使用。
ZADD leaderboard GT 110 player1 ZADD leaderboard LT 80 player1
第一条命令只有在 110 大于 player1 当前分数时才会更新,第二条命令只有在 80 小于 player1 当前分数时才会更新。这种比较规则在排行榜的最大值或最小值维护中非常有用,可以避免低分数据覆盖高分数据。
三、增量更新与返回值变化
ZADD 还支持 INCR 选项,它会把给定的分数作为增量加到成员当前分数上,而不是直接替换。使用时只需要传一个成员和一个增量值,命令执行成功后会返回成员更新后的最新分数,返回类型表现为字符串形式的数值。如果成员不存在,Redis 会先将它的分数视为 0,然后再加上增量值。
ZADD leaderboard INCR 5 player1
假设 player1 当前分数为 100,上述命令执行后分数变为 105,返回值为 105。INCR 选项不能与 NX、XX、GT、LT 同时使用,也不适合一次处理多个成员,因为它要求每次调用只能指定一个成员和一个增量值。如果需要频繁增加分数,ZINCRBY 命令在语义上更直观,但 ZADD INCR 能够把创建和增量合并到一条命令中完成。
CH 选项可以改变返回值的统计口径。默认情况下 ZADD 返回新增成员数量,当加上 CH 后,返回值会统计新增成员数量和发生分数更新的成员数量之和。例如先执行一次 ZADD leaderboard 100 player1 95 player2,再执行 ZADD leaderboard CH 110 player1 88 player2,第二次返回值为 2,因为两个成员的分数都发生了变化。
四、底层数据结构与写入复杂度
有序集合在 Redis 中通常由跳表和哈希表共同实现。哈希表负责根据成员名称快速定位到成员对应的分数,时间复杂度为 O(1),跳表则负责维护按分数排序的序列,插入、删除、范围查询的时间复杂度都是 O(log N)。ZADD 写入一个成员时,Redis 会先在哈希表中判断成员是否存在,然后再决定是插入新节点还是修改已有节点的分数。
插入新成员的流程大致如下:Redis 根据成员名计算哈希并写入哈希表,同时在跳表中从最高层开始比较 score 和 member,找到合适的插入位置,然后随机生成新节点的层数并完成指针连接。如果新节点层数较高,还需要更新跳表上层的前向指针。由于跳表每一层相当于一个有序链表,层数越高跨度越大,因此查找和插入的平均复杂度可以保持在 O(log N)。
更新已有成员的分数比新增更复杂一些。因为分数变化会改变成员在跳表中的排序位置,所以 Redis 不能直接修改原节点的 score,而是需要先从跳表中删除旧节点,再以新分数重新插入。整个过程在哈希表中只做一次分值覆盖,而在跳表中则表现为删除加插入两个步骤。这个操作必须在命令执行期间保持一致性,否则可能出现哈希表已更新但跳表未同步的情况。
五、典型应用场景与性能注意事项
排行榜是 ZADD 最直接的应用场景。每次用户得分变化时,可以使用 ZADD 写入或更新分数,再通过 ZREVRANGE 或 ZRANGE 获取排名靠前的成员。对于只希望记录最高分的排行榜,可以用 GT 选项防止旧的低分覆盖高分。对于只希望维护最低延迟的任务队列,可以用 LT 选项保证分数始终向更小值更新。
ZADD ranking 1000 user:1001 850 user:1002 720 user:1003 ZREVRANGE ranking 0 2 WITHSCORES
延迟队列也是常见用法。可以把任务的触发时间戳作为 score,把任务 ID 作为 member 写入有序集合,消费端按时间范围取出已到期的任务并执行。由于 ZADD 支持批量写入,可以一次加入多个任务,减少网络往返次数。
ZADD delay_queue 1710000000 task:order:123 1710000100 task:order:124 ZRANGEBYSCORE delay_queue 0 1710000050 LIMIT 0 10
在批量写入大量成员时,ZADD 的单次命令时间会随着成员数量线性增长,但相比多次单独调用仍然能显著降低网络开销。如果有序集合规模达到百万级以上,需要关注删除旧节点带来的跳表指针调整成本,并尽量将写入操作分散到低峰期执行。另外,由于 ZADD 在更新分数时需要删除并重新插入节点,频繁修改同一批成员的分数可能会带来额外的 CPU 消耗,这一点在延迟队列场景中尤其需要注意。
Redis ZADD有序集合成员分数修改时间:2026-08-30 12:08:17