String Operations

Strings are Redis’s most versatile type — counters, locks, serialized objects, and bitmaps all use strings under the hood.

  # Basic get/set
SET product:42:name "Wireless Mouse"
GET product:42:name

# Conditional set — distributed locking foundation
SETNX lock:resource "owner-1"
SET product:42:stock 100 XX   # only if key exists
SET product:99:stock 50 NX    # only if key does NOT exist

# Atomic numeric operations
INCR api:rate:user:1001
INCRBY page:views:daily 1
DECRBY inventory:sku:99 3
INCRBYFLOAT price:sku:99 -0.50

# Multi-key atomic operations
MSET user:1:name "Alice" user:2:name "Bob"
MGET user:1:name user:2:name

GETSET counter:last_id 42   # returns old value, sets new
  

String as JSON Cache

  import json
redis.setex(f"product:{pid}", 600, json.dumps(product_dict))
data = json.loads(redis.get(f"product:{pid}") or "null")
  

Prefer hashes when individual fields update frequently.

Expiration and TTL

Every cache key should have a deliberate TTL strategy.

  SET cache:homepage "<html>...</html>"
EXPIRE cache:homepage 300
TTL cache:homepage          # seconds remaining; -1 = no expiry; -2 = key missing

SETEX cache:api:users 60 "[...]"   # SET + EXPIRE atomically
PERSIST cache:homepage             # remove expiration

# Check expiry type
TTL session:abc     # seconds
PTTL session:abc    # milliseconds
  

Use SETEX or SET ... EX n instead of separate SET + EXPIRE to avoid race conditions where a key exists without TTL.

Hash Field Operations

Hashes store field-value pairs — ideal for objects and sessions.

  HSET user:1001 name "Alice" country "US" login_count 0
HGET user:1001 name
HEXISTS user:1001 phone
HDEL user:1001 country
HKEYS user:1001
HVALS user:1001
HLEN user:1001
HINCRBY user:1001 login_count 1
HSETNX user:1001 verified 1
  

Partial Updates Without Read-Modify-Write

  # Update single field — no need to fetch entire object
redis.hset(f"user:{user_id}", "last_seen", int(time.time()))
redis.hincrby(f"user:{user_id}", "page_views", 1)
  

Hash vs String Memory

For objects with fewer than ~100 fields and small values, hashes use listpack encoding and consume less memory than equivalent JSON strings.

List as Queue

Lists implement FIFO queues with O(1) push/pop at ends.

  # Producer
LPUSH job:queue "job-001" "job-002"

# Consumer — non-blocking
RPOP job:queue

# Consumer — blocking (efficient polling)
BRPOP job:queue 30    # wait up to 30 seconds

# Reliable queue — move to processing list atomically
RPOPLPUSH job:queue job:processing
# After successful processing:
LREM job:processing 1 "job-001"
  

Reliable Queue Pattern

  Producer → LPUSH queue → Worker BRPOP/RPOPLPUSH → processing list → ACK (LREM)
  

If a worker crashes, jobs remain in job:processing for recovery scans.

List as Stack

  LPUSH undo:stack "action1"
LPUSH undo:stack "action2"
LPOP undo:stack   # returns "action2" (LIFO)
  

List as Recent Activity Feed

  LPUSH user:1001:activity "viewed product:42"
LTRIM user:1001:activity 0 49    # keep only 50 items
LRANGE user:1001:activity 0 9    # latest 10
  

Trim and Inspect

  LTRIM recent:searches 0 49
LLEN recent:searches
LINDEX recent:searches 0
LINSERT recent:searches BEFORE "query-a" "query-new"
  

Pipelining

Batch commands to reduce round-trip latency. Each command still executes atomically on its own — pipelining is not a transaction.

  pipe = redis.pipeline()
pipe.set("key1", "val1")
pipe.incr("counter")
pipe.hset("user:1", "name", "Alice")
pipe.expire("key1", 300)
results = pipe.execute()
  
  # redis-cli pipelining
echo -e "SET k1 v1\nINCR c1\nGET k1" | redis-cli --pipe
  

Pipelining 1,000 commands can reduce total latency from seconds to milliseconds compared to individual round trips.

Transactions (MULTI/EXEC)

  MULTI
INCR account:1001:balance
DECR account:1002:balance
EXEC
  

Transactions queue commands — they do not roll back on failure mid-EXEC. Use Lua scripts for conditional logic.

Best Practices

  1. Always set TTL on cache keys at creation
  2. Use BRPOP instead of polling with sleep loops
  3. Keep list lengths bounded with LTRIM for activity feeds
  4. Use pipelining for bulk inserts (thousands of keys)
  5. Prefer hashes for session data with multiple fields

Common Mistakes

Mistake Impact
SET then EXPIRE separately Window where key has no TTL
Unbounded list growth Memory exhaustion
Polling RPOP in a tight loop Wastes CPU and connections
Storing multi-MB strings Blocks event loop during read/write
Using lists for membership tests O(N) instead of O(1) with sets

Troubleshooting

Blocking clients pile up:

  INFO clients
# High blocked_clients from BRPOP — normal for workers; ensure enough consumers
CLIENT LIST
  

Queue jobs stuck in processing:

  LLEN job:processing
LRANGE job:processing 0 -1
# Requeue stale jobs with a scheduled recovery script
  

TTL returns -1 unexpectedly:

  TTL mykey
# Key exists without expiry — add EXPIRE or investigate creation path
  

Performance Tips

  • MGET / HMGET for batch reads instead of loops
  • Cap list sizes with LTRIM after every LPUSH in hot paths
  • Use connection pooling — one pool per application process
  • Monitor slowlog for unexpectedly slow LRANGE on large lists

Production Scenario

An email delivery service processed 200K jobs/hour using BRPOP workers with RPOPLPUSH to a processing list. Crash recovery scanned job:processing every 5 minutes, requeuing jobs older than 10 minutes. Pipelined batch enqueues (LPUSH × 100) cut enqueue latency from 800ms to 12ms during traffic spikes.

Strings, hashes, and lists cover the majority of Redis application patterns — master these before moving to sets, streams, and advanced caching strategies.