Start HereOrdered learning track

Lists, Sets, and Sorted Sets: Queues, Membership, Ranking, Scheduling

Learn Java Redis In Action - Part 006

Production use of Redis Lists, Sets, and Sorted Sets from Java systems for queues, membership, deduplication, ranking, scheduling, sliding windows, and bounded collection modeling.

15 min read2926 words
PrevNext
Lesson 0634 lesson track0106 Start Here
#java#redis#lists#sets+4 more

Part 006 — Lists, Sets, and Sorted Sets: Queues, Membership, Ranking, Scheduling

Part 005 covered Redis Hashes as bounded object-like storage. This part covers Redis collection structures that model ordering, uniqueness, and score-based ordering.

The three structures are:

List       -> ordered sequence, duplicate values allowed
Set        -> unordered unique membership
Sorted Set -> unique membership ordered by numeric score

These three structures power many production patterns:

job queues
deduplication
presence
feature membership
leaderboards
sliding windows
delayed jobs
expiry indexes
scheduler indexes
ranking tables

The goal is not to memorize commands. The goal is to choose the right collection shape, keep it bounded, make failure behavior explicit, and implement safe Java access patterns.


1. Kaufman Skill Decomposition

The target skill:

Given a collection-like requirement in a Java service, choose List, Set, Sorted Set, Stream, Hash, or database intentionally, then design its write/read/cleanup/failure behavior.

Sub-skills:

Sub-skillWhat you must be able to do
Structure selectionChoose List vs Set vs Sorted Set based on semantics
Queue modelingModel FIFO/LIFO and understand reliability limits
Membership modelingRepresent uniqueness, dedupe, tags, and relation indexes
Score modelingUse timestamps, ranks, priorities, deadlines, and composite members
Bound managementPrevent collection keys from growing forever
Java accessImplement repository methods with safe command usage
Atomic workflowsUse Lua for multi-step claim/ack/trim patterns
Failure modelingHandle lost workers, duplicate jobs, stale membership, hot keys, and large keys

A senior engineer does not ask “which Redis command?” first. They ask:

What invariant does the collection need to preserve?

2. Quick Decision Matrix

RequirementBest Redis structureWhy
FIFO queue with simple semanticsListMaintains insertion order, cheap push/pop
StackListPush and pop from same side
Unique membershipSetMember exists or does not exist
Deduplicate event IDsSetIdempotency by membership
Random member samplingSetNative random member operations
LeaderboardSorted SetUnique member ordered by score
Delayed job schedulerSorted SetScore is due timestamp
Sliding window rate limitSorted SetScore is request timestamp
Expiry indexSorted SetScore is expiration timestamp
Durable multi-consumer streamStreamConsumer groups and pending entries
Object fieldsHashNamed attributes of bounded object

A compact mental model:

Need order only?             List
Need uniqueness only?        Set
Need uniqueness + ordering?  Sorted Set
Need replay/consumer group?  Stream
Need object fields?          Hash

3. Redis Lists

A Redis List is an ordered sequence of string elements. It supports pushing and popping from both ends.

left/head                                      right/tail
  [ job-3, job-2, job-1, job-0 ]

Common commands:

CommandMeaning
LPUSHPush to left/head
RPUSHPush to right/tail
LPOPPop from left/head
RPOPPop from right/tail
BLPOPBlocking pop from left
BRPOPBlocking pop from right
LLENList length
LRANGERead range
LTRIMKeep only a range
LMOVEAtomically move element between lists
BLMOVEBlocking move between lists

3.1 FIFO queue with List

A simple FIFO queue:

LPUSH queue:email job-1
LPUSH queue:email job-2
RPOP queue:email

Or:

RPUSH queue:email job-1
RPUSH queue:email job-2
LPOP queue:email

Pick one convention and document it.

Producer: LPUSH
Consumer: RPOP

This gives FIFO behavior because older items move toward the right.

3.2 Blocking workers

Consumers can block until work arrives:

BRPOP queue:email 5

This avoids tight polling.

Operational notes:

ConcernRecommendation
TimeoutUse finite timeout, not infinite, so worker can check shutdown signal
PayloadStore job ID or compact payload, not huge JSON blobs
BackpressureMonitor LLEN
Poison jobsSimple List queues do not solve this alone
ReliabilityUse Streams or a reliable-queue pattern if jobs must not be lost

