jonnifyredis

R4.05.3 · dive 3

The score-update path

A score changes in one of two ways: ZADD overwrites it, ZINCRBY accumulates onto it. Neither touches a rank — no rank is stored, so the rank is recomputed from the order on the next read.

Updating a leaderboard means updating a score. There are two updates. ZADD sets a score to a new absolute value, replacing whatever was there. ZINCRBY adds a delta to the current score, accumulating. Either way, the member is re-placed in the order by its new score — and that is the entire update. No rank is written, because no rank is stored; the rank is derived from the order whenever it is next read. Set an update on the right and watch the order, and the ranks, recompute.

Two updates · ZADD overwrites, ZINCRBY accumulates

1800
ZADD leaderboard "player:alice" — rank · player · score

ZADD — overwrite to an absolute value

ZADD sets a member's score to the value given, whether or not the member is already on the board:

ZADD leaderboard 2000 "player:alice"    # alice's score is now 2000, replacing any prior score

This is the update to use when the new score is known outright — a final result, a recomputed total, a snapshot value. The member is re-placed in the order by 2000; if alice was at rank 4 and 2000 lifts her above three players, she moves to rank 1 on the next read. ZADD carries the whole new state of the score in one write.

ZADD overwrites a member's score to an absolute value and re-places it in the order; reach for it when the new score is known outright.

ZINCRBY — accumulate a delta

ZINCRBY adds a delta to the current score and returns the new score:

ZINCRBY leaderboard 50 "player:alice"   # alice's score rises by 50; returns the new total

This is the update to use when points arrive incrementally — fifty more for a win, ten for a step completed. Because the increment happens inside Redis, two concurrent ZINCRBY calls on the same member both apply: the score rises by the sum of the deltas, with no lost update. A plain read-modify-write in the application would lose one of two concurrent increments; ZINCRBY does not. A member not yet on the board is treated as starting from 0, so the first ZINCRBY of 50 sets the score to 50.

ZINCRBY adds a delta in place and returns the new total; the increment runs inside Redis, so two concurrent calls both apply with no lost update.

The rank is recomputed on read, never stored

Neither update writes a rank. The rank of any member is its position in the score order, and the set computes that position when ZREVRANK or a range read asks for it — in O(log N), by walking the order. Two consequences follow. First, the rank is always exactly the current order: there is no stored rank that an update could leave stale, so a score change is reflected the instant the rank is next read. Second, updating one member's score can shift the ranks of many others — lift one player past three and those three each drop a place — but none of those neighbours is rewritten. Their scores are unchanged; only their computed position differs on the next read. The work of a score update is O(log N) for the re-placement; the ripple through everyone else's rank costs nothing at write time, because it is computed at read time.

One write, many ranks · before and after one ZADD

before — rank · player · score

A score update writes one member in O(log N); the ranks of every neighbour it passed shift on the next read, none of them rewritten, because the rank is computed from the order, not stored.

In Portal — updating the progress percent a ranking would read

The pattern is a score updated two ways, the rank recomputed on read. Portal runs no sorted set. It records a learner's lesson progress as a percent — Portal.Enrollment.Progress, the field percent :: 0..100, namespace PRG, an in-memory store row. There is no progress ranking; the percent is a stored value that the learner's activity moves over time.

# Portal.Enrollment.Progress — the real struct (echo/apps/portal): percent 0..100, namespace PRG
%Portal.Enrollment.Progress{enrollment_id: "ENR…", lesson_id: "LSN…", percent: 85}

# the score update a ranked view would write as the percent advances (the applied pattern — Portal runs no sorted set)
ZADD board 85 "learner:dana"      # set the score to the new percent
ZREVRANK board "learner:dana"      # the rank, read from the percent order — not stored

A ranking view of learners by progress is this dive applied to that data. As a learner's percent changes, a ranking board would update its score: ZADD board <new-percent> <learner> to set the percent outright, the way a recomputed progress percent is known as an absolute value. The learner is re-placed by their new percent, and every other learner's rank recomputes on the next read — no neighbour's percent is touched. State it plainly: Portal updates the percent in its store today; were a ranked view added, the score update is the same ZADD, and the rank is read from the percent order, never stored.

the pattern

A score is updated by ZADD (overwrite) or ZINCRBY (accumulate), and the rank is recomputed from the order on read, never stored.

in Portal

Portal stores each learner's progress as a percent (the PRG struct) and updates it as the learner advances. A ranking view sets the score with ZADD board percent learner on each change and reads the rank from the percent order — Portal runs no sorted set; the update is the structure to add over the percent it records.

Update the score, not the rank — ZADD to overwrite, ZINCRBY to accumulate. The rank is read from the order, so it is never stale. Portal's progress percent is the score such an update would write.

Door · not depth How the queue updates a job's standing within its group, and how the group re-orders on a change, is the subject of the dedicated EchoMQ course. The intra-group ranking is E4 · Groups. This dive teaches the score update and the rank-on-read; that course teaches the group re-ordering built on it.

Where this is heading — EchoMQ 2.0

The score-update path is generic sorted-set work — ZADD to overwrite, ZINCRBY to accumulate, the rank read from the order. It is not part of the EchoMQ queue protocol. Where EchoMQ does order by a sorted-set score is its priority queue (R4.03's emq:{queue}:prioritized), and the settled EchoMQ 2.0 design — the protocol break on the first EMQ rung, emq.1 — renames that keyspace to emq:{queue}:prioritized: the emq: prefix replacing emq:, every Lua key declared in KEYS[], meta.version bumped to echomq:2.0.0 — with the score order unchanged. A score update and the rank it recomputes are a property of the order; they are independent of the prefix or the version.

EchoMQ 2.0 renames the priority keyspace from emq:{queue}:prioritized to emq:{queue}:prioritized and declares its keys; a score update and the rank it recomputes are a property of the order, unchanged by the prefix or the version.

References

The two writes that change a score — the overwrite and the in-place accumulate — the read that derives the rank from the order, and the sorted set an update re-places a member in.

Sources

  • Redis — ZADD — set a member's score to an absolute value, overwriting any prior score.
  • Redis — ZINCRBY — add a delta to a member's score in place, with no lost update under concurrency.
  • Redis — ZREVRANK — read the rank from the order, recomputed on each read.
  • Redis — Sorted sets — the ordered structure an update re-places a member in.

Related in this course