3.3 Java worker sketch

import io.lettuce.core.KeyValue;
import io.lettuce.core.api.sync.RedisCommands;
import java.time.Duration;

public final class EmailJobWorker implements Runnable {

    private final RedisCommands<String, String> redis;
    private final EmailJobHandler handler;
    private volatile boolean running = true;

    public EmailJobWorker(RedisCommands<String, String> redis, EmailJobHandler handler) {
        this.redis = redis;
        this.handler = handler;
    }

    @Override
    public void run() {
        while (running) {
            KeyValue<String, String> item = redis.brpop(5, "queue:email");
            if (item == null || !item.hasValue()) {
                continue;
            }

            String jobId = item.getValue();
            try {
                handler.handle(jobId);
            } catch (Exception ex) {
                // Do not silently drop failures in real systems.
                // Send to retry queue, dead-letter set, or a Stream-based pipeline.
                redis.lpush("queue:email:failed", jobId);
            }
        }
    }

    public void stop() {
        running = false;
    }
}

This is intentionally simple. It is not a fully reliable job system.


4. Reliability Limits of List Queues

A basic pop removes the job before processing:

RPOP queue:email

Failure window:

1. Worker pops job.
2. Redis removes job from queue.
3. Worker crashes before processing.
4. Job is lost unless stored elsewhere.

If losing a job is unacceptable, use one of these:

RequirementBetter design
Simple retry but not perfectList + processing list + reaper
Delayed retrySorted Set scheduler + worker
Multi-consumer durabilityRedis Streams consumer groups
Business-critical workflowDatabase/outbox/workflow engine plus Redis only as accelerator

4.1 Processing-list pattern

Use LMOVE to atomically move a job from ready queue to processing queue.

LMOVE queue:email queue:email:processing RIGHT LEFT

Flow:

A worker claims work:

LMOVE queue:email queue:email:processing RIGHT LEFT

After success:

LREM queue:email:processing 1 job-123

But this still has problems:

  • The processing list needs timestamps or external metadata.
  • Reaper logic needs to know which jobs are stale.
  • LREM can become expensive if the processing list is large.
  • Duplicate processing is still possible.

For serious job processing, Streams are usually a better Redis-native fit. Lists are good for simple bounded queues, local work buffers, and lightweight pipelines.


5. Lists for Recent Activity Windows

Lists are useful for keeping the most recent N items.

Example: last 100 viewed product IDs.

LPUSH user:{userId}:recent-products product-9
LTRIM user:{userId}:recent-products 0 99

Read:

LRANGE user:{userId}:recent-products 0 19

This is a bounded list pattern.

Important:

LPUSH + LTRIM should be treated as one logical operation.

If consistency matters, use Lua or a transaction.

Lua version:

local key = KEYS[1]
local value = ARGV[1]
local maxLen = tonumber(ARGV[2])

redis.call('LPUSH', key, value)
redis.call('LTRIM', key, 0, maxLen - 1)
return redis.call('LLEN', key)

Use cases:

recent products
recent searches
recent failed login IP hashes
recent validation errors
recent notification IDs

Do not use this for audit logs or compliance logs. Redis Lists are not a compliance-grade source of truth.


6. Redis Sets

A Redis Set is an unordered collection of unique string members.

feature:{featureId}:enabled-tenants
  tenant-a
  tenant-b
  tenant-c

Common commands:

CommandMeaning
SADDAdd one or more members
SREMRemove one or more members
SISMEMBERCheck membership
SMISMEMBERCheck multiple memberships
SCARDCount members
SMEMBERSReturn all members
SSCANIncremental scan
SINTERIntersection
SUNIONUnion
SDIFFDifference
SPOPRemove random member
SRANDMEMBERRead random member

6.1 Membership pattern

Feature enabled for tenants:

SADD feature:advanced-pricing:tenants tenant-a tenant-b
SISMEMBER feature:advanced-pricing:tenants tenant-a

Java repository:

public final class RedisFeatureMembershipRepository {

    private final RedisCommands<String, String> redis;

    public RedisFeatureMembershipRepository(RedisCommands<String, String> redis) {
        this.redis = redis;
    }

    public boolean isEnabledForTenant(String feature, String tenantId) {
        String key = "feature:" + feature + ":tenants";
        return redis.sismember(key, tenantId);
    }

    public void enableForTenant(String feature, String tenantId) {
        redis.sadd("feature:" + feature + ":tenants", tenantId);
    }

    public void disableForTenant(String feature, String tenantId) {
        redis.srem("feature:" + feature + ":tenants", tenantId);
    }
}

6.2 Deduplication pattern

Deduplicate event IDs for a bounded time window:

SADD dedupe:order-events:{yyyyMMddHH} event-123
EXPIRE dedupe:order-events:{yyyyMMddHH} 900

If SADD returns 1, this is the first time the event appeared in that bucket. If it returns 0, it is a duplicate.

Java sketch:

public boolean firstSeen(String bucketKey, String eventId, long ttlSeconds) {
    Long added = redis.sadd(bucketKey, eventId);
    redis.expire(bucketKey, ttlSeconds);
    return added != null && added == 1L;
}

But the SADD + EXPIRE sequence has a small failure window:

SADD succeeds.
Application crashes before EXPIRE.
Set can live forever.

Use Lua for stronger lifecycle:

local key = KEYS[1]
local member = ARGV[1]
local ttl = tonumber(ARGV[2])

local added = redis.call('SADD', key, member)
redis.call('EXPIRE', key, ttl)
return added

6.3 Set as secondary index

Example: order IDs by customer.

customer:{customerId}:open-orders
  order-1
  order-2
  order-3

Commands:

SADD customer:{customerId}:open-orders order-1
SREM customer:{customerId}:open-orders order-1
SMEMBERS customer:{customerId}:open-orders

This is fine only when cardinality is bounded. If a customer can have millions of orders, this is not a safe hot-path SMEMBERS pattern.

Use:

SSCAN customer:{customerId}:open-orders 0 COUNT 100

or better, a database index/search system for pagination and filtering.


7. Set Algebra for Real Systems

Sets can answer useful questions cheaply when sets are bounded.

Example:

user:{userId}:roles
  APPROVER
  SALES_ADMIN
  QUOTE_MANAGER

permission:approve-discount:roles
  APPROVER
  FINANCE_MANAGER

Check overlap:

SINTER user:{userId}:roles permission:approve-discount:roles

But be careful: role/permission checks on every request may be better represented as compact cached authorization context rather than repeated set algebra.

Good set algebra use cases:

admin tooling
small feature flags
small cohort intersection
debugging and reconciliation
bounded tenant segments

Poor set algebra use cases:

large user segmentation on every request
real-time analytics over huge sets
pagination by arbitrary filters
source-of-truth authorization decisions without auditability

8. Set Cardinality and Large-Key Risk

SCARD is safe for cardinality. SMEMBERS can be dangerous for large sets.

Bad:

SMEMBERS tenant:big-tenant:all-users

This can return millions of members.

Better:

SSCAN tenant:big-tenant:all-users 0 COUNT 500

But if your normal application flow needs pagination, filtering, sorting, and stable page tokens, Redis Set is not enough. Use a database/search index.

Rule:

Use Sets for membership and bounded relation indexes.
Do not use Sets as your primary query engine.

9. Redis Sorted Sets

A Sorted Set is a set of unique members ordered by score.

leaderboard:daily
  user-7  -> 9910
  user-2  -> 8820
  user-9  -> 1200

Properties:

member is unique
score is numeric
ordering is by score
members with equal score are ordered lexicographically

Common commands:

CommandMeaning
ZADDAdd or update member score
ZSCOREGet member score
ZINCRBYIncrement score
ZRANGERead by rank, score, or lex range depending on options
ZREVRANGEOlder command style for reverse rank reads
ZRANKRank low-to-high
ZREVRANKRank high-to-low
ZREMRemove member
ZCARDCount members
ZCOUNTCount score range
ZPOPMINPop lowest-score members
ZPOPMAXPop highest-score members
ZREMRANGEBYSCORERemove score range

Sorted Sets are the most versatile Redis structure after Strings and Hashes. They are also easy to abuse.


10. Score Design

The score gives a Sorted Set its meaning.

Common score types:

Use caseScore
LeaderboardPoints
Delayed jobDue timestamp in milliseconds
Rate limitRequest timestamp in milliseconds
Expiry indexExpiration timestamp
Priority queuePriority number
Aging priority queueComputed priority + time component
Recent activityActivity timestamp

10.1 Timestamp score

ZADD jobs:delayed 1783012441000 job-123

This means:

job-123 becomes eligible at timestamp 1783012441000

10.2 Leaderboard score

ZINCRBY leaderboard:daily 50 user-98172

This means:

increase user's score by 50

10.3 Tie handling

If multiple members have the same score, Redis orders by member lexicographically. If that is unacceptable, encode a tie-breaker in the member or score model.

Example member:

1783012441000:job-123

But avoid making score math unreadable. For scheduling, duplicate scores are usually acceptable.

10.4 Score precision

Sorted Set scores are floating-point numbers. Timestamp-in-milliseconds is safe for ordinary modern timestamps because it is below the exact integer limit of IEEE 754 double precision for now. But do not pack too much information into the score.

Bad:

score = timestampMs * 1000000 + priority * 1000 + shard

This can create precision and readability problems.

Better:

score = dueAtMs
member = priority:jobId or jobId with metadata elsewhere

If priority is essential, use separate queues per priority or a carefully documented score scheme.


11. Leaderboard Pattern

Daily leaderboard:

ZINCRBY leaderboard:game:{yyyyMMdd} 100 user-1
ZINCRBY leaderboard:game:{yyyyMMdd} 70 user-2
ZINCRBY leaderboard:game:{yyyyMMdd} 30 user-3

Top 10 high-to-low:

ZRANGE leaderboard:game:{yyyyMMdd} 0 9 REV WITHSCORES

User rank high-to-low:

ZREVRANK leaderboard:game:{yyyyMMdd} user-1

TTL:

EXPIRE leaderboard:game:{yyyyMMdd} 604800

11.1 Java example

import io.lettuce.core.ScoredValue;
import io.lettuce.core.Range;
import io.lettuce.core.api.sync.RedisCommands;
import java.util.List;

public final class RedisLeaderboardRepository {

    private final RedisCommands<String, String> redis;

    public RedisLeaderboardRepository(RedisCommands<String, String> redis) {
        this.redis = redis;
    }

    public double addScore(String leaderboardKey, String userId, double delta) {
        return redis.zincrby(leaderboardKey, delta, userId);
    }

    public List<ScoredValue<String>> top(String leaderboardKey, int limit) {
        return redis.zrevrangeWithScores(leaderboardKey, 0, limit - 1);
    }

    public Long rank(String leaderboardKey, String userId) {
        return redis.zrevrank(leaderboardKey, userId);
    }
}

11.2 Production notes

ConcernRecommendation
Reset periodUse period in key name
Historical queryPersist final result to database/object storage
Cheating/fraudDo not let Redis score be the only fraud-resistant source
Huge leaderboardPage with rank ranges; avoid full reads
Multi-regionDefine whether score update ordering matters

12. Delayed Job Scheduler with Sorted Set

A delayed job is naturally modeled as:

member = jobId
score  = dueAtMs

Add job:

ZADD jobs:delayed 1783012441000 job-123

Find due jobs:

ZRANGE jobs:delayed -inf 1783012441000 BYSCORE LIMIT 0 100

Remove after claim:

ZREM jobs:delayed job-123

But the two-step read/remove has a race. Multiple workers can see the same job. Use Lua to claim due jobs atomically.

12.1 Atomic claim Lua

local delayedKey = KEYS[1]
local processingKey = KEYS[2]
local now = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local visibilityUntil = tonumber(ARGV[3])

local jobs = redis.call('ZRANGE', delayedKey, '-inf', now, 'BYSCORE', 'LIMIT', 0, limit)
for _, job in ipairs(jobs) do
  redis.call('ZREM', delayedKey, job)
  redis.call('ZADD', processingKey, visibilityUntil, job)
end
return jobs

This moves due jobs from delayed to processing.

12.2 Job metadata

Do not store huge payload as ZSet member. Use job ID as member and store metadata elsewhere.

jobs:delayed                     # ZSet: jobId -> dueAtMs
job:{jobId}:payload              # String/Hash/JSON with TTL
jobs:processing                  # ZSet: jobId -> visibilityDeadlineMs

This separates scheduling from payload.

12.3 When to avoid this

Do not build a full enterprise workflow engine from ZSets if you need:

audit history
human approvals
complex compensation
multi-step state machine
exact transition log
cross-service transactional workflow

Use a workflow engine, database-backed job system, Kafka/RabbitMQ, or Redis Streams depending on the requirement. Sorted Set scheduling is excellent for lightweight due-time selection.


13. Sliding Window Rate Limiter with Sorted Set

A precise sliding window can be modeled as:

key    = rate:{subject}:{windowName}
member = unique request ID
score  = request timestamp in milliseconds

Algorithm:

1. Remove entries older than window.
2. Count remaining entries.
3. If count < limit, add current request.
4. Set/refresh TTL.
5. Return allowed/denied.

This must be atomic.

Lua:

local key = KEYS[1]
local now = tonumber(ARGV[1])
local windowMs = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local member = ARGV[4]
local ttlSeconds = tonumber(ARGV[5])

local minScore = now - windowMs
redis.call('ZREMRANGEBYSCORE', key, '-inf', minScore)

local count = redis.call('ZCARD', key)
if count >= limit then
  return {0, count}
end

redis.call('ZADD', key, now, member)
redis.call('EXPIRE', key, ttlSeconds)
return {1, count + 1}

Trade-offs:

DimensionConsequence
AccuracyPrecise per request
MemoryStores one member per request in window
CostCleanup and count per request
Hot key riskHigh for popular subjects
AlternativesToken bucket with counters, approximate counters, local pre-limit

This is correct but can be expensive at very high QPS. Part 017 will go deeper into rate limiting strategies.


14. Expiry Index with Sorted Set

Sometimes you need to find records whose expiration time has passed. Redis key TTL deletes keys, but it does not give you an ordered query of “which items are due”.

Use a Sorted Set index:

quote:draft:expiry-index
  quote-1 -> expiresAtMs
  quote-2 -> expiresAtMs

Add/update:

ZADD quote:draft:expiry-index 1783012441000 quote-1

Find expired:

ZRANGE quote:draft:expiry-index -inf 1783012441000 BYSCORE LIMIT 0 100

Cleanup:

ZREM quote:draft:expiry-index quote-1
DEL quote:{quote-1}:draft-state

Use Lua if the cleanup must be atomic per item.

Pattern:

This is useful for:

draft quote expiration
temporary reservation cleanup
pending approval timeout
forgotten checkout cleanup
soft workflow deadline scanning

15. Presence with Sets and Sorted Sets

Presence has two different meanings:

currently connected
recently active

Use Set for connected membership:

SADD room:{roomId}:connections connection-1
SREM room:{roomId}:connections connection-1

Use Sorted Set for last-seen:

ZADD user:presence:last-seen 1783012441000 user-98172

Query active within last 30 seconds:

ZRANGE user:presence:last-seen 1783012411000 +inf BYSCORE

Do not rely only on disconnect events. Connections disappear without graceful cleanup. Use heartbeat timestamps and cleanup thresholds.


16. Java Repository Design for Collections

Avoid exposing raw Redis commands across business logic. Create domain repositories.

Bad:

redis.zadd("jobs:delayed", dueAtMs, jobId);

Better:

public interface DelayedJobScheduler {
    void schedule(JobId jobId, Instant dueAt);
    List<JobId> claimDueJobs(Instant now, int limit, Duration visibilityTimeout);
    void ack(JobId jobId);
    void retry(JobId jobId, Instant nextDueAt);
}

Implementation can use Sorted Sets, Streams, or database later without changing business logic.

16.1 Keep commands behind methods named by intent

Redis commandDomain method
SADDmarkEventSeen(eventId)
SISMEMBERhasSeenEvent(eventId)
ZADDschedule(jobId, dueAt)
ZPOPMINclaimLowestPriority(limit)
LPUSHappendRecentActivity(activityId)
LTRIMhidden inside appendRecentActivity

This keeps Redis a storage mechanism, not your domain language.


17. Bounded Collection Rules

Every Redis collection key needs a bound strategy.

StructureBound strategy
ListLTRIM, TTL, max enqueue policy
SetTTL, bucket by time, max cardinality, cleanup job
Sorted SetZREMRANGEBYSCORE, TTL, period keys, max rank trim

Examples:

LTRIM user:{id}:recent-searches 0 99
EXPIRE dedupe:events:{yyyyMMddHH} 7200
ZREMRANGEBYSCORE rate:{userId}:api -inf 1783012145000
ZREMRANGEBYRANK leaderboard:daily 0 -10001

Production invariant:

No Redis collection may grow forever unless it is explicitly approved as a capacity-managed index.

18. Hot Key and Sharding Patterns

Collection keys often become hot.

Example:

leaderboard:global

Every score update hits one key.

Mitigations:

ProblemMitigation
High write QPS leaderboardShard by game/region/time bucket, merge periodically
Popular queuePartition queue by shard/key range
Huge dedupe setBucket by time and shard
Large presence setPartition by room/server/tenant
Hot rate limit keyLocal pre-limit + Redis global limit, or subject sharding when valid

Example sharded dedupe:

dedupe:payment-events:{yyyyMMddHH}:00
dedupe:payment-events:{yyyyMMddHH}:01
dedupe:payment-events:{yyyyMMddHH}:02

Choose shard by stable hash of event ID.

Do not shard when exact global ordering is required unless you also design a merge model.


19. Multi-Key and Redis Cluster Constraints

Many collection workflows involve multiple keys:

queue ready -> processing
job metadata -> scheduler index
session state -> recent activity list

In Redis Cluster, multi-key operations require keys to be in the same hash slot. Use hash tags intentionally.

Example:

job:{jobId}:payload
job:{jobId}:attempts

These two keys are co-located because {jobId} is the hash tag.

For a scheduler, however, this may not help because the scheduler index is global:

jobs:delayed
job:{jobId}:payload

If a Lua script touches both, it may not be cluster-safe.

Options:

OptionTrade-off
Keep scheduler per shardMore operational complexity, cluster-safe locality
Keep payload embedded in memberSimpler but payload size risk
Use database for payloadMore durable and cluster-safe if Redis only stores IDs
Use StreamsBetter for consumer group processing

Cluster thinking must happen during data modeling, not after production traffic arrives.


20. Lists vs Streams for Queues

Lists can implement queues. Streams are often better for durable multi-consumer event/job pipelines.

NeedListStream
Simple FIFOGoodGood
Blocking readGoodGood
Consumer groupsNoYes
Pending entriesManualBuilt-in
ReplayManualBuilt-in-ish by ID
Message IDsManualBuilt-in
Operational simplicitySimpleMore concepts
Serious retry pipelineManualBetter fit

Use Lists when:

queue is simple
loss or duplicate is acceptable or externally handled
single-consumer or simple worker pool is enough
payload is small
operational scope is limited

Use Streams when:

consumer groups matter
replay matters
pending entries matter
job processing status matters
multiple independent consumers need the same event

Part 007 covers Streams deeply.


21. Failure Modeling

21.1 List failure modes

FailureResultMitigation
Worker crashes after RPOPJob lostUse LMOVE processing list or Streams
Worker stuck after claimJob never completesVisibility timeout and reaper
Queue grows faster than workersMemory pressureBackpressure, autoscaling, reject policy
Poison job retries foreverInfinite churnRetry count and dead-letter
Huge payload in listMemory/network spikeStore job ID and payload elsewhere

21.2 Set failure modes

FailureResultMitigation
SADD without TTLPermanent dedupe garbageLua with EXPIRE
Unbounded setLarge keyBucket, shard, TTL, database
SMEMBERS on huge setLatency spikeUse SSCAN or different storage
Stale membershipWrong authorization/feature decisionTTL/invalidation/source-of-truth check

21.3 Sorted Set failure modes

FailureResultMitigation
Non-atomic due job claimDuplicate processingLua claim or Streams
Old items never removedMemory growthZREMRANGEBYSCORE cleanup
Score precision abuseWrong orderingKeep score simple
Huge global ZSetHot key and memory pressureShard or periodize
Clock skewEarly/late schedulingUse server-side time or bounded clock assumptions

22. Observability

Metrics by structure:

22.1 Lists

redis.queue.email.length
redis.queue.email.pop.latency
redis.queue.email.failed.count
redis.queue.email.processing.length
redis.queue.email.requeue.count

22.2 Sets

redis.set.dedupe.cardinality
redis.set.dedupe.first_seen.count
redis.set.dedupe.duplicate.count
redis.set.membership.lookup.latency
redis.set.large_key.detected.count

22.3 Sorted Sets

redis.zset.scheduler.delayed.count
redis.zset.scheduler.due.count
redis.zset.scheduler.claim.latency
redis.zset.scheduler.processing.count
redis.zset.scheduler.visibility_expired.count
redis.zset.leaderboard.cardinality

Also log key pattern, not raw full key when it may contain identifiers or sensitive data.

Example:

{
  "event": "redis_zset_due_jobs_claimed",
  "keyPattern": "jobs:delayed:{shard}",
  "claimedCount": 100,
  "nowMs": 1783012441000
}

23. Production Recipes

23.1 Recent searches

user:{userId}:recent-searches

Commands:

LPUSH user:{userId}:recent-searches "redis cluster"
LTRIM user:{userId}:recent-searches 0 19
EXPIRE user:{userId}:recent-searches 2592000

Use Lua to combine push/trim/expire.

23.2 Idempotency dedupe bucket

idempotency:{service}:{yyyyMMddHH}:{shard}

Commands:

SADD idempotency:payment:2026070210:03 event-123
EXPIRE idempotency:payment:2026070210:03 7200

Use Lua to avoid missing TTL.

23.3 Leaderboard

leaderboard:{gameId}:{yyyyMMdd}

Commands:

ZINCRBY leaderboard:game-1:20260702 25 user-7
ZRANGE leaderboard:game-1:20260702 0 9 REV WITHSCORES

23.4 Delayed retry

retry:{pipeline}:delayed

Commands:

ZADD retry:quote-pricing:delayed 1783012441000 job-123
ZRANGE retry:quote-pricing:delayed -inf 1783012441000 BYSCORE LIMIT 0 100

Use Lua claim.

23.5 Online presence

room:{roomId}:connections
user:presence:last-seen

Commands:

SADD room:{roomId}:connections connection-123
ZADD user:presence:last-seen 1783012441000 user-98172

Use heartbeat and cleanup.


24. Practice: Build Three Collection Components

Spend 90-120 minutes.

Component A — Recent activity list

Implement:

void addRecentActivity(UserId userId, ActivityId activityId);
List<ActivityId> recentActivities(UserId userId, int limit);

Constraints:

max 100 activities
TTL 30 days
atomic LPUSH + LTRIM + EXPIRE
no duplicate removal required

Component B — Event dedupe set

Implement:

boolean firstSeen(EventId eventId, Instant eventTime);

Constraints:

bucket by hour
shard by event ID hash
TTL 2 hours
SADD and EXPIRE must be atomic

Component C — Delayed retry scheduler

Implement:

void schedule(JobId jobId, Instant dueAt);
List<JobId> claimDue(Instant now, int limit, Duration visibilityTimeout);
void ack(JobId jobId);
void retry(JobId jobId, Instant nextDueAt);

Constraints:

Sorted Set score = dueAtMs
claim must be atomic
processing set uses visibility deadline
payload is not stored in ZSet member

Feedback questions

What is the maximum size of each Redis key?
What happens if the worker crashes after claim?
What happens if Redis loses the collection?
What metrics show backlog, duplicates, and stale processing jobs?
Is this still valid under Redis Cluster?

25. Summary

Lists, Sets, and Sorted Sets are Redis collection primitives. They become production-grade only when paired with clear invariants and cleanup rules.

Use this model:

List       -> ordered sequence, simple queues, recent N items
Set        -> uniqueness, membership, dedupe, bounded relation indexes
Sorted Set -> score-ordered uniqueness, ranking, scheduling, expiry indexes, sliding windows

The two most important operational rules:

1. Every collection needs a bound strategy.
2. Every multi-step collection workflow needs explicit failure handling.

Next, we move to Redis Streams, which are built for append-only event-like data, consumer groups, pending entries, replay, and more serious worker pipelines.


References

Lesson Recap

You just completed lesson 06 in start here. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.